Versie 0.2.0
This commit is contained in:
commit
e77d8fd373
495 files changed
+81026
No files matched your search
@@ -0,0 +1,114 @@
|
||||
// Generated, do not edit. sha256:958c567a3f8a5513bb292a105e1d9e3838bf252c71b7a83188de3f33183e7f66
|
||||
import { DatabaseSync } from 'node:sqlite';
|
||||
|
||||
import { SCHEMA as BASELINE_SCHEMA } from '../../server/db/schema.js';
|
||||
import { CONFIG } from '../../shared/config.js';
|
||||
|
||||
const NU = '2026-08-12T10:00:00.000Z';
|
||||
|
||||
export function openBaselineDb() {
|
||||
const db = new DatabaseSync(':memory:', { enableForeignKeyConstraints: true });
|
||||
db.exec(`PRAGMA foreign_keys = ON`);
|
||||
db.exec(BASELINE_SCHEMA);
|
||||
return db;
|
||||
}
|
||||
|
||||
export function foreignKeysAan(db) {
|
||||
return db.prepare(`PRAGMA foreign_keys`).get().foreign_keys === 1;
|
||||
}
|
||||
|
||||
const FIXTURE = [
|
||||
[`Asset`, `(symbol, name, searchName, description, unit, subunit, displayUnit, decimals, kind)
|
||||
VALUES ('BTC','Bitcoin','bitcoin',NULL,'BTC','sat','subunit',8,'crypto')`],
|
||||
[`Account`, `(name, searchName, description, kind, isOwnCustody, scanEnabled, isActive, closedAt)
|
||||
VALUES ('Kluis','kluis',NULL,'wallet',1,0,1,NULL)`],
|
||||
[`Label`, `(name, searchName, description) VALUES ('Werk','werk',NULL)`],
|
||||
[`MetalProduct`, `(assetId, name, searchName, description, fineGrams, form)
|
||||
VALUES (1,'Baar 1 kg','baar 1 kg',NULL,10000000000,'bar')`],
|
||||
[`Provider`, `(key, name, searchName, description, baseUrl, authenticationHeaderName, authenticationParameterName,
|
||||
authenticationKind, authenticationRequired, isActive, isSeed, isSystem, createdAt)
|
||||
VALUES ('mempool','mempool','mempool',NULL,'https://voorbeeld.test',NULL,NULL,'none',0,1,1,0,'${NU}')`],
|
||||
[`ProviderAuthenticationHmac`, `(providerId, messageParts, keyHeader, signatureHeader, timestampHeader,
|
||||
windowHeader, windowMs, hashAlgorithm, encoding)
|
||||
VALUES (1,'timestamp,method','K','S','T',NULL,NULL,'SHA-256','hex')`],
|
||||
[`ProviderEndpoint`, `(providerId, pathTemplate, queryTemplate, capability, method)
|
||||
VALUES (1,'/api/prijs',NULL,'spot','GET')`],
|
||||
[`ProviderFixedRate`, `(providerId, fiatPrice) VALUES (1,1.0)`],
|
||||
[`ProviderCallLimit`, `(providerId, callsPerMonth, createdAt) VALUES (1,100,'${NU}')`],
|
||||
[`ProviderInputMap`, `(providerId, symbol, externalId, isCatchAll) VALUES (1,'XAU','gold',0)`],
|
||||
[`ProviderOutputMap`, `(providerId, jsonPath, capability, internalField, transform)
|
||||
VALUES (1,'EUR','spot','price','none')`],
|
||||
[`ProviderCoverage`, `(providerId, symbol) VALUES (1,'BTC')`],
|
||||
[`ApiCallMetric`, `(providerId, capability, bucketMinute, okCount, failCount, sumLatencyMs, maxLatencyMs)
|
||||
VALUES (1,'spot','2026-08-12T10:00',1,0,120,120)`],
|
||||
[`Connection`, `(name, searchName, description, url, kind, isActive, isSystem, lastStatus,
|
||||
lastCheckedAt, createdAt, updatedAt)
|
||||
VALUES ('Self-hosted','self-hosted',NULL,'https://knoop.test','btc_explorer',1,0,NULL,NULL,'${NU}','${NU}')`],
|
||||
[`WalletSource`, `(accountId, connectionId, value, name, kind, scriptType, createdAt)
|
||||
VALUES (1,NULL,'xpub6voorbeeld','Hoofdbron','xpub','p2tr','${NU}')`],
|
||||
[`WalletAddress`, `(accountId, sourceId, address, automaticLabel, chainIndex, derivationIndex,
|
||||
balanceSats, transactionCount, lastScannedAt)
|
||||
VALUES (1,1,'bc1qvoorbeeld',NULL,0,0,1000,2,'${NU}')`],
|
||||
[`WalletTransaction`, `(accountId, sourceId, labelId, layer, txid, address, valueSats, feeSats, blockHeight,
|
||||
blockTime, direction, createdAt)
|
||||
VALUES (1,1,1,'onchain','aabbcc','bc1qvoorbeeld',1000,10,900000,'${NU}','in','${NU}')`],
|
||||
[`Event`, `(assetId, fromAccountId, toAccountId, productId, productCount, importReference, externalAddress,
|
||||
note, amount, fiatPrice, fiatFee, unitFee, type, source, occurredAt, createdAt)
|
||||
VALUES (1,1,1,1,1,NULL,NULL,NULL,10000000000,50000.0,NULL,NULL,'buy','manual','${NU}','${NU}')`],
|
||||
[`Valuation`, `(assetId, accountId, balance, fiatRate, source, valuedOn, rateAt)
|
||||
VALUES (1,1,1000,50000.0,'manual','2026-08-12','${NU}')`],
|
||||
[`ScanLog`, `(accountId, addressesFound, balanceFound, proposalsCreated, rawData, status, scannedAt)
|
||||
VALUES (1,2,1000,0,NULL,'success','${NU}')`],
|
||||
[`ReconcileProposal`, `(accountId, assetId, walletTransactionId, resolvedEventId, transactionHash, amount,
|
||||
direction, suggestedType, status, detectedAt)
|
||||
VALUES (1,1,1,1,'aabbcc',1000,'in','transfer','pending','${NU}')`],
|
||||
[`WalletTransactionEventLink`, `(walletTransactionId, eventId, method, createdAt) VALUES (1,1,'exact','${NU}')`],
|
||||
[`ImportBatch`, `(accountId, filename, rowCount, rawData, origin, importedAt)
|
||||
VALUES (1,'export.csv',3,NULL,'Bitvavo','${NU}')`],
|
||||
[`PriceHistory`, `(assetId, providerId, fiatPrice, resolution, observedAt, updatedAt)
|
||||
VALUES (1,1,50000.0,'day','${NU}','${NU}')`],
|
||||
[`InterestSchedule`, `(accountId, principalAssetId, payoutAssetId, note, principalAmount, annualRate,
|
||||
rateKind, creditCadence, creditWeekday, creditDayOfMonth, startsOn, endsOn, compounding, createdAt)
|
||||
VALUES (1,1,1,NULL,100000,0.05,'fixed','weekly',1,NULL,'2026-01-01',NULL,0,'${NU}')`],
|
||||
[`InterestPeriod`, `(scheduleId, startsOn, annualRate)
|
||||
VALUES (1,'2026-04-01',0.037)`],
|
||||
[`InterestCredit`, `(scheduleId, eventId, status, periodStart, periodEnd, createdAt)
|
||||
VALUES (1,1,'booked','2026-08-01','2026-08-08','${NU}')`],
|
||||
[`Notification`, `(count, highWater, kind, dismissedAt, createdAt)
|
||||
VALUES (1,3,'reconcile',NULL,'${NU}')`],
|
||||
[`PriceSource`, `(assetId, providerId, capability, rank, status)
|
||||
VALUES (1,1,'spot',1,'confirmed')`],
|
||||
[`DashboardProfile`, `(name, searchName, description, sortOrder, isActive, isSystem, createdAt, updatedAt)
|
||||
VALUES ('Standaard','standaard',NULL,0,1,1,'${NU}','${NU}')`],
|
||||
[`DashboardWidget`, `(profileId, assetId, instanceKey, gridX, gridY, spanWidth, spanHeight, type, settings)
|
||||
VALUES (1,1,'spot:1',0,0,2,2,'spot',NULL)`],
|
||||
[`Preference`, `(scope, key, value) VALUES ('ui','theme','"dark"')`],
|
||||
[`AiToolCall`, `(sessionId, client, tool, arguments, fieldsShared, charactersOut, outcome, detail, occurredAt)
|
||||
VALUES ('11111111-2222-3333-4444-555555555555','voorbeeld-client','connection_test','{}','[]',42,'ok',NULL,'${NU}')`],
|
||||
[`AiToolCallMessage`, `(callId, kind, text, occurredAt)
|
||||
VALUES (1,'request','{"note":"hallo"}','${NU}')`],
|
||||
[`BusSignal`, `(name, occurredAt) VALUES ('providers-changed','${NU}')`],
|
||||
[`Command`, `(kind, payload, state, expiresAt, createdAt)
|
||||
VALUES ('navigate','{"route":"system","tab":"ai"}','open','${NU}','${NU}')`],
|
||||
[`ChangeLog`, `(tableName, rowId, operation, origin, originId, changes, occurredAt)
|
||||
VALUES ('Account',1,'update','manual',NULL,'{"name":["Oude naam","Nieuwe naam"]}','${NU}')`],
|
||||
[`AiProposal`, `(kind, assetId, fiatPrice, observedAt, note, status, createdAt)
|
||||
VALUES ('price_point',1,50000.0,'${NU}','premie volgens de muntwinkel','pending','${NU}')`],
|
||||
[`HygieneAcceptance`, `(kind, fingerprint, summary, note, createdAt)
|
||||
VALUES ('transfer_without_counterpart','abc123','Verplaatsing zonder tegenkant (1)',NULL,'${NU}')`],
|
||||
[`IgnoredPriceGap`, `(assetId, fromDay, untilDay, createdAt) VALUES (1,'1994-01-01','1994-01-31','${NU}')`],
|
||||
[`PriceAbsence`, `(assetId, providerId, day, createdAt) VALUES (1,1,'2025-12-26','${NU}')`],
|
||||
[`IntegrationCall`, `(target, providerKey, operation, subject, outcome, httpStatus, errorKey, detail, latencyMs, requestId, occurredAt)
|
||||
VALUES ('service',NULL,'/v1/series','crypto:BTC','fail',404,'marketService.rejected api.noSeries','No series',42,'a1b2c3d4-0000-4000-8000-000000000000','${NU}')`],
|
||||
[`AssetNote`, `(assetId, body, createdAt) VALUES (1,'2026-09-26 14:32 geërfd van oma','${NU}')`],
|
||||
[`Schema`, `(id, version) VALUES (1,${CONFIG.SCHEMA_VERSION})`],
|
||||
[`Portfolio`, `(id, baseCurrency, createdAt) VALUES (1,'EUR','${NU}')`],
|
||||
];
|
||||
|
||||
export function vulFixture(db) {
|
||||
for (const [tabel, rest] of FIXTURE) db.exec(`INSERT INTO ${tabel} ${rest}`);
|
||||
return FIXTURE.map(([tabel]) => tabel);
|
||||
}
|
||||
|
||||
export const FIXTURE_TABELLEN = FIXTURE.map(([tabel]) => tabel);
|
||||
export const FIXTURE_TIJD = NU;
|
||||
@@ -0,0 +1,80 @@
|
||||
// Generated, do not edit. sha256:82c1a99dd9bcb14921519651f00e41b83ebb6753c9cc496d2de5d3792c7a8814
|
||||
import { DatabaseSync } from 'node:sqlite';
|
||||
import { SCHEMA, createDatabase as echteCreateDatabase } from '../../server/db.js';
|
||||
import { CONFIG, PREFERENCES } from '../../shared/config.js';
|
||||
|
||||
const bind = params => params.map(v => {
|
||||
if (v === undefined) return null;
|
||||
if (typeof v === 'boolean') return v ? 1n : 0n;
|
||||
if (typeof v === 'number' && Number.isInteger(v)) return BigInt(v);
|
||||
return v;
|
||||
});
|
||||
|
||||
export function createDbHarness({ seedPreferences = false } = {}) {
|
||||
let sqlite;
|
||||
let calls;
|
||||
|
||||
function fresh() {
|
||||
sqlite = new DatabaseSync(':memory:', { enableForeignKeyConstraints: true });
|
||||
sqlite.exec(SCHEMA);
|
||||
sqlite.prepare(`INSERT INTO Schema (version) VALUES (?)`).run(CONFIG.SCHEMA_VERSION);
|
||||
sqlite.prepare(`INSERT INTO Portfolio (id, baseCurrency, createdAt) VALUES (1, ?, ?)`)
|
||||
.run(CONFIG.PORTFOLIO.DEFAULT_BASE_CURRENCY, '2026-01-01T00:00:00.000Z');
|
||||
calls = { run: 0, persist: 0, seedPriceSource: [] };
|
||||
if (seedPreferences) seedPrefs();
|
||||
}
|
||||
|
||||
function seedPrefs() {
|
||||
for (const [scope, keys] of Object.entries(PREFERENCES)) {
|
||||
for (const [key, spec] of Object.entries(keys)) {
|
||||
if (spec.secret) continue;
|
||||
api.run(`INSERT INTO Preference (scope, key, value) VALUES (?,?,?)`,
|
||||
[scope, key, JSON.stringify(spec.initial)]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const api = {
|
||||
run(sql, params = []) {
|
||||
calls.run++;
|
||||
if (params.length === 0) sqlite.exec(sql);
|
||||
else sqlite.prepare(sql).run(...bind(params));
|
||||
},
|
||||
all(sql, params = []) {
|
||||
return sqlite.prepare(sql).all(...bind(params)).map(row => ({ ...row }));
|
||||
},
|
||||
one(sql, params = []) {
|
||||
const rows = api.all(sql, params);
|
||||
return rows.length ? rows[0] : null;
|
||||
},
|
||||
lastId() {
|
||||
const r = api.one(`SELECT last_insert_rowid() AS id`);
|
||||
return r ? r.id : null;
|
||||
},
|
||||
schemaVersion() {
|
||||
const r = api.one(`SELECT version FROM Schema`);
|
||||
return r ? r.version : null;
|
||||
},
|
||||
async persist() { calls.persist++; },
|
||||
seedPriceSource(assetId, status) { calls.seedPriceSource.push([assetId, status]); },
|
||||
|
||||
async init() { return sqlite; },
|
||||
exportBytes() { throw new Error('db-node-harness: exportBytes() niet ondersteund in tests.'); },
|
||||
async reset() { throw new Error('db-node-harness: reset() niet ondersteund; gebruik harness.recreate().'); },
|
||||
async importBytes() { throw new Error('db-node-harness: importBytes() niet ondersteund in tests.'); },
|
||||
createDatabase: echteCreateDatabase,
|
||||
async idbGet(key) { return idb.has(key) ? idb.get(key) : null; },
|
||||
async idbPut(key, value) { idb.set(key, value); },
|
||||
};
|
||||
|
||||
const idb = new Map();
|
||||
|
||||
fresh();
|
||||
|
||||
return {
|
||||
exports: api,
|
||||
recreate() { sqlite.close(); idb.clear(); fresh(); },
|
||||
get sqlite() { return sqlite; },
|
||||
get calls() { return calls; },
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,459 @@
|
||||
// Generated, do not edit. sha256:db0c8db151ef8689662b07ab525d02d912551c1706e34e778f8bcf4190dc7dc0
|
||||
const VOID = new Set(['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'source', 'track', 'wbr']);
|
||||
const RAW_TEXT = new Set(['textarea', 'script', 'style', 'title']);
|
||||
const SELF_CLOSING_OK = new Set(['svg', 'path', 'line', 'circle', 'rect', 'polyline', 'polygon', 'g', 'use']);
|
||||
|
||||
const ENTITIES = { amp: '&', lt: '<', gt: '>', quot: '"', apos: "'" };
|
||||
export function decodeEntities(s) {
|
||||
return s.replace(/&(#x[0-9a-f]+|#\d+|[a-z]+);/gi, (all, name) => {
|
||||
if (name[0] === '#') {
|
||||
const code = name[1] === 'x' || name[1] === 'X' ? parseInt(name.slice(2), 16) : parseInt(name.slice(1), 10);
|
||||
return Number.isFinite(code) ? String.fromCodePoint(code) : all;
|
||||
}
|
||||
return Object.prototype.hasOwnProperty.call(ENTITIES, name.toLowerCase()) ? ENTITIES[name.toLowerCase()] : all;
|
||||
});
|
||||
}
|
||||
const escText = s => String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
const escAttr = s => String(s).replace(/&/g, '&').replace(/"/g, '"');
|
||||
|
||||
const hidden = (obj, key, value) => Object.defineProperty(obj, key, { value, writable: true, enumerable: false, configurable: true });
|
||||
|
||||
class Node {
|
||||
constructor(doc) { hidden(this, 'ownerDocument', doc); hidden(this, 'parentNode', null); this.childNodes = []; }
|
||||
get parentElement() { return this.parentNode instanceof Element ? this.parentNode : null; }
|
||||
get isConnected() {
|
||||
let n = this;
|
||||
while (n) { if (n === this.ownerDocument) return true; n = n.parentNode; }
|
||||
return false;
|
||||
}
|
||||
appendChild(child) {
|
||||
if (child.parentNode) child.parentNode._removeChild(child);
|
||||
child.parentNode = this;
|
||||
this.childNodes.push(child);
|
||||
return child;
|
||||
}
|
||||
append(...children) { for (const c of children) this.appendChild(c); }
|
||||
_removeChild(child) {
|
||||
const i = this.childNodes.indexOf(child);
|
||||
if (i >= 0) this.childNodes.splice(i, 1);
|
||||
child.parentNode = null;
|
||||
}
|
||||
remove() { if (this.parentNode) this.parentNode._removeChild(this); }
|
||||
contains(other) {
|
||||
for (let n = other; n; n = n.parentNode) if (n === this) return true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
class Text extends Node {
|
||||
constructor(doc, data) { super(doc); this.nodeType = 3; this.data = data; }
|
||||
get textContent() { return this.data; }
|
||||
set textContent(v) { this.data = String(v); }
|
||||
}
|
||||
|
||||
class ClassList {
|
||||
constructor(el) { this.el = el; }
|
||||
_get() { return (this.el.getAttribute('class') || '').split(/\s+/).filter(Boolean); }
|
||||
_set(list) { this.el.setAttribute('class', list.join(' ')); }
|
||||
contains(c) { return this._get().includes(c); }
|
||||
add(...cs) { const l = this._get(); for (const c of cs) if (c && !l.includes(c)) l.push(c); this._set(l); }
|
||||
remove(...cs) { this._set(this._get().filter(c => !cs.includes(c))); }
|
||||
toggle(c, force) {
|
||||
const on = force === undefined ? !this.contains(c) : !!force;
|
||||
if (on) this.add(c); else this.remove(c);
|
||||
return on;
|
||||
}
|
||||
}
|
||||
|
||||
const kebab = s => s.replace(/[A-Z]/g, m => '-' + m.toLowerCase());
|
||||
const camel = s => s.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
||||
|
||||
function makeStyle() {
|
||||
const props = new Map();
|
||||
return new Proxy({
|
||||
setProperty(k, v) { props.set(k, String(v)); },
|
||||
getPropertyValue(k) { return props.get(k) ?? ''; },
|
||||
removeProperty(k) { props.delete(k); },
|
||||
}, {
|
||||
get(target, key) { return key in target ? target[key] : (props.get(kebab(String(key))) ?? ''); },
|
||||
set(_t, key, v) { props.set(kebab(String(key)), String(v)); return true; },
|
||||
});
|
||||
}
|
||||
|
||||
export class Element extends Node {
|
||||
constructor(doc, tag) {
|
||||
super(doc);
|
||||
this.nodeType = 1;
|
||||
this.localName = tag.toLowerCase();
|
||||
this.attrs = new Map();
|
||||
hidden(this, 'style', makeStyle());
|
||||
hidden(this, 'classList', new ClassList(this));
|
||||
hidden(this, 'listeners', []);
|
||||
this.scrollTop = 0;
|
||||
this.scrollLeft = 0;
|
||||
}
|
||||
get tagName() { return this.localName.toUpperCase(); }
|
||||
[Symbol.for('nodejs.util.inspect.custom')]() {
|
||||
const attrs = [...this.attrs].map(([k, v]) => (v === '' ? ` ${k}` : ` ${k}="${v}"`)).join('');
|
||||
return `<${this.localName}${attrs}>`;
|
||||
}
|
||||
getAttribute(n) { return this.attrs.has(n.toLowerCase()) ? this.attrs.get(n.toLowerCase()) : null; }
|
||||
setAttribute(n, v) { this.attrs.set(n.toLowerCase(), String(v)); }
|
||||
removeAttribute(n) { this.attrs.delete(n.toLowerCase()); }
|
||||
hasAttribute(n) { return this.attrs.has(n.toLowerCase()); }
|
||||
get id() { return this.getAttribute('id') || ''; }
|
||||
set id(v) { this.setAttribute('id', v); }
|
||||
get className() { return this.getAttribute('class') || ''; }
|
||||
set className(v) { this.setAttribute('class', v); }
|
||||
get title() { return this.getAttribute('title') || ''; }
|
||||
set title(v) { this.setAttribute('title', v); }
|
||||
get src() { return this.getAttribute('src') || ''; }
|
||||
set src(v) { this.setAttribute('src', v); }
|
||||
get type() { return this.localName === 'input' ? (this.getAttribute('type') || 'text').toLowerCase() : (this.getAttribute('type') || ''); }
|
||||
set type(v) { this.setAttribute('type', v); }
|
||||
get disabled() { return this.hasAttribute('disabled'); }
|
||||
set disabled(v) { if (v) this.setAttribute('disabled', ''); else this.removeAttribute('disabled'); }
|
||||
get dataset() {
|
||||
const el = this;
|
||||
return new Proxy({}, {
|
||||
get(_t, key) { return typeof key === 'string' ? (el.getAttribute('data-' + kebab(key)) ?? undefined) : undefined; },
|
||||
set(_t, key, v) { el.setAttribute('data-' + kebab(String(key)), v); return true; },
|
||||
has(_t, key) { return el.hasAttribute('data-' + kebab(String(key))); },
|
||||
ownKeys() { return [...el.attrs.keys()].filter(k => k.startsWith('data-')).map(k => camel(k.slice(5))); },
|
||||
getOwnPropertyDescriptor(_t, key) {
|
||||
const v = el.getAttribute('data-' + kebab(String(key)));
|
||||
return v === null ? undefined : { value: v, enumerable: true, configurable: true };
|
||||
},
|
||||
});
|
||||
}
|
||||
get children() { return this.childNodes.filter(n => n instanceof Element); }
|
||||
|
||||
get value() {
|
||||
if (this.localName === 'select') {
|
||||
if (this._value !== undefined) return this._value;
|
||||
const opts = this.querySelectorAll('option');
|
||||
const sel = opts.find(o => o.hasAttribute('selected')) || opts[0];
|
||||
return sel ? sel.value : '';
|
||||
}
|
||||
if (this.localName === 'option') return this.getAttribute('value') ?? this.textContent;
|
||||
if (this.localName === 'textarea') return this._value !== undefined ? this._value : this.textContent;
|
||||
return this._value !== undefined ? this._value : (this.getAttribute('value') ?? '');
|
||||
}
|
||||
set value(v) { this._value = String(v); }
|
||||
get checked() { return this._checked !== undefined ? this._checked : this.hasAttribute('checked'); }
|
||||
set checked(v) { this._checked = !!v; }
|
||||
get selectionStart() { return this._selStart ?? this.value.length; }
|
||||
get selectionEnd() { return this._selEnd ?? this.value.length; }
|
||||
setSelectionRange(a, b) { this._selStart = a; this._selEnd = b; }
|
||||
|
||||
get offsetHeight() { return 0; }
|
||||
get offsetWidth() { return 0; }
|
||||
get scrollHeight() { return 0; }
|
||||
get clientHeight() { return 0; }
|
||||
get clientWidth() { return 0; }
|
||||
getBoundingClientRect() { return { left: 0, top: 0, right: 0, bottom: 0, width: 0, height: 0 }; }
|
||||
scrollIntoView(opts) { this.ownerDocument.scrolledIntoView.push({ el: this, opts }); }
|
||||
focus() { this.ownerDocument.activeElement = this; }
|
||||
blur() { if (this.ownerDocument.activeElement === this) this.ownerDocument.activeElement = this.ownerDocument.body; }
|
||||
|
||||
get innerHTML() { return this.childNodes.map(serialize).join(''); }
|
||||
set innerHTML(html) {
|
||||
for (const c of [...this.childNodes]) this._removeChild(c);
|
||||
for (const c of parseHTML(this.ownerDocument, String(html), this.localName)) this.appendChild(c);
|
||||
this._value = undefined;
|
||||
}
|
||||
get outerHTML() { return serialize(this); }
|
||||
get textContent() { return this.childNodes.map(n => n.textContent).join(''); }
|
||||
set textContent(v) {
|
||||
for (const c of [...this.childNodes]) this._removeChild(c);
|
||||
if (v !== '' && v != null) this.appendChild(new Text(this.ownerDocument, String(v)));
|
||||
}
|
||||
|
||||
querySelectorAll(sel) {
|
||||
const list = parseSelector(sel);
|
||||
const out = [];
|
||||
walk(this, el => { if (list.some(s => matchComplex(el, s, this))) out.push(el); });
|
||||
return out;
|
||||
}
|
||||
querySelector(sel) { return this.querySelectorAll(sel)[0] || null; }
|
||||
matches(sel) { return parseSelector(sel).some(s => matchComplex(this, s, null)); }
|
||||
closest(sel) {
|
||||
const list = parseSelector(sel);
|
||||
for (let n = this; n instanceof Element; n = n.parentNode) if (list.some(s => matchComplex(n, s, null))) return n;
|
||||
return null;
|
||||
}
|
||||
|
||||
addEventListener(type, fn, opts) { this.listeners.push({ type, fn, capture: opts === true || !!(opts && opts.capture) }); }
|
||||
removeEventListener(type, fn, opts) {
|
||||
const capture = opts === true || !!(opts && opts.capture);
|
||||
this.listeners = this.listeners.filter(l => !(l.type === type && l.fn === fn && l.capture === capture));
|
||||
}
|
||||
dispatchEvent(ev) { return dispatch(this, ev.type, { bubbles: ev.bubbles, ...pickInit(ev) }); }
|
||||
}
|
||||
|
||||
function pickInit(ev) {
|
||||
const init = {};
|
||||
for (const k of ['key', 'ctrlKey', 'metaKey', 'shiftKey', 'altKey', 'relatedTarget', 'detail']) if (ev[k] !== undefined) init[k] = ev[k];
|
||||
return init;
|
||||
}
|
||||
|
||||
function walk(root, fn) {
|
||||
for (const c of root.childNodes) {
|
||||
if (c instanceof Element) { fn(c); walk(c, fn); }
|
||||
}
|
||||
}
|
||||
|
||||
function serialize(n) {
|
||||
if (n instanceof Text) return escText(n.data);
|
||||
if (!(n instanceof Element)) return '';
|
||||
const attrs = [...n.attrs].map(([k, v]) => (v === '' ? ` ${k}` : ` ${k}="${escAttr(v)}"`)).join('');
|
||||
if (VOID.has(n.localName)) return `<${n.localName}${attrs}>`;
|
||||
return `<${n.localName}${attrs}>${n.childNodes.map(serialize).join('')}</${n.localName}>`;
|
||||
}
|
||||
|
||||
export function parseHTML(doc, html) {
|
||||
const root = new Element(doc, '#fragment');
|
||||
const stack = [root];
|
||||
const top = () => stack[stack.length - 1];
|
||||
let i = 0;
|
||||
while (i < html.length) {
|
||||
if (html.startsWith('<!--', i)) {
|
||||
const end = html.indexOf('-->', i + 4);
|
||||
i = end < 0 ? html.length : end + 3;
|
||||
continue;
|
||||
}
|
||||
if (html[i] === '<' && html[i + 1] === '/') {
|
||||
const end = html.indexOf('>', i);
|
||||
const name = html.slice(i + 2, end < 0 ? html.length : end).trim().toLowerCase();
|
||||
i = end < 0 ? html.length : end + 1;
|
||||
const at = stack.map(n => n.localName).lastIndexOf(name);
|
||||
if (at > 0) stack.length = at;
|
||||
continue;
|
||||
}
|
||||
if (html[i] === '<' && /[a-zA-Z]/.test(html[i + 1] || '')) {
|
||||
let j = i + 1, quote = null;
|
||||
for (; j < html.length; j++) {
|
||||
const ch = html[j];
|
||||
if (quote) { if (ch === quote) quote = null; continue; }
|
||||
if (ch === '"' || ch === "'") { quote = ch; continue; }
|
||||
if (ch === '>') break;
|
||||
}
|
||||
let body = html.slice(i + 1, j);
|
||||
i = j + 1;
|
||||
const selfClose = body.endsWith('/');
|
||||
if (selfClose) body = body.slice(0, -1);
|
||||
const name = body.match(/^[^\s/>]+/)[0].toLowerCase();
|
||||
const el = new Element(doc, name);
|
||||
const attrRe = /([^\s"'>\/=]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/g;
|
||||
const rest = body.slice(name.length);
|
||||
let m;
|
||||
while ((m = attrRe.exec(rest))) {
|
||||
const v = m[2] ?? m[3] ?? m[4] ?? '';
|
||||
el.setAttribute(m[1], decodeEntities(v));
|
||||
}
|
||||
top().appendChild(el);
|
||||
if (VOID.has(name) || (selfClose && SELF_CLOSING_OK.has(name))) continue;
|
||||
if (RAW_TEXT.has(name)) {
|
||||
const close = html.toLowerCase().indexOf(`</${name}`, i);
|
||||
const raw = html.slice(i, close < 0 ? html.length : close);
|
||||
if (raw) el.appendChild(new Text(doc, decodeEntities(raw)));
|
||||
const end = close < 0 ? html.length : html.indexOf('>', close);
|
||||
i = end < 0 ? html.length : end + 1;
|
||||
continue;
|
||||
}
|
||||
stack.push(el);
|
||||
continue;
|
||||
}
|
||||
const next = html.indexOf('<', i + 1);
|
||||
const text = html.slice(i, next < 0 ? html.length : next);
|
||||
i = next < 0 ? html.length : next;
|
||||
if (text) top().appendChild(new Text(doc, decodeEntities(text)));
|
||||
}
|
||||
const nodes = [...root.childNodes];
|
||||
for (const n of nodes) root._removeChild(n);
|
||||
return nodes;
|
||||
}
|
||||
|
||||
const cache = new Map();
|
||||
export function parseSelector(sel) {
|
||||
if (cache.has(sel)) return cache.get(sel);
|
||||
const list = splitTop(sel, ',').map(s => s.trim()).filter(Boolean).map(parseComplex);
|
||||
if (!list.length) throw new Error(`mini-dom: empty selector "${sel}"`);
|
||||
cache.set(sel, list);
|
||||
return list;
|
||||
}
|
||||
|
||||
function splitTop(s, sep) {
|
||||
const out = [];
|
||||
let depth = 0, quote = null, start = 0;
|
||||
for (let i = 0; i < s.length; i++) {
|
||||
const ch = s[i];
|
||||
if (quote) { if (ch === quote) quote = null; continue; }
|
||||
if (ch === '"' || ch === "'") quote = ch;
|
||||
else if (ch === '(' || ch === '[') depth++;
|
||||
else if (ch === ')' || ch === ']') depth--;
|
||||
else if (ch === sep && depth === 0) { out.push(s.slice(start, i)); start = i + 1; }
|
||||
}
|
||||
out.push(s.slice(start));
|
||||
return out;
|
||||
}
|
||||
|
||||
function parseComplex(s) {
|
||||
const parts = [];
|
||||
let i = 0;
|
||||
let combinator = null;
|
||||
while (i < s.length) {
|
||||
if (/\s/.test(s[i]) || s[i] === '>') {
|
||||
let comb = ' ';
|
||||
while (i < s.length && (/\s/.test(s[i]) || s[i] === '>')) { if (s[i] === '>') comb = '>'; i++; }
|
||||
if (parts.length) combinator = comb;
|
||||
continue;
|
||||
}
|
||||
const [compound, next] = parseCompound(s, i);
|
||||
if (parts.length) parts[parts.length - 1].combinator = combinator || ' ';
|
||||
parts.push({ compound, combinator: null });
|
||||
combinator = null;
|
||||
i = next;
|
||||
}
|
||||
return parts;
|
||||
}
|
||||
|
||||
function parseCompound(s, i) {
|
||||
const c = { tag: null, id: null, classes: [], attrs: [], nots: [] };
|
||||
const ident = (k) => { const m = s.slice(k).match(/^[A-Za-z0-9_-]+/); if (!m) throw new Error(`mini-dom: unsupported selector "${s}"`); return m[0]; };
|
||||
if (s[i] === '*') i++;
|
||||
else if (/[A-Za-z]/.test(s[i])) { c.tag = ident(i).toLowerCase(); i += c.tag.length; }
|
||||
while (i < s.length && !/[\s>]/.test(s[i])) {
|
||||
const ch = s[i];
|
||||
if (ch === '#') { const n = ident(i + 1); c.id = n; i += 1 + n.length; }
|
||||
else if (ch === '.') { const n = ident(i + 1); c.classes.push(n); i += 1 + n.length; }
|
||||
else if (ch === '[') {
|
||||
const end = findClose(s, i, '[', ']');
|
||||
const inner = s.slice(i + 1, end).trim();
|
||||
const m = inner.match(/^([A-Za-z0-9_:-]+)\s*(?:=\s*(?:"([^"]*)"|'([^']*)'|([^\s\]]+)))?$/);
|
||||
if (!m) throw new Error(`mini-dom: unsupported attribute selector "[${inner}]"`);
|
||||
c.attrs.push({ name: m[1].toLowerCase(), value: m[2] ?? m[3] ?? m[4] ?? null });
|
||||
i = end + 1;
|
||||
} else if (s.startsWith(':not(', i)) {
|
||||
const end = findClose(s, i + 4, '(', ')');
|
||||
c.nots.push(parseSelector(s.slice(i + 5, end)));
|
||||
i = end + 1;
|
||||
} else {
|
||||
throw new Error(`mini-dom: unsupported selector "${s}" at "${s.slice(i)}"`);
|
||||
}
|
||||
}
|
||||
return [c, i];
|
||||
}
|
||||
|
||||
function findClose(s, open, a, b) {
|
||||
let depth = 0, quote = null;
|
||||
for (let k = open; k < s.length; k++) {
|
||||
const ch = s[k];
|
||||
if (quote) { if (ch === quote) quote = null; continue; }
|
||||
if (ch === '"' || ch === "'") quote = ch;
|
||||
else if (ch === a) depth++;
|
||||
else if (ch === b && --depth === 0) return k;
|
||||
}
|
||||
throw new Error(`mini-dom: unclosed "${a}" in selector "${s}"`);
|
||||
}
|
||||
|
||||
function matchCompound(el, c) {
|
||||
if (!(el instanceof Element)) return false;
|
||||
if (c.tag && el.localName !== c.tag) return false;
|
||||
if (c.id && el.getAttribute('id') !== c.id) return false;
|
||||
for (const k of c.classes) if (!el.classList.contains(k)) return false;
|
||||
for (const a of c.attrs) {
|
||||
if (!el.hasAttribute(a.name)) return false;
|
||||
if (a.value !== null && el.getAttribute(a.name) !== a.value) return false;
|
||||
}
|
||||
for (const list of c.nots) if (list.some(s => matchComplex(el, s, null))) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function matchComplex(el, parts, _scope) {
|
||||
const at = (k, node) => {
|
||||
if (!matchCompound(node, parts[k].compound)) return false;
|
||||
if (k === 0) return true;
|
||||
const comb = parts[k - 1].combinator;
|
||||
if (comb === '>') return node.parentNode instanceof Element && at(k - 1, node.parentNode);
|
||||
for (let p = node.parentNode; p instanceof Element; p = p.parentNode) if (at(k - 1, p)) return true;
|
||||
return false;
|
||||
};
|
||||
return at(parts.length - 1, el);
|
||||
}
|
||||
|
||||
export function dispatch(target, type, init = {}) {
|
||||
const doc = target.ownerDocument || target;
|
||||
let stopped = false;
|
||||
const ev = {
|
||||
type, target, bubbles: init.bubbles !== false, defaultPrevented: false, currentTarget: null,
|
||||
key: init.key, ctrlKey: !!init.ctrlKey, metaKey: !!init.metaKey, shiftKey: !!init.shiftKey, altKey: !!init.altKey,
|
||||
relatedTarget: init.relatedTarget ?? null, detail: init.detail ?? null,
|
||||
stopPropagation() { stopped = true; },
|
||||
stopImmediatePropagation() { stopped = true; },
|
||||
preventDefault() { this.defaultPrevented = true; },
|
||||
};
|
||||
const path = [];
|
||||
for (let n = target; n; n = n.parentNode) path.push(n);
|
||||
if (path[path.length - 1] !== doc) path.push(doc);
|
||||
const run = (node, capture) => {
|
||||
ev.currentTarget = node;
|
||||
if (!capture && typeof node['on' + type] === 'function') node['on' + type](ev);
|
||||
for (const l of [...(node.listeners || [])]) if (l.type === type && l.capture === capture) l.fn(ev);
|
||||
};
|
||||
for (let k = path.length - 1; k > 0 && !stopped; k--) run(path[k], true);
|
||||
if (!stopped) { run(target, true); if (!stopped) run(target, false); }
|
||||
if (ev.bubbles) for (let k = 1; k < path.length && !stopped; k++) run(path[k], false);
|
||||
return !ev.defaultPrevented;
|
||||
}
|
||||
|
||||
class Document extends Node {
|
||||
constructor() {
|
||||
super(null);
|
||||
this.ownerDocument = this;
|
||||
this.listeners = [];
|
||||
this.documentElement = new Element(this, 'html');
|
||||
this.head = new Element(this, 'head');
|
||||
this.body = new Element(this, 'body');
|
||||
this.appendChild(this.documentElement);
|
||||
this.documentElement.appendChild(this.head);
|
||||
this.documentElement.appendChild(this.body);
|
||||
this.activeElement = this.body;
|
||||
this.scrolledIntoView = [];
|
||||
}
|
||||
[Symbol.for('nodejs.util.inspect.custom')]() { return '#document'; }
|
||||
createElement(tag) { return new Element(this, tag); }
|
||||
createTextNode(t) { return new Text(this, t); }
|
||||
querySelectorAll(sel) { return this.documentElement.querySelectorAll(sel); }
|
||||
querySelector(sel) { return this.documentElement.querySelector(sel); }
|
||||
getElementById(id) { return this.querySelector(`#${id}`); }
|
||||
addEventListener(type, fn, opts) { Element.prototype.addEventListener.call(this, type, fn, opts); }
|
||||
removeEventListener(type, fn, opts) { Element.prototype.removeEventListener.call(this, type, fn, opts); }
|
||||
dispatchEvent(ev) { return dispatch(this, ev.type, { bubbles: ev.bubbles, ...pickInit(ev) }); }
|
||||
}
|
||||
|
||||
export function installDom() {
|
||||
const document = new Document();
|
||||
const frames = [];
|
||||
const win = {
|
||||
innerWidth: 1280, innerHeight: 800, listeners: [],
|
||||
addEventListener(type, fn, opts) { Element.prototype.addEventListener.call(this, type, fn, opts); },
|
||||
removeEventListener(type, fn, opts) { Element.prototype.removeEventListener.call(this, type, fn, opts); },
|
||||
};
|
||||
globalThis.document = document;
|
||||
globalThis.window = win;
|
||||
globalThis.requestAnimationFrame = fn => { frames.push(fn); return frames.length; };
|
||||
globalThis.getComputedStyle = el => ({ getPropertyValue: k => el.style.getPropertyValue(k) });
|
||||
const fire = (el, type, init) => dispatch(el, type, init);
|
||||
return {
|
||||
document,
|
||||
window: win,
|
||||
fire,
|
||||
click: el => fire(el, 'click'),
|
||||
type(el, value) { el.value = value; fire(el, 'input'); fire(el, 'change'); },
|
||||
choose(el, value) { el.value = value; fire(el, 'change'); },
|
||||
key: (el, key, mods = {}) => fire(el, 'keydown', { key, ...mods }),
|
||||
flushFrames() { while (frames.length) frames.shift()(); },
|
||||
host() { const el = document.createElement('div'); document.body.appendChild(el); return el; },
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
// Generated, do not edit. sha256:3babd66c28c9f09e458e639e8c0ca81825c815be279b32e296b0e13ec48e11ca
|
||||
import { services } from '../../server/registers.mjs';
|
||||
|
||||
function pubkeyUit(uri) {
|
||||
const m = /^nostr\+?walletconnect:\/\/([^?/]+)/i.exec(String(uri || '').trim());
|
||||
if (!m) throw new Error('Ongeldige NWC-URI (verwacht nostr+walletconnect://…?relay=…&secret=…).');
|
||||
return m[1].toLowerCase();
|
||||
}
|
||||
|
||||
export function createNwcApiFake() {
|
||||
const opslag = new Map();
|
||||
const calls = { setUri: [], purgeUri: [], validateUri: [], call: [] };
|
||||
const fout = { setUri: null, purgeUri: null };
|
||||
|
||||
const exports = {
|
||||
async validateNwcUri(uri) {
|
||||
calls.validateUri.push(uri);
|
||||
return { walletPubkey: pubkeyUit(uri), relays: ['wss://relay.test'] };
|
||||
},
|
||||
async setNwcUri(sourceId, uri) {
|
||||
calls.setUri.push([Number(sourceId), uri]);
|
||||
if (fout.setUri) throw new Error(fout.setUri);
|
||||
const walletPubkey = pubkeyUit(uri);
|
||||
opslag.set(Number(sourceId), String(uri));
|
||||
return { source: Number(sourceId), uri_set: true, walletPubkey };
|
||||
},
|
||||
async deleteNwcUri(sourceId) {
|
||||
calls.purgeUri.push(Number(sourceId));
|
||||
if (fout.purgeUri) throw new Error(fout.purgeUri);
|
||||
opslag.delete(Number(sourceId));
|
||||
return { source: Number(sourceId), deleted: true };
|
||||
},
|
||||
async nwcStatus(sourceId) {
|
||||
const uri = opslag.get(Number(sourceId));
|
||||
return { source: Number(sourceId), uri_set: !!uri, walletPubkey: uri ? pubkeyUit(uri) : '' };
|
||||
},
|
||||
async nwcCall() { throw new Error('nwc-api-fake: een relay-call hoort niet in een datalaag-test.'); },
|
||||
async getInfo() { return exports.nwcCall(); },
|
||||
async getBalance() { return exports.nwcCall(); },
|
||||
async listTransactions() { return exports.nwcCall(); },
|
||||
async testNwc() { return exports.nwcCall(); },
|
||||
};
|
||||
|
||||
const api = {
|
||||
exports,
|
||||
calls,
|
||||
registreer() {
|
||||
services.register({ id: 'nwc', feature: 'lightning', setNwcUri: exports.setNwcUri, deleteNwcUri: exports.deleteNwcUri });
|
||||
return api;
|
||||
},
|
||||
fout,
|
||||
geheim: sourceId => opslag.get(Number(sourceId)) ?? null,
|
||||
get ids() { return [...opslag.keys()]; },
|
||||
leeg() {
|
||||
opslag.clear();
|
||||
calls.setUri.length = 0; calls.purgeUri.length = 0; calls.validateUri.length = 0;
|
||||
fout.setUri = null; fout.purgeUri = null;
|
||||
},
|
||||
};
|
||||
return api;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// Generated, do not edit. sha256:17b1281e54996148b279858b457fd106d4d2e1b529ec53afb0ad51df4cdcbc22
|
||||
const PREFS = new URL('../../client/data/preferences.js', import.meta.url).href;
|
||||
const APP_SETTINGS = new URL('../../client/app-settings.js', import.meta.url).href;
|
||||
|
||||
async function laad() {
|
||||
const { loadAppSettings } = await import(APP_SETTINGS);
|
||||
await loadAppSettings();
|
||||
}
|
||||
|
||||
export async function vasteRekenvaluta(mock, baseCurrency = 'EUR') {
|
||||
mock.module(PREFS, { namedExports: {
|
||||
getSettings: async () => ({ baseCurrency }),
|
||||
getPref: async () => null,
|
||||
setPref: async () => {},
|
||||
} });
|
||||
await laad();
|
||||
}
|
||||
|
||||
export async function rekenvalutaUitDatabase(mock) {
|
||||
const server = await import(new URL('../../server/data/preferences.js', import.meta.url).href);
|
||||
mock.module(PREFS, { namedExports: server });
|
||||
await laad();
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// Generated, do not edit. sha256:d225f580a4545f4be7759feb5bf7808d1ded43a0baf1cbde8115e6901c6a4521
|
||||
export function schemaVorm(db) {
|
||||
const tabellen = db.prepare(
|
||||
`SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' ORDER BY name`
|
||||
).all().map(r => r.name);
|
||||
|
||||
const uit = [];
|
||||
for (const t of tabellen) {
|
||||
for (const k of db.prepare(`PRAGMA table_info(${t})`).all()) {
|
||||
uit.push(`${t}.${k.name}: ${k.type}${k.notnull ? ' NOT NULL' : ''}${k.pk ? ` PK${k.pk}` : ''}`
|
||||
+ `${k.dflt_value == null ? '' : ` DEFAULT ${k.dflt_value}`}`);
|
||||
}
|
||||
for (const fk of db.prepare(`PRAGMA foreign_key_list(${t})`).all()) {
|
||||
uit.push(`${t}.${fk.from} -> ${fk.table}.${fk.to} ON DELETE ${fk.on_delete}`);
|
||||
}
|
||||
for (const ix of db.prepare(`PRAGMA index_list(${t})`).all()) {
|
||||
const kolommen = db.prepare(`PRAGMA index_info(${ix.name})`).all().map(k => k.name).join(',');
|
||||
uit.push(`${ix.unique ? 'UNIQUE ' : ''}INDEX ${ix.name} ON ${t}(${kolommen})`
|
||||
+ `${ix.partial ? ' PARTIAL' : ''}${ix.origin === 'u' ? ' [uit UNIQUE]' : ''}`);
|
||||
}
|
||||
}
|
||||
return uit.sort();
|
||||
}
|
||||
Reference in new issue
Block a user