@hasna/invoices 0.1.0 → 0.1.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/LICENSE +190 -0
- package/README.md +109 -62
- package/dashboard/dist/assets/index-BhR1BMmj.js +139 -0
- package/dashboard/dist/assets/index-khzrvnCp.css +1 -0
- package/dashboard/dist/index.html +13 -0
- package/dashboard/dist/logo.jpg +0 -0
- package/dist/cli/index.js +10111 -9633
- package/dist/cli/tui.d.ts +2 -0
- package/dist/cli/tui.d.ts.map +1 -0
- package/dist/db/agents.d.ts +19 -0
- package/dist/db/agents.d.ts.map +1 -0
- package/dist/db/cloud.d.ts +20 -0
- package/dist/db/cloud.d.ts.map +1 -0
- package/dist/db/core.test.d.ts +2 -0
- package/dist/db/core.test.d.ts.map +1 -0
- package/dist/db/database.d.ts +7 -8
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/invoices.d.ts +77 -90
- package/dist/db/invoices.d.ts.map +1 -1
- package/dist/db/migrate.d.ts +4 -0
- package/dist/db/migrate.d.ts.map +1 -0
- package/dist/db/pg.d.ts +3 -0
- package/dist/db/pg.d.ts.map +1 -0
- package/dist/db/schema.d.ts +6 -0
- package/dist/db/schema.d.ts.map +1 -0
- package/dist/index.d.ts +8 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1061 -510
- package/dist/lib/version.d.ts +2 -0
- package/dist/lib/version.d.ts.map +1 -0
- package/dist/mcp/index.d.ts +1 -1
- package/dist/mcp/index.js +1320 -766
- package/dist/server/index.d.ts +3 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +14919 -0
- package/dist/types/index.d.ts +18 -0
- package/dist/types/index.d.ts.map +1 -0
- package/package.json +21 -14
- package/dist/db/business.d.ts +0 -93
- package/dist/db/business.d.ts.map +0 -1
- package/dist/db/clients.d.ts +0 -45
- package/dist/db/clients.d.ts.map +0 -1
- package/dist/db/migrations.d.ts +0 -7
- package/dist/db/migrations.d.ts.map +0 -1
package/dist/mcp/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#!/usr/bin/env
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
2
|
// @bun
|
|
3
3
|
var __defProp = Object.defineProperty;
|
|
4
4
|
var __returnValue = (v) => v;
|
|
@@ -16,6 +16,9 @@ var __export = (target, all) => {
|
|
|
16
16
|
};
|
|
17
17
|
|
|
18
18
|
// src/mcp/index.ts
|
|
19
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
20
|
+
import { dirname as dirname3, join as join7 } from "path";
|
|
21
|
+
import { fileURLToPath } from "url";
|
|
19
22
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
20
23
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
21
24
|
|
|
@@ -3992,6 +3995,10 @@ var coerce = {
|
|
|
3992
3995
|
date: (arg) => ZodDate.create({ ...arg, coerce: true })
|
|
3993
3996
|
};
|
|
3994
3997
|
var NEVER = INVALID;
|
|
3998
|
+
// src/db/database.ts
|
|
3999
|
+
import { mkdirSync as mkdirSync3 } from "fs";
|
|
4000
|
+
import { dirname as dirname2, join as join5 } from "path";
|
|
4001
|
+
|
|
3995
4002
|
// node_modules/@hasna/cloud/dist/index.js
|
|
3996
4003
|
import { createRequire } from "module";
|
|
3997
4004
|
import { Database } from "bun:sqlite";
|
|
@@ -4009,6 +4016,7 @@ import { join as join2 } from "path";
|
|
|
4009
4016
|
import { readdirSync as readdirSync2, existsSync as existsSync3 } from "fs";
|
|
4010
4017
|
import { join as join3 } from "path";
|
|
4011
4018
|
import { homedir as homedir3 } from "os";
|
|
4019
|
+
import { hostname } from "os";
|
|
4012
4020
|
import { homedir as homedir4 } from "os";
|
|
4013
4021
|
import { join as join4 } from "path";
|
|
4014
4022
|
import { join as join6, dirname } from "path";
|
|
@@ -5352,7 +5360,7 @@ var require_cert_signatures = __commonJS((exports, module) => {
|
|
|
5352
5360
|
module.exports = { signatureAlgorithmHashFromCertificate };
|
|
5353
5361
|
});
|
|
5354
5362
|
var require_sasl = __commonJS((exports, module) => {
|
|
5355
|
-
var
|
|
5363
|
+
var crypto = require_utils2();
|
|
5356
5364
|
var { signatureAlgorithmHashFromCertificate } = require_cert_signatures();
|
|
5357
5365
|
function startSession(mechanisms, stream) {
|
|
5358
5366
|
const candidates = ["SCRAM-SHA-256"];
|
|
@@ -5365,7 +5373,7 @@ var require_sasl = __commonJS((exports, module) => {
|
|
|
5365
5373
|
if (mechanism === "SCRAM-SHA-256-PLUS" && typeof stream.getPeerCertificate !== "function") {
|
|
5366
5374
|
throw new Error("SASL: Mechanism SCRAM-SHA-256-PLUS requires a certificate");
|
|
5367
5375
|
}
|
|
5368
|
-
const clientNonce =
|
|
5376
|
+
const clientNonce = crypto.randomBytes(18).toString("base64");
|
|
5369
5377
|
const gs2Header = mechanism === "SCRAM-SHA-256-PLUS" ? "p=tls-server-end-point" : stream ? "y" : "n";
|
|
5370
5378
|
return {
|
|
5371
5379
|
mechanism,
|
|
@@ -5401,20 +5409,20 @@ var require_sasl = __commonJS((exports, module) => {
|
|
|
5401
5409
|
let hashName = signatureAlgorithmHashFromCertificate(peerCert);
|
|
5402
5410
|
if (hashName === "MD5" || hashName === "SHA-1")
|
|
5403
5411
|
hashName = "SHA-256";
|
|
5404
|
-
const certHash = await
|
|
5412
|
+
const certHash = await crypto.hashByName(hashName, peerCert);
|
|
5405
5413
|
const bindingData = Buffer.concat([Buffer.from("p=tls-server-end-point,,"), Buffer.from(certHash)]);
|
|
5406
5414
|
channelBinding = bindingData.toString("base64");
|
|
5407
5415
|
}
|
|
5408
5416
|
const clientFinalMessageWithoutProof = "c=" + channelBinding + ",r=" + sv.nonce;
|
|
5409
5417
|
const authMessage = clientFirstMessageBare + "," + serverFirstMessage + "," + clientFinalMessageWithoutProof;
|
|
5410
5418
|
const saltBytes = Buffer.from(sv.salt, "base64");
|
|
5411
|
-
const saltedPassword = await
|
|
5412
|
-
const clientKey = await
|
|
5413
|
-
const storedKey = await
|
|
5414
|
-
const clientSignature = await
|
|
5419
|
+
const saltedPassword = await crypto.deriveKey(password, saltBytes, sv.iteration);
|
|
5420
|
+
const clientKey = await crypto.hmacSha256(saltedPassword, "Client Key");
|
|
5421
|
+
const storedKey = await crypto.sha256(clientKey);
|
|
5422
|
+
const clientSignature = await crypto.hmacSha256(storedKey, authMessage);
|
|
5415
5423
|
const clientProof = xorBuffers(Buffer.from(clientKey), Buffer.from(clientSignature)).toString("base64");
|
|
5416
|
-
const serverKey = await
|
|
5417
|
-
const serverSignatureBytes = await
|
|
5424
|
+
const serverKey = await crypto.hmacSha256(saltedPassword, "Server Key");
|
|
5425
|
+
const serverSignatureBytes = await crypto.hmacSha256(serverKey, authMessage);
|
|
5418
5426
|
session.message = "SASLResponse";
|
|
5419
5427
|
session.serverSignature = Buffer.from(serverSignatureBytes).toString("base64");
|
|
5420
5428
|
session.response = clientFinalMessageWithoutProof + ",p=" + clientProof;
|
|
@@ -5577,11 +5585,11 @@ var require_pg_connection_string = __commonJS((exports, module) => {
|
|
|
5577
5585
|
config.client_encoding = result.searchParams.get("encoding");
|
|
5578
5586
|
return config;
|
|
5579
5587
|
}
|
|
5580
|
-
const
|
|
5588
|
+
const hostname2 = dummyHost ? "" : result.hostname;
|
|
5581
5589
|
if (!config.host) {
|
|
5582
|
-
config.host = decodeURIComponent(
|
|
5583
|
-
} else if (
|
|
5584
|
-
result.pathname =
|
|
5590
|
+
config.host = decodeURIComponent(hostname2);
|
|
5591
|
+
} else if (hostname2 && /^%2f/i.test(hostname2)) {
|
|
5592
|
+
result.pathname = hostname2 + result.pathname;
|
|
5585
5593
|
}
|
|
5586
5594
|
if (!config.port) {
|
|
5587
5595
|
config.port = result.port;
|
|
@@ -7432,7 +7440,7 @@ var require_client = __commonJS((exports, module) => {
|
|
|
7432
7440
|
var Query = require_query();
|
|
7433
7441
|
var defaults = require_defaults();
|
|
7434
7442
|
var Connection = require_connection();
|
|
7435
|
-
var
|
|
7443
|
+
var crypto = require_utils2();
|
|
7436
7444
|
var activeQueryDeprecationNotice = nodeUtils.deprecate(() => {}, "Client.activeQuery is deprecated and will be removed in pg@9.0");
|
|
7437
7445
|
var queryQueueDeprecationNotice = nodeUtils.deprecate(() => {}, "Client.queryQueue is deprecated and will be removed in pg@9.0.");
|
|
7438
7446
|
var pgPassDeprecationNotice = nodeUtils.deprecate(() => {}, "pgpass support is deprecated and will be removed in pg@9.0. " + "You can provide an async function as the password property to the Client/Pool constructor that returns a password instead. Within this function you can call the pgpass module in your own code.");
|
|
@@ -7648,7 +7656,7 @@ var require_client = __commonJS((exports, module) => {
|
|
|
7648
7656
|
_handleAuthMD5Password(msg) {
|
|
7649
7657
|
this._getPassword(async () => {
|
|
7650
7658
|
try {
|
|
7651
|
-
const hashedPassword = await
|
|
7659
|
+
const hashedPassword = await crypto.postgresMd5PasswordHash(this.user, this.password, msg.salt);
|
|
7652
7660
|
this.connection.password(hashedPassword);
|
|
7653
7661
|
} catch (e) {
|
|
7654
7662
|
this.emit("error", e);
|
|
@@ -8926,6 +8934,18 @@ function sqliteToPostgres(sql) {
|
|
|
8926
8934
|
}
|
|
8927
8935
|
return out;
|
|
8928
8936
|
}
|
|
8937
|
+
function translateDdl(ddl, dialect) {
|
|
8938
|
+
if (dialect === "sqlite")
|
|
8939
|
+
return ddl;
|
|
8940
|
+
let out = ddl;
|
|
8941
|
+
out = out.replace(/\bINTEGER\s+PRIMARY\s+KEY\s+AUTOINCREMENT\b/gi, "BIGSERIAL PRIMARY KEY");
|
|
8942
|
+
out = out.replace(/\bAUTOINCREMENT\b/gi, "");
|
|
8943
|
+
out = out.replace(/\bREAL\b/gi, "DOUBLE PRECISION");
|
|
8944
|
+
out = out.replace(/\bBLOB\b/gi, "BYTEA");
|
|
8945
|
+
out = sqliteToPostgres(out);
|
|
8946
|
+
return out;
|
|
8947
|
+
}
|
|
8948
|
+
|
|
8929
8949
|
class SqliteAdapter {
|
|
8930
8950
|
db;
|
|
8931
8951
|
constructor(path) {
|
|
@@ -9113,6 +9133,63 @@ class PgAdapter {
|
|
|
9113
9133
|
return this.pool;
|
|
9114
9134
|
}
|
|
9115
9135
|
}
|
|
9136
|
+
|
|
9137
|
+
class PgAdapterAsync {
|
|
9138
|
+
pool;
|
|
9139
|
+
constructor(arg) {
|
|
9140
|
+
if (typeof arg === "string") {
|
|
9141
|
+
const sslConfig = arg.includes("sslmode=require") || arg.includes("ssl=true") ? { rejectUnauthorized: false } : undefined;
|
|
9142
|
+
this.pool = new esm_default.Pool({ connectionString: arg, ssl: sslConfig });
|
|
9143
|
+
} else {
|
|
9144
|
+
this.pool = arg;
|
|
9145
|
+
}
|
|
9146
|
+
}
|
|
9147
|
+
async run(sql, ...params) {
|
|
9148
|
+
const pgSql = translateSql(sql, "pg");
|
|
9149
|
+
const pgParams = translateParams(params);
|
|
9150
|
+
const res = await this.pool.query(pgSql, pgParams);
|
|
9151
|
+
return {
|
|
9152
|
+
changes: res.rowCount ?? 0,
|
|
9153
|
+
lastInsertRowid: res.rows?.[0]?.id ?? 0
|
|
9154
|
+
};
|
|
9155
|
+
}
|
|
9156
|
+
async get(sql, ...params) {
|
|
9157
|
+
const pgSql = translateSql(sql, "pg");
|
|
9158
|
+
const pgParams = translateParams(params);
|
|
9159
|
+
const res = await this.pool.query(pgSql, pgParams);
|
|
9160
|
+
return res.rows[0] ?? null;
|
|
9161
|
+
}
|
|
9162
|
+
async all(sql, ...params) {
|
|
9163
|
+
const pgSql = translateSql(sql, "pg");
|
|
9164
|
+
const pgParams = translateParams(params);
|
|
9165
|
+
const res = await this.pool.query(pgSql, pgParams);
|
|
9166
|
+
return res.rows;
|
|
9167
|
+
}
|
|
9168
|
+
async exec(sql) {
|
|
9169
|
+
const pgSql = translateSql(sql, "pg");
|
|
9170
|
+
await this.pool.query(pgSql);
|
|
9171
|
+
}
|
|
9172
|
+
async close() {
|
|
9173
|
+
await this.pool.end();
|
|
9174
|
+
}
|
|
9175
|
+
async transaction(fn) {
|
|
9176
|
+
const client = await this.pool.connect();
|
|
9177
|
+
try {
|
|
9178
|
+
await client.query("BEGIN");
|
|
9179
|
+
const result = await fn(client);
|
|
9180
|
+
await client.query("COMMIT");
|
|
9181
|
+
return result;
|
|
9182
|
+
} catch (err) {
|
|
9183
|
+
await client.query("ROLLBACK");
|
|
9184
|
+
throw err;
|
|
9185
|
+
} finally {
|
|
9186
|
+
client.release();
|
|
9187
|
+
}
|
|
9188
|
+
}
|
|
9189
|
+
get raw() {
|
|
9190
|
+
return this.pool;
|
|
9191
|
+
}
|
|
9192
|
+
}
|
|
9116
9193
|
var init_adapter = __esm(() => {
|
|
9117
9194
|
init_esm();
|
|
9118
9195
|
});
|
|
@@ -13375,7 +13452,494 @@ var init_discover = __esm(() => {
|
|
|
13375
13452
|
});
|
|
13376
13453
|
init_adapter();
|
|
13377
13454
|
init_config();
|
|
13455
|
+
async function syncPush(local, remote, options) {
|
|
13456
|
+
const orderedTables = await getTableOrder(remote, options.tables);
|
|
13457
|
+
return syncTransfer(local, remote, { ...options, tables: orderedTables }, "push");
|
|
13458
|
+
}
|
|
13459
|
+
async function syncPull(remote, local, options) {
|
|
13460
|
+
const orderedTables = await getTableOrder(remote, options.tables);
|
|
13461
|
+
return syncTransfer(remote, local, { ...options, tables: orderedTables }, "pull");
|
|
13462
|
+
}
|
|
13463
|
+
async function getTableOrder(remote, tables) {
|
|
13464
|
+
if (tables.length <= 1)
|
|
13465
|
+
return tables;
|
|
13466
|
+
try {
|
|
13467
|
+
const fks = await remote.all(`
|
|
13468
|
+
SELECT DISTINCT
|
|
13469
|
+
tc.table_name AS source_table,
|
|
13470
|
+
ccu.table_name AS referenced_table
|
|
13471
|
+
FROM information_schema.table_constraints tc
|
|
13472
|
+
JOIN information_schema.constraint_column_usage ccu
|
|
13473
|
+
ON tc.constraint_name = ccu.constraint_name
|
|
13474
|
+
AND tc.table_schema = ccu.table_schema
|
|
13475
|
+
WHERE tc.constraint_type = 'FOREIGN KEY'
|
|
13476
|
+
AND tc.table_schema = 'public'
|
|
13477
|
+
`);
|
|
13478
|
+
if (fks.length > 0) {
|
|
13479
|
+
return topoSort(tables, fks);
|
|
13480
|
+
}
|
|
13481
|
+
} catch {}
|
|
13482
|
+
return heuristicOrder(tables);
|
|
13483
|
+
}
|
|
13484
|
+
function topoSort(tables, fks) {
|
|
13485
|
+
const tableSet = new Set(tables);
|
|
13486
|
+
const deps = new Map;
|
|
13487
|
+
for (const t of tables) {
|
|
13488
|
+
deps.set(t, new Set);
|
|
13489
|
+
}
|
|
13490
|
+
for (const fk of fks) {
|
|
13491
|
+
if (tableSet.has(fk.source_table) && tableSet.has(fk.referenced_table)) {
|
|
13492
|
+
deps.get(fk.source_table).add(fk.referenced_table);
|
|
13493
|
+
}
|
|
13494
|
+
}
|
|
13495
|
+
const sorted = [];
|
|
13496
|
+
const visited = new Set;
|
|
13497
|
+
const visiting = new Set;
|
|
13498
|
+
function visit(table) {
|
|
13499
|
+
if (visited.has(table))
|
|
13500
|
+
return;
|
|
13501
|
+
if (visiting.has(table)) {
|
|
13502
|
+
sorted.push(table);
|
|
13503
|
+
visited.add(table);
|
|
13504
|
+
return;
|
|
13505
|
+
}
|
|
13506
|
+
visiting.add(table);
|
|
13507
|
+
const tableDeps = deps.get(table) ?? new Set;
|
|
13508
|
+
for (const dep of tableDeps) {
|
|
13509
|
+
visit(dep);
|
|
13510
|
+
}
|
|
13511
|
+
visiting.delete(table);
|
|
13512
|
+
visited.add(table);
|
|
13513
|
+
sorted.push(table);
|
|
13514
|
+
}
|
|
13515
|
+
for (const t of tables) {
|
|
13516
|
+
visit(t);
|
|
13517
|
+
}
|
|
13518
|
+
return sorted;
|
|
13519
|
+
}
|
|
13520
|
+
function heuristicOrder(tables) {
|
|
13521
|
+
const sorted = [...tables].sort((a, b) => {
|
|
13522
|
+
const aIsChild = a.includes("_") && tables.some((t) => a.startsWith(t + "_") || a.endsWith("_" + t));
|
|
13523
|
+
const bIsChild = b.includes("_") && tables.some((t) => b.startsWith(t + "_") || b.endsWith("_" + t));
|
|
13524
|
+
if (aIsChild && !bIsChild)
|
|
13525
|
+
return 1;
|
|
13526
|
+
if (!aIsChild && bIsChild)
|
|
13527
|
+
return -1;
|
|
13528
|
+
return a.localeCompare(b);
|
|
13529
|
+
});
|
|
13530
|
+
return sorted;
|
|
13531
|
+
}
|
|
13532
|
+
function getSqlitePrimaryKeys(adapter, table) {
|
|
13533
|
+
try {
|
|
13534
|
+
const cols = adapter.all(`PRAGMA table_info("${table}")`);
|
|
13535
|
+
const pkCols = cols.filter((c) => c.pk > 0).sort((a, b) => a.pk - b.pk).map((c) => c.name);
|
|
13536
|
+
return pkCols;
|
|
13537
|
+
} catch {
|
|
13538
|
+
return [];
|
|
13539
|
+
}
|
|
13540
|
+
}
|
|
13541
|
+
async function getPgPrimaryKeys(adapter, table) {
|
|
13542
|
+
try {
|
|
13543
|
+
const rows = await adapter.all(`
|
|
13544
|
+
SELECT kcu.column_name, kcu.ordinal_position
|
|
13545
|
+
FROM information_schema.table_constraints tc
|
|
13546
|
+
JOIN information_schema.key_column_usage kcu
|
|
13547
|
+
ON tc.constraint_name = kcu.constraint_name
|
|
13548
|
+
AND tc.table_schema = kcu.table_schema
|
|
13549
|
+
WHERE tc.constraint_type = 'PRIMARY KEY'
|
|
13550
|
+
AND tc.table_schema = 'public'
|
|
13551
|
+
AND tc.table_name = '${table}'
|
|
13552
|
+
ORDER BY kcu.ordinal_position
|
|
13553
|
+
`);
|
|
13554
|
+
return rows.map((r) => r.column_name);
|
|
13555
|
+
} catch {
|
|
13556
|
+
return [];
|
|
13557
|
+
}
|
|
13558
|
+
}
|
|
13559
|
+
async function detectPrimaryKeys(adapter, table) {
|
|
13560
|
+
if (isAsyncAdapter(adapter)) {
|
|
13561
|
+
return getPgPrimaryKeys(adapter, table);
|
|
13562
|
+
}
|
|
13563
|
+
return getSqlitePrimaryKeys(adapter, table);
|
|
13564
|
+
}
|
|
13565
|
+
async function resolvePrimaryKeys(source, target, table, pkOption) {
|
|
13566
|
+
if (pkOption) {
|
|
13567
|
+
return Array.isArray(pkOption) ? pkOption : [pkOption];
|
|
13568
|
+
}
|
|
13569
|
+
let pks = await detectPrimaryKeys(source, table);
|
|
13570
|
+
if (pks.length === 0) {
|
|
13571
|
+
pks = await detectPrimaryKeys(target, table);
|
|
13572
|
+
}
|
|
13573
|
+
return pks;
|
|
13574
|
+
}
|
|
13575
|
+
function pgTypeToSqlite(pgType) {
|
|
13576
|
+
const t = pgType.toLowerCase();
|
|
13577
|
+
if (t.includes("int") || t === "bigint" || t === "smallint" || t === "serial" || t === "bigserial")
|
|
13578
|
+
return "INTEGER";
|
|
13579
|
+
if (t.includes("bool"))
|
|
13580
|
+
return "INTEGER";
|
|
13581
|
+
if (t.includes("float") || t.includes("double") || t === "real" || t === "numeric" || t === "decimal")
|
|
13582
|
+
return "REAL";
|
|
13583
|
+
if (t === "bytea")
|
|
13584
|
+
return "BLOB";
|
|
13585
|
+
return "TEXT";
|
|
13586
|
+
}
|
|
13587
|
+
async function ensureTableInSqliteFromPg(target, source, table) {
|
|
13588
|
+
const existing = target.all(`SELECT name FROM sqlite_master WHERE type='table' AND name=?`, table);
|
|
13589
|
+
if (existing.length > 0)
|
|
13590
|
+
return false;
|
|
13591
|
+
const cols = await source.all(`SELECT column_name, data_type, is_nullable, column_default
|
|
13592
|
+
FROM information_schema.columns
|
|
13593
|
+
WHERE table_schema = 'public' AND table_name = '${table}'
|
|
13594
|
+
ORDER BY ordinal_position`);
|
|
13595
|
+
if (cols.length === 0)
|
|
13596
|
+
return false;
|
|
13597
|
+
const pkCols = await source.all(`SELECT kcu.column_name
|
|
13598
|
+
FROM information_schema.table_constraints tc
|
|
13599
|
+
JOIN information_schema.key_column_usage kcu
|
|
13600
|
+
ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema
|
|
13601
|
+
WHERE tc.constraint_type = 'PRIMARY KEY' AND tc.table_schema = 'public' AND tc.table_name = '${table}'
|
|
13602
|
+
ORDER BY kcu.ordinal_position`);
|
|
13603
|
+
const pkSet = new Set(pkCols.map((c) => c.column_name));
|
|
13604
|
+
const skipTypes = new Set(["tsvector", "tsquery", "user-defined"]);
|
|
13605
|
+
const filteredCols = cols.filter((c) => !skipTypes.has(c.data_type));
|
|
13606
|
+
const colDefs = filteredCols.map((c) => {
|
|
13607
|
+
const sqliteType = pgTypeToSqlite(c.data_type);
|
|
13608
|
+
const notNull = c.is_nullable === "NO" && !pkSet.has(c.column_name) ? " NOT NULL" : "";
|
|
13609
|
+
return `"${c.column_name}" ${sqliteType}${notNull}`;
|
|
13610
|
+
});
|
|
13611
|
+
if (pkSet.size > 0) {
|
|
13612
|
+
const pkList = [...pkSet].map((c) => `"${c}"`).join(", ");
|
|
13613
|
+
colDefs.push(`PRIMARY KEY (${pkList})`);
|
|
13614
|
+
}
|
|
13615
|
+
const sql = `CREATE TABLE IF NOT EXISTS "${table}" (${colDefs.join(", ")})`;
|
|
13616
|
+
target.exec(sql);
|
|
13617
|
+
process.stderr.write(` [sync] ${table}: auto-created in SQLite from PG schema
|
|
13618
|
+
`);
|
|
13619
|
+
return true;
|
|
13620
|
+
}
|
|
13621
|
+
async function ensureTablesExist(source, target, tables) {
|
|
13622
|
+
for (const table of tables) {
|
|
13623
|
+
if (!isAsyncAdapter(target) && isAsyncAdapter(source)) {
|
|
13624
|
+
await ensureTableInSqliteFromPg(target, source, table);
|
|
13625
|
+
}
|
|
13626
|
+
}
|
|
13627
|
+
}
|
|
13628
|
+
async function filterColumnsForTarget(target, table, sourceColumns) {
|
|
13629
|
+
try {
|
|
13630
|
+
if (!isAsyncAdapter(target)) {
|
|
13631
|
+
const colInfo = target.all(`PRAGMA table_info("${table}")`);
|
|
13632
|
+
if (Array.isArray(colInfo) && colInfo.length > 0) {
|
|
13633
|
+
const targetCols = new Set(colInfo.map((c) => c.name));
|
|
13634
|
+
const filtered = sourceColumns.filter((c) => targetCols.has(c));
|
|
13635
|
+
if (filtered.length < sourceColumns.length) {
|
|
13636
|
+
const dropped = sourceColumns.filter((c) => !targetCols.has(c));
|
|
13637
|
+
process.stderr.write(` [sync] ${table}: dropping ${dropped.length} columns not in target: ${dropped.join(", ")}
|
|
13638
|
+
`);
|
|
13639
|
+
}
|
|
13640
|
+
return filtered;
|
|
13641
|
+
}
|
|
13642
|
+
} else {
|
|
13643
|
+
const colInfo = await target.all(`SELECT column_name FROM information_schema.columns WHERE table_schema = 'public' AND table_name = '${table}'`);
|
|
13644
|
+
if (colInfo.length > 0) {
|
|
13645
|
+
const targetCols = new Set(colInfo.map((c) => c.column_name));
|
|
13646
|
+
return sourceColumns.filter((c) => targetCols.has(c));
|
|
13647
|
+
}
|
|
13648
|
+
}
|
|
13649
|
+
} catch {}
|
|
13650
|
+
return sourceColumns;
|
|
13651
|
+
}
|
|
13652
|
+
async function syncTransfer(source, target, options, _direction) {
|
|
13653
|
+
const {
|
|
13654
|
+
tables,
|
|
13655
|
+
onProgress,
|
|
13656
|
+
batchSize = 100,
|
|
13657
|
+
conflictColumn = "updated_at",
|
|
13658
|
+
primaryKey: pkOption
|
|
13659
|
+
} = options;
|
|
13660
|
+
const results = [];
|
|
13661
|
+
const sqliteTarget = !isAsyncAdapter(target) ? target : null;
|
|
13662
|
+
await ensureTablesExist(source, target, tables);
|
|
13663
|
+
if (sqliteTarget) {
|
|
13664
|
+
try {
|
|
13665
|
+
sqliteTarget.exec("PRAGMA foreign_keys = OFF");
|
|
13666
|
+
} catch {}
|
|
13667
|
+
}
|
|
13668
|
+
try {
|
|
13669
|
+
for (let i = 0;i < tables.length; i++) {
|
|
13670
|
+
const table = tables[i];
|
|
13671
|
+
const result = {
|
|
13672
|
+
table,
|
|
13673
|
+
rowsRead: 0,
|
|
13674
|
+
rowsWritten: 0,
|
|
13675
|
+
rowsSkipped: 0,
|
|
13676
|
+
errors: []
|
|
13677
|
+
};
|
|
13678
|
+
try {
|
|
13679
|
+
onProgress?.({
|
|
13680
|
+
table,
|
|
13681
|
+
phase: "reading",
|
|
13682
|
+
rowsRead: 0,
|
|
13683
|
+
rowsWritten: 0,
|
|
13684
|
+
totalTables: tables.length,
|
|
13685
|
+
currentTableIndex: i
|
|
13686
|
+
});
|
|
13687
|
+
const rows = await readAll(source, `SELECT * FROM "${table}"`);
|
|
13688
|
+
result.rowsRead = rows.length;
|
|
13689
|
+
if (rows.length === 0) {
|
|
13690
|
+
onProgress?.({
|
|
13691
|
+
table,
|
|
13692
|
+
phase: "done",
|
|
13693
|
+
rowsRead: 0,
|
|
13694
|
+
rowsWritten: 0,
|
|
13695
|
+
totalTables: tables.length,
|
|
13696
|
+
currentTableIndex: i
|
|
13697
|
+
});
|
|
13698
|
+
results.push(result);
|
|
13699
|
+
continue;
|
|
13700
|
+
}
|
|
13701
|
+
const pkColumns = await resolvePrimaryKeys(source, target, table, pkOption);
|
|
13702
|
+
const sourceColumns = Object.keys(rows[0]);
|
|
13703
|
+
const columns = await filterColumnsForTarget(target, table, sourceColumns);
|
|
13704
|
+
if (pkColumns.length === 0) {
|
|
13705
|
+
result.errors.push(`Table "${table}" has no primary key \u2014 inserting without conflict handling`);
|
|
13706
|
+
onProgress?.({
|
|
13707
|
+
table,
|
|
13708
|
+
phase: "writing",
|
|
13709
|
+
rowsRead: result.rowsRead,
|
|
13710
|
+
rowsWritten: 0,
|
|
13711
|
+
totalTables: tables.length,
|
|
13712
|
+
currentTableIndex: i
|
|
13713
|
+
});
|
|
13714
|
+
for (let offset = 0;offset < rows.length; offset += batchSize) {
|
|
13715
|
+
const batch = rows.slice(offset, offset + batchSize);
|
|
13716
|
+
try {
|
|
13717
|
+
if (isAsyncAdapter(target)) {
|
|
13718
|
+
await batchInsertPg(target, table, columns, batch);
|
|
13719
|
+
} else {
|
|
13720
|
+
batchInsertSqlite(target, table, columns, batch);
|
|
13721
|
+
}
|
|
13722
|
+
result.rowsWritten += batch.length;
|
|
13723
|
+
} catch (err) {
|
|
13724
|
+
result.errors.push(`Batch at offset ${offset}: ${err?.message ?? String(err)}`);
|
|
13725
|
+
}
|
|
13726
|
+
}
|
|
13727
|
+
onProgress?.({
|
|
13728
|
+
table,
|
|
13729
|
+
phase: "done",
|
|
13730
|
+
rowsRead: result.rowsRead,
|
|
13731
|
+
rowsWritten: result.rowsWritten,
|
|
13732
|
+
totalTables: tables.length,
|
|
13733
|
+
currentTableIndex: i
|
|
13734
|
+
});
|
|
13735
|
+
results.push(result);
|
|
13736
|
+
continue;
|
|
13737
|
+
}
|
|
13738
|
+
const missingPks = pkColumns.filter((pk) => !columns.includes(pk));
|
|
13739
|
+
if (missingPks.length > 0) {
|
|
13740
|
+
result.errors.push(`Table "${table}" missing PK columns in data: ${missingPks.join(", ")} \u2014 skipping`);
|
|
13741
|
+
results.push(result);
|
|
13742
|
+
continue;
|
|
13743
|
+
}
|
|
13744
|
+
onProgress?.({
|
|
13745
|
+
table,
|
|
13746
|
+
phase: "writing",
|
|
13747
|
+
rowsRead: result.rowsRead,
|
|
13748
|
+
rowsWritten: 0,
|
|
13749
|
+
totalTables: tables.length,
|
|
13750
|
+
currentTableIndex: i
|
|
13751
|
+
});
|
|
13752
|
+
const updateCols = columns.filter((c) => !pkColumns.includes(c));
|
|
13753
|
+
for (let offset = 0;offset < rows.length; offset += batchSize) {
|
|
13754
|
+
const batch = rows.slice(offset, offset + batchSize);
|
|
13755
|
+
try {
|
|
13756
|
+
if (isAsyncAdapter(target)) {
|
|
13757
|
+
await batchUpsertPg(target, table, columns, updateCols, pkColumns, batch, columns.includes(conflictColumn) ? conflictColumn : undefined);
|
|
13758
|
+
} else {
|
|
13759
|
+
batchUpsertSqlite(target, table, columns, updateCols, pkColumns, batch, columns.includes(conflictColumn) ? conflictColumn : undefined);
|
|
13760
|
+
}
|
|
13761
|
+
result.rowsWritten += batch.length;
|
|
13762
|
+
} catch (err) {
|
|
13763
|
+
result.errors.push(`Batch at offset ${offset}: ${err?.message ?? String(err)}`);
|
|
13764
|
+
}
|
|
13765
|
+
onProgress?.({
|
|
13766
|
+
table,
|
|
13767
|
+
phase: "writing",
|
|
13768
|
+
rowsRead: result.rowsRead,
|
|
13769
|
+
rowsWritten: result.rowsWritten,
|
|
13770
|
+
totalTables: tables.length,
|
|
13771
|
+
currentTableIndex: i
|
|
13772
|
+
});
|
|
13773
|
+
}
|
|
13774
|
+
onProgress?.({
|
|
13775
|
+
table,
|
|
13776
|
+
phase: "done",
|
|
13777
|
+
rowsRead: result.rowsRead,
|
|
13778
|
+
rowsWritten: result.rowsWritten,
|
|
13779
|
+
totalTables: tables.length,
|
|
13780
|
+
currentTableIndex: i
|
|
13781
|
+
});
|
|
13782
|
+
} catch (err) {
|
|
13783
|
+
result.errors.push(`Table "${table}": ${err?.message ?? String(err)}`);
|
|
13784
|
+
}
|
|
13785
|
+
results.push(result);
|
|
13786
|
+
}
|
|
13787
|
+
} finally {
|
|
13788
|
+
if (sqliteTarget) {
|
|
13789
|
+
try {
|
|
13790
|
+
sqliteTarget.exec("PRAGMA foreign_keys = ON");
|
|
13791
|
+
} catch {}
|
|
13792
|
+
try {
|
|
13793
|
+
const violations = sqliteTarget.all("PRAGMA foreign_key_check");
|
|
13794
|
+
if (violations.length > 0) {
|
|
13795
|
+
const tables2 = [...new Set(violations.map((v) => v.table))];
|
|
13796
|
+
const msg = `FK integrity check: ${violations.length} violation(s) in table(s): ${tables2.join(", ")}`;
|
|
13797
|
+
if (results.length > 0) {
|
|
13798
|
+
results[results.length - 1].errors.push(msg);
|
|
13799
|
+
}
|
|
13800
|
+
}
|
|
13801
|
+
} catch {}
|
|
13802
|
+
}
|
|
13803
|
+
}
|
|
13804
|
+
return results;
|
|
13805
|
+
}
|
|
13806
|
+
async function batchUpsertPg(target, table, columns, updateCols, primaryKeys, batch, conflictColumn) {
|
|
13807
|
+
if (batch.length === 0)
|
|
13808
|
+
return;
|
|
13809
|
+
const colList = columns.map((c) => `"${c}"`).join(", ");
|
|
13810
|
+
const valuePlaceholders = batch.map((_, rowIdx) => {
|
|
13811
|
+
const offset = rowIdx * columns.length;
|
|
13812
|
+
return `(${columns.map((_2, colIdx) => `$${offset + colIdx + 1}`).join(", ")})`;
|
|
13813
|
+
}).join(", ");
|
|
13814
|
+
const pkList = primaryKeys.map((c) => `"${c}"`).join(", ");
|
|
13815
|
+
const setClause = updateCols.length > 0 ? updateCols.map((c) => `"${c}" = EXCLUDED."${c}"`).join(", ") : `"${primaryKeys[0]}" = EXCLUDED."${primaryKeys[0]}"`;
|
|
13816
|
+
const whereClause = conflictColumn && updateCols.includes(conflictColumn) ? ` WHERE "${table}"."${conflictColumn}" IS NULL OR EXCLUDED."${conflictColumn}" >= "${table}"."${conflictColumn}"` : "";
|
|
13817
|
+
const sql = `INSERT INTO "${table}" (${colList}) VALUES ${valuePlaceholders}
|
|
13818
|
+
ON CONFLICT (${pkList}) DO UPDATE SET ${setClause}${whereClause}`;
|
|
13819
|
+
const params = batch.flatMap((row) => columns.map((c) => row[c] ?? null));
|
|
13820
|
+
await target.run(sql, ...params);
|
|
13821
|
+
}
|
|
13822
|
+
function batchUpsertSqlite(target, table, columns, updateCols, primaryKeys, batch, conflictColumn) {
|
|
13823
|
+
if (batch.length === 0)
|
|
13824
|
+
return;
|
|
13825
|
+
const colList = columns.map((c) => `"${c}"`).join(", ");
|
|
13826
|
+
const valuePlaceholders = batch.map(() => `(${columns.map(() => "?").join(", ")})`).join(", ");
|
|
13827
|
+
const pkList = primaryKeys.map((c) => `"${c}"`).join(", ");
|
|
13828
|
+
const setClause = updateCols.length > 0 ? updateCols.map((c) => `"${c}" = EXCLUDED."${c}"`).join(", ") : `"${primaryKeys[0]}" = EXCLUDED."${primaryKeys[0]}"`;
|
|
13829
|
+
const whereClause = conflictColumn && updateCols.includes(conflictColumn) ? ` WHERE "${table}"."${conflictColumn}" IS NULL OR EXCLUDED."${conflictColumn}" >= "${table}"."${conflictColumn}"` : "";
|
|
13830
|
+
const sql = `INSERT INTO "${table}" (${colList}) VALUES ${valuePlaceholders}
|
|
13831
|
+
ON CONFLICT (${pkList}) DO UPDATE SET ${setClause}${whereClause}`;
|
|
13832
|
+
const params = batch.flatMap((row) => columns.map((c) => coerceForSqlite(row[c])));
|
|
13833
|
+
target.run(sql, ...params);
|
|
13834
|
+
}
|
|
13835
|
+
async function batchInsertPg(target, table, columns, batch) {
|
|
13836
|
+
if (batch.length === 0)
|
|
13837
|
+
return;
|
|
13838
|
+
const colList = columns.map((c) => `"${c}"`).join(", ");
|
|
13839
|
+
const valuePlaceholders = batch.map((_, rowIdx) => {
|
|
13840
|
+
const offset = rowIdx * columns.length;
|
|
13841
|
+
return `(${columns.map((_2, colIdx) => `$${offset + colIdx + 1}`).join(", ")})`;
|
|
13842
|
+
}).join(", ");
|
|
13843
|
+
const sql = `INSERT INTO "${table}" (${colList}) VALUES ${valuePlaceholders}`;
|
|
13844
|
+
const params = batch.flatMap((row) => columns.map((c) => row[c] ?? null));
|
|
13845
|
+
await target.run(sql, ...params);
|
|
13846
|
+
}
|
|
13847
|
+
function batchInsertSqlite(target, table, columns, batch) {
|
|
13848
|
+
if (batch.length === 0)
|
|
13849
|
+
return;
|
|
13850
|
+
const colList = columns.map((c) => `"${c}"`).join(", ");
|
|
13851
|
+
const valuePlaceholders = batch.map(() => `(${columns.map(() => "?").join(", ")})`).join(", ");
|
|
13852
|
+
const sql = `INSERT INTO "${table}" (${colList}) VALUES ${valuePlaceholders}`;
|
|
13853
|
+
const params = batch.flatMap((row) => columns.map((c) => coerceForSqlite(row[c])));
|
|
13854
|
+
target.run(sql, ...params);
|
|
13855
|
+
}
|
|
13856
|
+
function coerceForSqlite(value) {
|
|
13857
|
+
if (value === null || value === undefined)
|
|
13858
|
+
return null;
|
|
13859
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "bigint" || typeof value === "boolean")
|
|
13860
|
+
return value;
|
|
13861
|
+
if (value instanceof Date)
|
|
13862
|
+
return value.toISOString();
|
|
13863
|
+
if (Buffer.isBuffer(value) || value instanceof Uint8Array)
|
|
13864
|
+
return value;
|
|
13865
|
+
if (typeof value === "object")
|
|
13866
|
+
return JSON.stringify(value);
|
|
13867
|
+
return String(value);
|
|
13868
|
+
}
|
|
13869
|
+
function isAsyncAdapter(adapter) {
|
|
13870
|
+
return adapter.constructor.name === "PgAdapterAsync" || typeof adapter.raw?.connect === "function";
|
|
13871
|
+
}
|
|
13872
|
+
async function readAll(adapter, sql) {
|
|
13873
|
+
const result = adapter.all(sql);
|
|
13874
|
+
return result instanceof Promise ? await result : result;
|
|
13875
|
+
}
|
|
13876
|
+
function listSqliteTables(db) {
|
|
13877
|
+
const rows = db.all(`SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name`);
|
|
13878
|
+
return rows.map((r) => r.name);
|
|
13879
|
+
}
|
|
13378
13880
|
init_config();
|
|
13881
|
+
var FEEDBACK_TABLE_SQL = `
|
|
13882
|
+
CREATE TABLE IF NOT EXISTS feedback (
|
|
13883
|
+
id TEXT PRIMARY KEY,
|
|
13884
|
+
service TEXT NOT NULL,
|
|
13885
|
+
version TEXT DEFAULT '',
|
|
13886
|
+
message TEXT NOT NULL,
|
|
13887
|
+
email TEXT DEFAULT '',
|
|
13888
|
+
machine_id TEXT DEFAULT '',
|
|
13889
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
13890
|
+
)`;
|
|
13891
|
+
function ensureFeedbackTable(db) {
|
|
13892
|
+
db.exec(FEEDBACK_TABLE_SQL);
|
|
13893
|
+
}
|
|
13894
|
+
function saveFeedback(db, feedback) {
|
|
13895
|
+
ensureFeedbackTable(db);
|
|
13896
|
+
const id = feedback.id ?? Math.random().toString(36).slice(2) + Date.now().toString(36);
|
|
13897
|
+
const now = new Date().toISOString();
|
|
13898
|
+
const machineId = feedback.machine_id ?? hostname();
|
|
13899
|
+
db.run(`INSERT INTO feedback (id, service, version, message, email, machine_id, created_at)
|
|
13900
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`, id, feedback.service, feedback.version ?? "", feedback.message, feedback.email ?? "", machineId, feedback.created_at ?? now);
|
|
13901
|
+
return id;
|
|
13902
|
+
}
|
|
13903
|
+
async function sendFeedback(feedback, db) {
|
|
13904
|
+
const config = getCloudConfig();
|
|
13905
|
+
const id = feedback.id ?? Math.random().toString(36).slice(2) + Date.now().toString(36);
|
|
13906
|
+
const machineId = feedback.machine_id ?? hostname();
|
|
13907
|
+
const now = new Date().toISOString();
|
|
13908
|
+
const payload = {
|
|
13909
|
+
id,
|
|
13910
|
+
service: feedback.service,
|
|
13911
|
+
version: feedback.version ?? "",
|
|
13912
|
+
message: feedback.message,
|
|
13913
|
+
email: feedback.email ?? "",
|
|
13914
|
+
machine_id: machineId,
|
|
13915
|
+
created_at: feedback.created_at ?? now
|
|
13916
|
+
};
|
|
13917
|
+
try {
|
|
13918
|
+
const res = await fetch(config.feedback_endpoint, {
|
|
13919
|
+
method: "POST",
|
|
13920
|
+
headers: { "Content-Type": "application/json" },
|
|
13921
|
+
body: JSON.stringify(payload),
|
|
13922
|
+
signal: AbortSignal.timeout(1e4)
|
|
13923
|
+
});
|
|
13924
|
+
if (!res.ok) {
|
|
13925
|
+
throw new Error(`HTTP ${res.status}: ${res.statusText}`);
|
|
13926
|
+
}
|
|
13927
|
+
if (db) {
|
|
13928
|
+
try {
|
|
13929
|
+
saveFeedback(db, { ...feedback, id });
|
|
13930
|
+
} catch {}
|
|
13931
|
+
}
|
|
13932
|
+
return { sent: true, id };
|
|
13933
|
+
} catch (err) {
|
|
13934
|
+
const errorMsg = err?.message ?? String(err);
|
|
13935
|
+
if (db) {
|
|
13936
|
+
try {
|
|
13937
|
+
saveFeedback(db, { ...feedback, id });
|
|
13938
|
+
} catch {}
|
|
13939
|
+
}
|
|
13940
|
+
return { sent: false, id, error: errorMsg };
|
|
13941
|
+
}
|
|
13942
|
+
}
|
|
13379
13943
|
init_dotfile();
|
|
13380
13944
|
|
|
13381
13945
|
class SyncProgressTracker {
|
|
@@ -13508,6 +14072,42 @@ init_config();
|
|
|
13508
14072
|
var CONFIG_DIR2 = join6(homedir5(), ".hasna", "cloud");
|
|
13509
14073
|
init_adapter();
|
|
13510
14074
|
init_config();
|
|
14075
|
+
async function applyPgMigrations(connectionString, migrations, service = "unknown") {
|
|
14076
|
+
const pg2 = new PgAdapterAsync(connectionString);
|
|
14077
|
+
const result = {
|
|
14078
|
+
service,
|
|
14079
|
+
applied: [],
|
|
14080
|
+
alreadyApplied: [],
|
|
14081
|
+
errors: [],
|
|
14082
|
+
totalMigrations: migrations.length
|
|
14083
|
+
};
|
|
14084
|
+
try {
|
|
14085
|
+
await pg2.run(`CREATE TABLE IF NOT EXISTS _pg_migrations (
|
|
14086
|
+
id SERIAL PRIMARY KEY,
|
|
14087
|
+
version INT UNIQUE NOT NULL,
|
|
14088
|
+
applied_at TIMESTAMPTZ DEFAULT NOW()
|
|
14089
|
+
)`);
|
|
14090
|
+
const applied = await pg2.all("SELECT version FROM _pg_migrations ORDER BY version");
|
|
14091
|
+
const appliedSet = new Set(applied.map((r) => r.version));
|
|
14092
|
+
for (let i = 0;i < migrations.length; i++) {
|
|
14093
|
+
if (appliedSet.has(i)) {
|
|
14094
|
+
result.alreadyApplied.push(i);
|
|
14095
|
+
continue;
|
|
14096
|
+
}
|
|
14097
|
+
try {
|
|
14098
|
+
await pg2.exec(migrations[i]);
|
|
14099
|
+
await pg2.run("INSERT INTO _pg_migrations (version) VALUES ($1) ON CONFLICT DO NOTHING", i);
|
|
14100
|
+
result.applied.push(i);
|
|
14101
|
+
} catch (err) {
|
|
14102
|
+
result.errors.push(`Migration ${i}: ${err?.message ?? String(err)}`);
|
|
14103
|
+
break;
|
|
14104
|
+
}
|
|
14105
|
+
}
|
|
14106
|
+
} finally {
|
|
14107
|
+
await pg2.close();
|
|
14108
|
+
}
|
|
14109
|
+
return result;
|
|
14110
|
+
}
|
|
13511
14111
|
init_discover();
|
|
13512
14112
|
init_zod();
|
|
13513
14113
|
init_config();
|
|
@@ -13518,847 +14118,801 @@ init_dotfile();
|
|
|
13518
14118
|
init_adapter();
|
|
13519
14119
|
|
|
13520
14120
|
// src/db/database.ts
|
|
13521
|
-
|
|
13522
|
-
|
|
14121
|
+
function defaultDatabasePath() {
|
|
14122
|
+
const baseDir = join5(process.env.HOME ?? ".", ".hasna", "invoices");
|
|
14123
|
+
mkdirSync3(baseDir, { recursive: true });
|
|
14124
|
+
return join5(baseDir, "invoices.db");
|
|
14125
|
+
}
|
|
14126
|
+
function openInvoiceDatabase(options = {}) {
|
|
14127
|
+
const dbPath = options.dbPath ?? defaultDatabasePath();
|
|
14128
|
+
mkdirSync3(dirname2(dbPath), { recursive: true });
|
|
14129
|
+
const db = new SqliteAdapter(dbPath);
|
|
14130
|
+
db.exec("PRAGMA journal_mode = WAL;");
|
|
14131
|
+
db.exec("PRAGMA foreign_keys = ON;");
|
|
14132
|
+
db.exec("PRAGMA busy_timeout = 5000;");
|
|
14133
|
+
return db;
|
|
14134
|
+
}
|
|
13523
14135
|
|
|
13524
|
-
// src/db/
|
|
14136
|
+
// src/db/schema.ts
|
|
13525
14137
|
var MIGRATIONS = [
|
|
13526
14138
|
{
|
|
13527
|
-
id:
|
|
13528
|
-
name: "initial_schema",
|
|
14139
|
+
id: "0001_init",
|
|
13529
14140
|
sql: `
|
|
13530
|
-
|
|
13531
|
-
|
|
13532
|
-
|
|
13533
|
-
|
|
13534
|
-
phone TEXT,
|
|
13535
|
-
address TEXT,
|
|
13536
|
-
tax_id TEXT,
|
|
13537
|
-
notes TEXT,
|
|
13538
|
-
metadata TEXT NOT NULL DEFAULT '{}',
|
|
13539
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
13540
|
-
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
13541
|
-
);
|
|
13542
|
-
|
|
13543
|
-
CREATE TABLE IF NOT EXISTS invoices (
|
|
13544
|
-
id TEXT PRIMARY KEY,
|
|
13545
|
-
invoice_number TEXT NOT NULL UNIQUE,
|
|
13546
|
-
client_id TEXT REFERENCES clients(id) ON DELETE SET NULL,
|
|
13547
|
-
status TEXT NOT NULL DEFAULT 'draft' CHECK(status IN ('draft', 'sent', 'paid', 'overdue', 'cancelled', 'refunded')),
|
|
13548
|
-
issue_date TEXT NOT NULL DEFAULT (date('now')),
|
|
13549
|
-
due_date TEXT,
|
|
13550
|
-
currency TEXT NOT NULL DEFAULT 'USD',
|
|
13551
|
-
subtotal REAL NOT NULL DEFAULT 0,
|
|
13552
|
-
tax_rate REAL NOT NULL DEFAULT 0,
|
|
13553
|
-
tax_amount REAL NOT NULL DEFAULT 0,
|
|
13554
|
-
discount REAL NOT NULL DEFAULT 0,
|
|
13555
|
-
total REAL NOT NULL DEFAULT 0,
|
|
13556
|
-
notes TEXT,
|
|
13557
|
-
metadata TEXT NOT NULL DEFAULT '{}',
|
|
13558
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
13559
|
-
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
13560
|
-
paid_at TEXT
|
|
13561
|
-
);
|
|
14141
|
+
CREATE TABLE IF NOT EXISTS migrations (
|
|
14142
|
+
id TEXT PRIMARY KEY,
|
|
14143
|
+
applied_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
14144
|
+
);
|
|
13562
14145
|
|
|
13563
|
-
|
|
13564
|
-
|
|
13565
|
-
|
|
13566
|
-
|
|
13567
|
-
|
|
13568
|
-
|
|
13569
|
-
|
|
13570
|
-
|
|
13571
|
-
|
|
13572
|
-
|
|
14146
|
+
CREATE TABLE IF NOT EXISTS parties (
|
|
14147
|
+
id TEXT PRIMARY KEY,
|
|
14148
|
+
kind TEXT NOT NULL CHECK(kind IN ('issuer', 'customer')),
|
|
14149
|
+
legal_name TEXT NOT NULL,
|
|
14150
|
+
email TEXT,
|
|
14151
|
+
tax_id TEXT,
|
|
14152
|
+
country TEXT,
|
|
14153
|
+
address TEXT,
|
|
14154
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
14155
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
14156
|
+
);
|
|
13573
14157
|
|
|
13574
|
-
|
|
13575
|
-
|
|
13576
|
-
|
|
13577
|
-
|
|
13578
|
-
|
|
13579
|
-
|
|
13580
|
-
|
|
13581
|
-
|
|
13582
|
-
|
|
13583
|
-
|
|
14158
|
+
CREATE TABLE IF NOT EXISTS invoices (
|
|
14159
|
+
id TEXT PRIMARY KEY,
|
|
14160
|
+
number TEXT NOT NULL UNIQUE,
|
|
14161
|
+
issuer_id TEXT NOT NULL REFERENCES parties(id),
|
|
14162
|
+
customer_id TEXT NOT NULL REFERENCES parties(id),
|
|
14163
|
+
status TEXT NOT NULL CHECK(status IN ('draft', 'sent', 'partially_paid', 'paid', 'void', 'overdue')),
|
|
14164
|
+
currency TEXT NOT NULL,
|
|
14165
|
+
notes TEXT,
|
|
14166
|
+
issued_at TEXT NOT NULL,
|
|
14167
|
+
due_at TEXT,
|
|
14168
|
+
subtotal_cents INTEGER NOT NULL DEFAULT 0,
|
|
14169
|
+
tax_cents INTEGER NOT NULL DEFAULT 0,
|
|
14170
|
+
total_cents INTEGER NOT NULL DEFAULT 0,
|
|
14171
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
14172
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
14173
|
+
);
|
|
13584
14174
|
|
|
13585
|
-
|
|
13586
|
-
|
|
13587
|
-
|
|
13588
|
-
|
|
13589
|
-
|
|
14175
|
+
CREATE TABLE IF NOT EXISTS invoice_lines (
|
|
14176
|
+
id TEXT PRIMARY KEY,
|
|
14177
|
+
invoice_id TEXT NOT NULL REFERENCES invoices(id) ON DELETE CASCADE,
|
|
14178
|
+
position INTEGER NOT NULL,
|
|
14179
|
+
description TEXT NOT NULL,
|
|
14180
|
+
quantity REAL NOT NULL DEFAULT 1,
|
|
14181
|
+
unit_price_cents INTEGER NOT NULL DEFAULT 0,
|
|
14182
|
+
tax_rate_basis_points INTEGER NOT NULL DEFAULT 0,
|
|
14183
|
+
line_total_cents INTEGER NOT NULL DEFAULT 0,
|
|
14184
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
14185
|
+
);
|
|
13590
14186
|
|
|
13591
|
-
|
|
14187
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_invoice_lines_invoice_position
|
|
14188
|
+
ON invoice_lines(invoice_id, position);
|
|
13592
14189
|
|
|
13593
|
-
|
|
13594
|
-
|
|
13595
|
-
|
|
13596
|
-
|
|
13597
|
-
CREATE INDEX IF NOT EXISTS idx_line_items_invoice ON line_items(invoice_id);
|
|
13598
|
-
CREATE INDEX IF NOT EXISTS idx_payments_invoice ON payments(invoice_id);
|
|
13599
|
-
CREATE INDEX IF NOT EXISTS idx_clients_name ON clients(name);
|
|
13600
|
-
CREATE INDEX IF NOT EXISTS idx_clients_email ON clients(email);
|
|
13601
|
-
`
|
|
13602
|
-
},
|
|
13603
|
-
{
|
|
13604
|
-
id: 2,
|
|
13605
|
-
name: "multi_country_support",
|
|
13606
|
-
sql: `
|
|
13607
|
-
CREATE TABLE IF NOT EXISTS business_profiles (
|
|
13608
|
-
id TEXT PRIMARY KEY,
|
|
13609
|
-
name TEXT NOT NULL,
|
|
13610
|
-
address_line1 TEXT,
|
|
13611
|
-
address_line2 TEXT,
|
|
13612
|
-
city TEXT,
|
|
13613
|
-
state TEXT,
|
|
13614
|
-
postal_code TEXT,
|
|
13615
|
-
country TEXT NOT NULL DEFAULT 'US',
|
|
13616
|
-
tax_id TEXT,
|
|
13617
|
-
vat_number TEXT,
|
|
13618
|
-
registration_number TEXT,
|
|
13619
|
-
email TEXT,
|
|
13620
|
-
phone TEXT,
|
|
13621
|
-
website TEXT,
|
|
13622
|
-
bank_name TEXT,
|
|
13623
|
-
bank_iban TEXT,
|
|
13624
|
-
bank_swift TEXT,
|
|
13625
|
-
bank_account TEXT,
|
|
13626
|
-
logo_url TEXT,
|
|
13627
|
-
is_default INTEGER NOT NULL DEFAULT 0,
|
|
13628
|
-
metadata TEXT NOT NULL DEFAULT '{}',
|
|
13629
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
13630
|
-
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
13631
|
-
);
|
|
14190
|
+
CREATE INDEX IF NOT EXISTS idx_invoices_number ON invoices(number);
|
|
14191
|
+
CREATE INDEX IF NOT EXISTS idx_invoices_status ON invoices(status);
|
|
14192
|
+
CREATE INDEX IF NOT EXISTS idx_invoices_issued_at ON invoices(issued_at);
|
|
14193
|
+
CREATE INDEX IF NOT EXISTS idx_invoices_due_at ON invoices(due_at);
|
|
13632
14194
|
|
|
13633
|
-
|
|
13634
|
-
|
|
13635
|
-
|
|
13636
|
-
|
|
13637
|
-
|
|
13638
|
-
|
|
13639
|
-
|
|
13640
|
-
is_default INTEGER NOT NULL DEFAULT 0,
|
|
13641
|
-
reverse_charge INTEGER NOT NULL DEFAULT 0,
|
|
13642
|
-
description TEXT,
|
|
13643
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
13644
|
-
);
|
|
14195
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS invoices_fts USING fts5(
|
|
14196
|
+
invoice_id UNINDEXED,
|
|
14197
|
+
number,
|
|
14198
|
+
issuer_name,
|
|
14199
|
+
customer_name,
|
|
14200
|
+
notes
|
|
14201
|
+
);
|
|
13645
14202
|
|
|
13646
|
-
|
|
13647
|
-
|
|
13648
|
-
|
|
13649
|
-
|
|
13650
|
-
|
|
13651
|
-
|
|
14203
|
+
CREATE TRIGGER IF NOT EXISTS invoices_ai_fts
|
|
14204
|
+
AFTER INSERT ON invoices
|
|
14205
|
+
BEGIN
|
|
14206
|
+
INSERT INTO invoices_fts(invoice_id, number, issuer_name, customer_name, notes)
|
|
14207
|
+
VALUES (
|
|
14208
|
+
NEW.id,
|
|
14209
|
+
NEW.number,
|
|
14210
|
+
COALESCE((SELECT legal_name FROM parties WHERE id = NEW.issuer_id), ''),
|
|
14211
|
+
COALESCE((SELECT legal_name FROM parties WHERE id = NEW.customer_id), ''),
|
|
14212
|
+
COALESCE(NEW.notes, '')
|
|
14213
|
+
);
|
|
14214
|
+
END;
|
|
13652
14215
|
|
|
13653
|
-
|
|
13654
|
-
|
|
13655
|
-
|
|
13656
|
-
|
|
13657
|
-
|
|
13658
|
-
|
|
13659
|
-
|
|
13660
|
-
|
|
13661
|
-
|
|
14216
|
+
CREATE TRIGGER IF NOT EXISTS invoices_au_fts
|
|
14217
|
+
AFTER UPDATE ON invoices
|
|
14218
|
+
BEGIN
|
|
14219
|
+
DELETE FROM invoices_fts WHERE invoice_id = OLD.id;
|
|
14220
|
+
INSERT INTO invoices_fts(invoice_id, number, issuer_name, customer_name, notes)
|
|
14221
|
+
VALUES (
|
|
14222
|
+
NEW.id,
|
|
14223
|
+
NEW.number,
|
|
14224
|
+
COALESCE((SELECT legal_name FROM parties WHERE id = NEW.issuer_id), ''),
|
|
14225
|
+
COALESCE((SELECT legal_name FROM parties WHERE id = NEW.customer_id), ''),
|
|
14226
|
+
COALESCE(NEW.notes, '')
|
|
14227
|
+
);
|
|
14228
|
+
END;
|
|
13662
14229
|
|
|
13663
|
-
|
|
13664
|
-
|
|
13665
|
-
|
|
14230
|
+
CREATE TRIGGER IF NOT EXISTS invoices_ad_fts
|
|
14231
|
+
AFTER DELETE ON invoices
|
|
14232
|
+
BEGIN
|
|
14233
|
+
DELETE FROM invoices_fts WHERE invoice_id = OLD.id;
|
|
14234
|
+
END;
|
|
14235
|
+
`
|
|
14236
|
+
},
|
|
14237
|
+
{
|
|
14238
|
+
id: "0002_party_fts_sync",
|
|
14239
|
+
sql: `
|
|
14240
|
+
CREATE TRIGGER IF NOT EXISTS parties_au_invoice_fts
|
|
14241
|
+
AFTER UPDATE ON parties
|
|
14242
|
+
BEGIN
|
|
14243
|
+
DELETE FROM invoices_fts
|
|
14244
|
+
WHERE invoice_id IN (
|
|
14245
|
+
SELECT id FROM invoices WHERE issuer_id = NEW.id OR customer_id = NEW.id
|
|
14246
|
+
);
|
|
13666
14247
|
|
|
13667
|
-
|
|
13668
|
-
|
|
13669
|
-
|
|
13670
|
-
|
|
13671
|
-
|
|
13672
|
-
|
|
13673
|
-
|
|
13674
|
-
|
|
13675
|
-
|
|
13676
|
-
|
|
13677
|
-
('tax-uk-vat-5', 'GB', 'VAT', 5, 'vat', 0, 'UK reduced VAT 5%'),
|
|
13678
|
-
('tax-uk-vat-0', 'GB', 'VAT', 0, 'vat', 0, 'UK zero-rated VAT'),
|
|
13679
|
-
('tax-de-vat-19', 'DE', 'MwSt', 19, 'vat', 1, 'Germany standard VAT 19%'),
|
|
13680
|
-
('tax-de-vat-7', 'DE', 'MwSt', 7, 'vat', 0, 'Germany reduced VAT 7%'),
|
|
13681
|
-
('tax-fr-vat-20', 'FR', 'TVA', 20, 'vat', 1, 'France standard VAT 20%'),
|
|
13682
|
-
('tax-fr-vat-10', 'FR', 'TVA', 10, 'vat', 0, 'France reduced VAT 10%'),
|
|
13683
|
-
('tax-fr-vat-55', 'FR', 'TVA', 5.5, 'vat', 0, 'France reduced VAT 5.5%'),
|
|
13684
|
-
('tax-nl-vat-21', 'NL', 'BTW', 21, 'vat', 1, 'Netherlands standard VAT 21%'),
|
|
13685
|
-
('tax-it-vat-22', 'IT', 'IVA', 22, 'vat', 1, 'Italy standard VAT 22%'),
|
|
13686
|
-
('tax-es-vat-21', 'ES', 'IVA', 21, 'vat', 1, 'Spain standard VAT 21%'),
|
|
13687
|
-
('tax-at-vat-20', 'AT', 'USt', 20, 'vat', 1, 'Austria standard VAT 20%'),
|
|
13688
|
-
('tax-be-vat-21', 'BE', 'BTW', 21, 'vat', 1, 'Belgium standard VAT 21%'),
|
|
13689
|
-
('tax-pl-vat-23', 'PL', 'VAT', 23, 'vat', 1, 'Poland standard VAT 23%'),
|
|
13690
|
-
('tax-ie-vat-23', 'IE', 'VAT', 23, 'vat', 1, 'Ireland standard VAT 23%'),
|
|
13691
|
-
('tax-se-vat-25', 'SE', 'Moms', 25, 'vat', 1, 'Sweden standard VAT 25%'),
|
|
13692
|
-
('tax-dk-vat-25', 'DK', 'Moms', 25, 'vat', 1, 'Denmark standard VAT 25%'),
|
|
13693
|
-
('tax-hu-vat-27', 'HU', 'AFA', 27, 'vat', 1, 'Hungary standard VAT 27%'),
|
|
13694
|
-
('tax-bg-vat-20', 'BG', 'DDC', 20, 'vat', 1, 'Bulgaria standard VAT 20%'),
|
|
13695
|
-
('tax-eu-rc', 'EU', 'Reverse Charge', 0, 'vat', 0, 'EU B2B reverse charge mechanism');
|
|
14248
|
+
INSERT INTO invoices_fts(invoice_id, number, issuer_name, customer_name, notes)
|
|
14249
|
+
SELECT
|
|
14250
|
+
i.id,
|
|
14251
|
+
i.number,
|
|
14252
|
+
COALESCE((SELECT legal_name FROM parties p WHERE p.id = i.issuer_id), ''),
|
|
14253
|
+
COALESCE((SELECT legal_name FROM parties p WHERE p.id = i.customer_id), ''),
|
|
14254
|
+
COALESCE(i.notes, '')
|
|
14255
|
+
FROM invoices i
|
|
14256
|
+
WHERE i.issuer_id = NEW.id OR i.customer_id = NEW.id;
|
|
14257
|
+
END;
|
|
13696
14258
|
|
|
13697
|
-
|
|
13698
|
-
|
|
13699
|
-
|
|
13700
|
-
|
|
14259
|
+
INSERT INTO invoices_fts(invoice_id, number, issuer_name, customer_name, notes)
|
|
14260
|
+
SELECT
|
|
14261
|
+
i.id,
|
|
14262
|
+
i.number,
|
|
14263
|
+
COALESCE((SELECT legal_name FROM parties p WHERE p.id = i.issuer_id), ''),
|
|
14264
|
+
COALESCE((SELECT legal_name FROM parties p WHERE p.id = i.customer_id), ''),
|
|
14265
|
+
COALESCE(i.notes, '')
|
|
14266
|
+
FROM invoices i
|
|
14267
|
+
WHERE i.id NOT IN (SELECT invoice_id FROM invoices_fts);
|
|
14268
|
+
`
|
|
14269
|
+
},
|
|
14270
|
+
{
|
|
14271
|
+
id: "0003_agents",
|
|
14272
|
+
sql: `
|
|
14273
|
+
CREATE TABLE IF NOT EXISTS agents (
|
|
14274
|
+
id TEXT PRIMARY KEY,
|
|
14275
|
+
name TEXT NOT NULL UNIQUE,
|
|
14276
|
+
description TEXT,
|
|
14277
|
+
focus TEXT,
|
|
14278
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
14279
|
+
last_seen_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
14280
|
+
);
|
|
14281
|
+
`
|
|
13701
14282
|
}
|
|
13702
14283
|
];
|
|
13703
14284
|
|
|
13704
|
-
// src/db/
|
|
13705
|
-
|
|
13706
|
-
|
|
13707
|
-
|
|
13708
|
-
|
|
13709
|
-
|
|
13710
|
-
|
|
13711
|
-
|
|
13712
|
-
|
|
13713
|
-
|
|
13714
|
-
|
|
13715
|
-
|
|
13716
|
-
|
|
13717
|
-
const
|
|
13718
|
-
const dir = dirname2(dbPath);
|
|
13719
|
-
if (!existsSync4(dir)) {
|
|
13720
|
-
mkdirSync3(dir, { recursive: true });
|
|
13721
|
-
}
|
|
13722
|
-
const adapter = new SqliteAdapter(dbPath);
|
|
13723
|
-
_db = adapter.raw;
|
|
13724
|
-
_db.exec(`
|
|
13725
|
-
CREATE TABLE IF NOT EXISTS _migrations (
|
|
13726
|
-
id INTEGER PRIMARY KEY,
|
|
13727
|
-
name TEXT NOT NULL,
|
|
13728
|
-
applied_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
13729
|
-
)
|
|
13730
|
-
`);
|
|
13731
|
-
const applied = _db.query("SELECT id FROM _migrations ORDER BY id").all();
|
|
13732
|
-
const appliedIds = new Set(applied.map((r) => r.id));
|
|
14285
|
+
// src/db/migrate.ts
|
|
14286
|
+
function ensureMigrationsTable(db) {
|
|
14287
|
+
db.exec(`
|
|
14288
|
+
CREATE TABLE IF NOT EXISTS migrations (
|
|
14289
|
+
id TEXT PRIMARY KEY,
|
|
14290
|
+
applied_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
14291
|
+
);
|
|
14292
|
+
`);
|
|
14293
|
+
}
|
|
14294
|
+
function applyMigrations(db) {
|
|
14295
|
+
ensureMigrationsTable(db);
|
|
14296
|
+
const getMigration = db.query("SELECT id FROM migrations WHERE id = ?1");
|
|
14297
|
+
const markMigration = db.query("INSERT INTO migrations (id) VALUES (?1)");
|
|
14298
|
+
const applied = [];
|
|
13733
14299
|
for (const migration of MIGRATIONS) {
|
|
13734
|
-
|
|
14300
|
+
const row = getMigration.get(migration.id);
|
|
14301
|
+
if (row) {
|
|
13735
14302
|
continue;
|
|
13736
|
-
_db.exec("BEGIN");
|
|
13737
|
-
try {
|
|
13738
|
-
_db.exec(migration.sql);
|
|
13739
|
-
_db.prepare("INSERT INTO _migrations (id, name) VALUES (?, ?)").run(migration.id, migration.name);
|
|
13740
|
-
_db.exec("COMMIT");
|
|
13741
|
-
} catch (error) {
|
|
13742
|
-
_db.exec("ROLLBACK");
|
|
13743
|
-
throw new Error(`Migration ${migration.id} (${migration.name}) failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
13744
14303
|
}
|
|
14304
|
+
db.transaction(() => {
|
|
14305
|
+
db.exec(migration.sql);
|
|
14306
|
+
markMigration.run(migration.id);
|
|
14307
|
+
});
|
|
14308
|
+
applied.push(migration.id);
|
|
14309
|
+
}
|
|
14310
|
+
return applied;
|
|
14311
|
+
}
|
|
14312
|
+
function migrateDatabase(options = {}) {
|
|
14313
|
+
const db = openInvoiceDatabase(options);
|
|
14314
|
+
try {
|
|
14315
|
+
return applyMigrations(db);
|
|
14316
|
+
} finally {
|
|
14317
|
+
db.close();
|
|
13745
14318
|
}
|
|
13746
|
-
return _db;
|
|
13747
14319
|
}
|
|
13748
14320
|
|
|
13749
|
-
// src/db/
|
|
13750
|
-
function
|
|
13751
|
-
return
|
|
13752
|
-
...row,
|
|
13753
|
-
reverse_charge: row.reverse_charge === 1,
|
|
13754
|
-
tax_name: row["tax_name"] || "Tax",
|
|
13755
|
-
language: row["language"] || "en",
|
|
13756
|
-
footer_text: row["footer_text"] ?? null,
|
|
13757
|
-
business_profile_id: row["business_profile_id"] ?? null,
|
|
13758
|
-
metadata: JSON.parse(row.metadata || "{}")
|
|
13759
|
-
};
|
|
13760
|
-
}
|
|
13761
|
-
function nextInvoiceNumber() {
|
|
13762
|
-
const db = getDatabase();
|
|
13763
|
-
const counter = db.prepare("SELECT prefix, next_number FROM invoice_counter WHERE id = 1").get();
|
|
13764
|
-
const number = `${counter.prefix}-${String(counter.next_number).padStart(5, "0")}`;
|
|
13765
|
-
db.prepare("UPDATE invoice_counter SET next_number = next_number + 1 WHERE id = 1").run();
|
|
13766
|
-
return number;
|
|
13767
|
-
}
|
|
13768
|
-
function recalculateInvoice(invoiceId) {
|
|
13769
|
-
const db = getDatabase();
|
|
13770
|
-
const row = db.prepare("SELECT COALESCE(SUM(amount), 0) as subtotal FROM line_items WHERE invoice_id = ?").get(invoiceId);
|
|
13771
|
-
const invoice = db.prepare("SELECT tax_rate, discount FROM invoices WHERE id = ?").get(invoiceId);
|
|
13772
|
-
const subtotal = row.subtotal;
|
|
13773
|
-
const discounted = subtotal - (invoice.discount || 0);
|
|
13774
|
-
const taxAmount = discounted * ((invoice.tax_rate || 0) / 100);
|
|
13775
|
-
const total = discounted + taxAmount;
|
|
13776
|
-
db.prepare("UPDATE invoices SET subtotal = ?, tax_amount = ?, total = ?, updated_at = datetime('now') WHERE id = ?").run(subtotal, taxAmount, total, invoiceId);
|
|
13777
|
-
}
|
|
13778
|
-
function createInvoice(input = {}) {
|
|
13779
|
-
const db = getDatabase();
|
|
13780
|
-
const id = crypto.randomUUID();
|
|
13781
|
-
const invoiceNumber = input.invoice_number || nextInvoiceNumber();
|
|
13782
|
-
db.prepare(`INSERT INTO invoices (id, invoice_number, client_id, business_profile_id, issue_date, due_date, currency, tax_rate, tax_name, discount, reverse_charge, language, notes, footer_text)
|
|
13783
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, invoiceNumber, input.client_id || null, input.business_profile_id || null, input.issue_date ?? new Date().toISOString().split("T")[0], input.due_date ?? null, input.currency || "USD", input.tax_rate || 0, input.tax_name || "Tax", input.discount || 0, input.reverse_charge ? 1 : 0, input.language || "en", input.notes || null, input.footer_text || null);
|
|
13784
|
-
return getInvoice(id);
|
|
13785
|
-
}
|
|
13786
|
-
function getInvoice(id) {
|
|
13787
|
-
const db = getDatabase();
|
|
13788
|
-
const row = db.prepare("SELECT * FROM invoices WHERE id = ? OR invoice_number = ?").get(id, id);
|
|
13789
|
-
return row ? rowToInvoice(row) : null;
|
|
13790
|
-
}
|
|
13791
|
-
function getInvoiceWithItems(id) {
|
|
13792
|
-
const invoice = getInvoice(id);
|
|
13793
|
-
if (!invoice)
|
|
13794
|
-
return null;
|
|
13795
|
-
const db = getDatabase();
|
|
13796
|
-
const line_items = db.prepare("SELECT * FROM line_items WHERE invoice_id = ? ORDER BY sort_order").all(invoice.id);
|
|
13797
|
-
const payments = db.prepare("SELECT * FROM payments WHERE invoice_id = ? ORDER BY paid_at").all(invoice.id);
|
|
13798
|
-
return { ...invoice, line_items, payments };
|
|
13799
|
-
}
|
|
13800
|
-
function listInvoices(options = {}) {
|
|
13801
|
-
const db = getDatabase();
|
|
13802
|
-
const conditions = [];
|
|
13803
|
-
const params = [];
|
|
13804
|
-
if (options.status) {
|
|
13805
|
-
conditions.push("status = ?");
|
|
13806
|
-
params.push(options.status);
|
|
13807
|
-
}
|
|
13808
|
-
if (options.client_id) {
|
|
13809
|
-
conditions.push("client_id = ?");
|
|
13810
|
-
params.push(options.client_id);
|
|
13811
|
-
}
|
|
13812
|
-
if (options.from_date) {
|
|
13813
|
-
conditions.push("issue_date >= ?");
|
|
13814
|
-
params.push(options.from_date);
|
|
13815
|
-
}
|
|
13816
|
-
if (options.to_date) {
|
|
13817
|
-
conditions.push("issue_date <= ?");
|
|
13818
|
-
params.push(options.to_date);
|
|
13819
|
-
}
|
|
13820
|
-
let sql = "SELECT * FROM invoices";
|
|
13821
|
-
if (conditions.length > 0) {
|
|
13822
|
-
sql += " WHERE " + conditions.join(" AND ");
|
|
13823
|
-
}
|
|
13824
|
-
sql += " ORDER BY created_at DESC";
|
|
13825
|
-
if (options.limit) {
|
|
13826
|
-
sql += " LIMIT ?";
|
|
13827
|
-
params.push(options.limit);
|
|
13828
|
-
}
|
|
13829
|
-
const rows = db.prepare(sql).all(...params);
|
|
13830
|
-
return rows.map(rowToInvoice);
|
|
13831
|
-
}
|
|
13832
|
-
function updateInvoiceStatus(id, status) {
|
|
13833
|
-
const db = getDatabase();
|
|
13834
|
-
const paidAt = status === "paid" ? "datetime('now')" : "NULL";
|
|
13835
|
-
db.prepare(`UPDATE invoices SET status = ?, paid_at = ${paidAt}, updated_at = datetime('now') WHERE id = ?`).run(status, id);
|
|
13836
|
-
return getInvoice(id);
|
|
13837
|
-
}
|
|
13838
|
-
function deleteInvoice(id) {
|
|
13839
|
-
const db = getDatabase();
|
|
13840
|
-
return db.prepare("DELETE FROM invoices WHERE id = ?").run(id).changes > 0;
|
|
13841
|
-
}
|
|
13842
|
-
function addLineItem(input) {
|
|
13843
|
-
const db = getDatabase();
|
|
13844
|
-
const id = crypto.randomUUID();
|
|
13845
|
-
const quantity = input.quantity || 1;
|
|
13846
|
-
const amount = quantity * input.unit_price;
|
|
13847
|
-
const maxOrder = db.prepare("SELECT COALESCE(MAX(sort_order), -1) as max_order FROM line_items WHERE invoice_id = ?").get(input.invoice_id);
|
|
13848
|
-
db.prepare(`INSERT INTO line_items (id, invoice_id, description, quantity, unit_price, amount, sort_order)
|
|
13849
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)`).run(id, input.invoice_id, input.description, quantity, input.unit_price, amount, maxOrder.max_order + 1);
|
|
13850
|
-
recalculateInvoice(input.invoice_id);
|
|
13851
|
-
return db.prepare("SELECT * FROM line_items WHERE id = ?").get(id);
|
|
13852
|
-
}
|
|
13853
|
-
function removeLineItem(id) {
|
|
13854
|
-
const db = getDatabase();
|
|
13855
|
-
const item = db.prepare("SELECT invoice_id FROM line_items WHERE id = ?").get(id);
|
|
13856
|
-
if (!item)
|
|
13857
|
-
return false;
|
|
13858
|
-
db.prepare("DELETE FROM line_items WHERE id = ?").run(id);
|
|
13859
|
-
recalculateInvoice(item.invoice_id);
|
|
13860
|
-
return true;
|
|
14321
|
+
// src/db/cloud.ts
|
|
14322
|
+
function defaultConnectionString() {
|
|
14323
|
+
return process.env.INVOICES_DATABASE_URL ?? getConnectionString("invoices");
|
|
13861
14324
|
}
|
|
13862
|
-
function
|
|
13863
|
-
|
|
13864
|
-
|
|
13865
|
-
|
|
13866
|
-
VALUES (?, ?, ?, ?, ?, ?)`).run(id, input.invoice_id, input.amount, input.method || null, input.reference || null, input.notes || null);
|
|
13867
|
-
const invoice = getInvoice(input.invoice_id);
|
|
13868
|
-
if (invoice) {
|
|
13869
|
-
const totalPaid = db.prepare("SELECT COALESCE(SUM(amount), 0) as total FROM payments WHERE invoice_id = ?").get(input.invoice_id);
|
|
13870
|
-
if (totalPaid.total >= invoice.total) {
|
|
13871
|
-
updateInvoiceStatus(input.invoice_id, "paid");
|
|
13872
|
-
}
|
|
13873
|
-
}
|
|
13874
|
-
return db.prepare("SELECT * FROM payments WHERE id = ?").get(id);
|
|
13875
|
-
}
|
|
13876
|
-
function getInvoiceSummary() {
|
|
13877
|
-
const db = getDatabase();
|
|
13878
|
-
const counts = db.prepare(`SELECT
|
|
13879
|
-
COUNT(*) as total_invoices,
|
|
13880
|
-
SUM(CASE WHEN status = 'draft' THEN 1 ELSE 0 END) as draft,
|
|
13881
|
-
SUM(CASE WHEN status = 'sent' THEN 1 ELSE 0 END) as sent,
|
|
13882
|
-
SUM(CASE WHEN status = 'paid' THEN 1 ELSE 0 END) as paid,
|
|
13883
|
-
SUM(CASE WHEN status = 'overdue' THEN 1 ELSE 0 END) as overdue,
|
|
13884
|
-
COALESCE(SUM(CASE WHEN status IN ('sent', 'overdue') THEN total ELSE 0 END), 0) as total_outstanding,
|
|
13885
|
-
COALESCE(SUM(CASE WHEN status = 'paid' THEN total ELSE 0 END), 0) as total_paid
|
|
13886
|
-
FROM invoices`).get();
|
|
13887
|
-
return counts;
|
|
14325
|
+
function parseTablesArg(tables) {
|
|
14326
|
+
if (!tables)
|
|
14327
|
+
return [];
|
|
14328
|
+
return tables.split(",").map((table) => table.trim()).filter(Boolean);
|
|
13888
14329
|
}
|
|
13889
|
-
|
|
13890
|
-
|
|
13891
|
-
|
|
13892
|
-
|
|
13893
|
-
|
|
13894
|
-
|
|
13895
|
-
|
|
13896
|
-
|
|
13897
|
-
|
|
13898
|
-
|
|
13899
|
-
|
|
13900
|
-
|
|
13901
|
-
function getClient(id) {
|
|
13902
|
-
const db = getDatabase();
|
|
13903
|
-
const row = db.prepare("SELECT * FROM clients WHERE id = ?").get(id);
|
|
13904
|
-
return row ? rowToClient(row) : null;
|
|
13905
|
-
}
|
|
13906
|
-
function listClients(search) {
|
|
13907
|
-
const db = getDatabase();
|
|
13908
|
-
if (search) {
|
|
13909
|
-
const q = `%${search}%`;
|
|
13910
|
-
const rows = db.prepare("SELECT * FROM clients WHERE name LIKE ? OR email LIKE ? ORDER BY name").all(q, q);
|
|
13911
|
-
return rows.map(rowToClient);
|
|
13912
|
-
}
|
|
13913
|
-
return db.prepare("SELECT * FROM clients ORDER BY name").all().map(rowToClient);
|
|
13914
|
-
}
|
|
13915
|
-
function updateClient(id, input) {
|
|
13916
|
-
const db = getDatabase();
|
|
13917
|
-
const existing = getClient(id);
|
|
13918
|
-
if (!existing)
|
|
13919
|
-
return null;
|
|
13920
|
-
const sets = [];
|
|
13921
|
-
const params = [];
|
|
13922
|
-
for (const [key, value] of Object.entries(input)) {
|
|
13923
|
-
if (value !== undefined) {
|
|
13924
|
-
sets.push(`${key} = ?`);
|
|
13925
|
-
params.push(value);
|
|
13926
|
-
}
|
|
14330
|
+
function resolveTables(local, tablesCsv) {
|
|
14331
|
+
const configured = parseTablesArg(tablesCsv);
|
|
14332
|
+
return configured.length > 0 ? configured : listSqliteTables(local);
|
|
14333
|
+
}
|
|
14334
|
+
async function cloudPush(local, tablesCsv, connectionString = defaultConnectionString()) {
|
|
14335
|
+
const remote = new PgAdapterAsync(connectionString);
|
|
14336
|
+
try {
|
|
14337
|
+
const tables = resolveTables(local, tablesCsv);
|
|
14338
|
+
const result = await syncPush(local, remote, { tables });
|
|
14339
|
+
return { direction: "push", tables, result };
|
|
14340
|
+
} finally {
|
|
14341
|
+
await remote.close();
|
|
13927
14342
|
}
|
|
13928
|
-
if (sets.length === 0)
|
|
13929
|
-
return existing;
|
|
13930
|
-
sets.push("updated_at = datetime('now')");
|
|
13931
|
-
params.push(id);
|
|
13932
|
-
db.prepare(`UPDATE clients SET ${sets.join(", ")} WHERE id = ?`).run(...params);
|
|
13933
|
-
return getClient(id);
|
|
13934
14343
|
}
|
|
13935
|
-
function
|
|
13936
|
-
const
|
|
13937
|
-
|
|
14344
|
+
async function cloudPull(local, tablesCsv, connectionString = defaultConnectionString()) {
|
|
14345
|
+
const remote = new PgAdapterAsync(connectionString);
|
|
14346
|
+
try {
|
|
14347
|
+
const tables = resolveTables(local, tablesCsv);
|
|
14348
|
+
const result = await syncPull(remote, local, { tables });
|
|
14349
|
+
return { direction: "pull", tables, result };
|
|
14350
|
+
} finally {
|
|
14351
|
+
await remote.close();
|
|
14352
|
+
}
|
|
14353
|
+
}
|
|
14354
|
+
async function submitFeedback(local, feedback) {
|
|
14355
|
+
const payload = {
|
|
14356
|
+
service: "invoices",
|
|
14357
|
+
version: feedback.version,
|
|
14358
|
+
message: feedback.message,
|
|
14359
|
+
email: feedback.email,
|
|
14360
|
+
machine_id: feedback.machine_id
|
|
14361
|
+
};
|
|
14362
|
+
const delivery = await sendFeedback(payload, local);
|
|
14363
|
+
if (!delivery.sent) {
|
|
14364
|
+
const id = saveFeedback(local, payload);
|
|
14365
|
+
return { ...delivery, id, stored_locally: true };
|
|
14366
|
+
}
|
|
14367
|
+
return { ...delivery, stored_locally: false };
|
|
13938
14368
|
}
|
|
13939
14369
|
|
|
13940
|
-
// src/db/
|
|
13941
|
-
function
|
|
14370
|
+
// src/db/pg.ts
|
|
14371
|
+
function defaultPgConnectionString() {
|
|
14372
|
+
return process.env.INVOICES_DATABASE_URL ?? getConnectionString("invoices");
|
|
14373
|
+
}
|
|
14374
|
+
async function migratePgDatabase(connectionString = defaultPgConnectionString()) {
|
|
14375
|
+
const pgMigrations = MIGRATIONS.map((migration) => translateDdl(migration.sql, "pg"));
|
|
14376
|
+
return applyPgMigrations(connectionString, pgMigrations, "invoices");
|
|
14377
|
+
}
|
|
14378
|
+
|
|
14379
|
+
// src/db/invoices.ts
|
|
14380
|
+
import { randomUUID } from "crypto";
|
|
14381
|
+
function toLineTotalCents(line) {
|
|
14382
|
+
const subtotal = Math.round(line.quantity * line.unitPriceCents);
|
|
14383
|
+
const taxRate = line.taxRateBasisPoints ?? 0;
|
|
14384
|
+
const tax = Math.round(subtotal * taxRate / 1e4);
|
|
14385
|
+
return { lineTotalCents: subtotal, lineTaxCents: tax };
|
|
14386
|
+
}
|
|
14387
|
+
function mapPartyRow(row) {
|
|
13942
14388
|
return {
|
|
13943
|
-
|
|
13944
|
-
|
|
13945
|
-
|
|
13946
|
-
|
|
13947
|
-
|
|
13948
|
-
|
|
13949
|
-
|
|
13950
|
-
|
|
13951
|
-
|
|
13952
|
-
|
|
13953
|
-
|
|
13954
|
-
|
|
13955
|
-
|
|
13956
|
-
|
|
13957
|
-
|
|
13958
|
-
|
|
13959
|
-
|
|
13960
|
-
|
|
13961
|
-
|
|
13962
|
-
|
|
13963
|
-
|
|
13964
|
-
|
|
13965
|
-
|
|
13966
|
-
|
|
13967
|
-
|
|
13968
|
-
|
|
13969
|
-
|
|
13970
|
-
|
|
13971
|
-
}
|
|
13972
|
-
function
|
|
13973
|
-
|
|
13974
|
-
|
|
13975
|
-
|
|
13976
|
-
|
|
13977
|
-
|
|
13978
|
-
|
|
13979
|
-
|
|
13980
|
-
|
|
13981
|
-
|
|
13982
|
-
|
|
13983
|
-
|
|
13984
|
-
|
|
13985
|
-
|
|
13986
|
-
|
|
13987
|
-
|
|
13988
|
-
|
|
14389
|
+
id: row.id,
|
|
14390
|
+
kind: row.kind,
|
|
14391
|
+
legalName: row.legal_name,
|
|
14392
|
+
email: row.email ?? undefined,
|
|
14393
|
+
taxId: row.tax_id ?? undefined,
|
|
14394
|
+
country: row.country ?? undefined,
|
|
14395
|
+
address: row.address ?? undefined,
|
|
14396
|
+
createdAt: row.created_at,
|
|
14397
|
+
updatedAt: row.updated_at
|
|
14398
|
+
};
|
|
14399
|
+
}
|
|
14400
|
+
function mapInvoiceRow(row) {
|
|
14401
|
+
return {
|
|
14402
|
+
id: row.id,
|
|
14403
|
+
number: row.number,
|
|
14404
|
+
issuerId: row.issuer_id,
|
|
14405
|
+
customerId: row.customer_id,
|
|
14406
|
+
status: row.status,
|
|
14407
|
+
currency: row.currency,
|
|
14408
|
+
notes: row.notes ?? undefined,
|
|
14409
|
+
issuedAt: row.issued_at,
|
|
14410
|
+
dueAt: row.due_at ?? undefined,
|
|
14411
|
+
subtotalCents: row.subtotal_cents,
|
|
14412
|
+
taxCents: row.tax_cents,
|
|
14413
|
+
totalCents: row.total_cents,
|
|
14414
|
+
createdAt: row.created_at,
|
|
14415
|
+
updatedAt: row.updated_at
|
|
14416
|
+
};
|
|
14417
|
+
}
|
|
14418
|
+
function mapInvoiceLineRow(row) {
|
|
14419
|
+
return {
|
|
14420
|
+
id: row.id,
|
|
14421
|
+
invoiceId: row.invoice_id,
|
|
14422
|
+
position: row.position,
|
|
14423
|
+
description: row.description,
|
|
14424
|
+
quantity: row.quantity,
|
|
14425
|
+
unitPriceCents: row.unit_price_cents,
|
|
14426
|
+
taxRateBasisPoints: row.tax_rate_basis_points,
|
|
14427
|
+
lineTotalCents: row.line_total_cents,
|
|
14428
|
+
createdAt: row.created_at
|
|
14429
|
+
};
|
|
14430
|
+
}
|
|
14431
|
+
function createParty(db, input) {
|
|
14432
|
+
const id = input.id ?? randomUUID();
|
|
14433
|
+
db.query(`
|
|
14434
|
+
INSERT INTO parties (id, kind, legal_name, email, tax_id, country, address)
|
|
14435
|
+
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)
|
|
14436
|
+
`).run(id, input.kind, input.legalName, input.email ?? null, input.taxId ?? null, input.country ?? null, input.address ?? null);
|
|
14437
|
+
return getPartyById(db, id);
|
|
14438
|
+
}
|
|
14439
|
+
function getPartyById(db, id) {
|
|
14440
|
+
const row = db.query("SELECT * FROM parties WHERE id = ?1").get(id);
|
|
14441
|
+
return row ? mapPartyRow(row) : null;
|
|
14442
|
+
}
|
|
14443
|
+
function listParties(db, kind) {
|
|
14444
|
+
if (kind) {
|
|
14445
|
+
return db.query("SELECT * FROM parties WHERE kind = ?1 ORDER BY legal_name ASC").all(kind).map(mapPartyRow);
|
|
14446
|
+
}
|
|
14447
|
+
return db.query("SELECT * FROM parties ORDER BY legal_name ASC").all().map(mapPartyRow);
|
|
14448
|
+
}
|
|
14449
|
+
function createInvoice(db, input) {
|
|
14450
|
+
const invoiceId = input.id ?? randomUUID();
|
|
14451
|
+
const preparedLines = input.lines.map((line, index) => {
|
|
14452
|
+
const { lineTotalCents, lineTaxCents } = toLineTotalCents(line);
|
|
14453
|
+
return {
|
|
14454
|
+
id: line.id ?? randomUUID(),
|
|
14455
|
+
position: index,
|
|
14456
|
+
description: line.description,
|
|
14457
|
+
quantity: line.quantity,
|
|
14458
|
+
unitPriceCents: line.unitPriceCents,
|
|
14459
|
+
taxRateBasisPoints: line.taxRateBasisPoints ?? 0,
|
|
14460
|
+
lineTotalCents,
|
|
14461
|
+
lineTaxCents
|
|
14462
|
+
};
|
|
14463
|
+
});
|
|
14464
|
+
const subtotalCents = preparedLines.reduce((sum, line) => sum + line.lineTotalCents, 0);
|
|
14465
|
+
const taxCents = preparedLines.reduce((sum, line) => sum + line.lineTaxCents, 0);
|
|
14466
|
+
const totalCents = subtotalCents + taxCents;
|
|
14467
|
+
db.transaction(() => {
|
|
14468
|
+
db.query(`
|
|
14469
|
+
INSERT INTO invoices (
|
|
14470
|
+
id, number, issuer_id, customer_id, status, currency, notes, issued_at, due_at, subtotal_cents, tax_cents, total_cents
|
|
14471
|
+
)
|
|
14472
|
+
VALUES (?1, ?2, ?3, ?4, 'draft', ?5, ?6, ?7, ?8, ?9, ?10, ?11)
|
|
14473
|
+
`).run(invoiceId, input.number, input.issuerId, input.customerId, input.currency, input.notes ?? null, input.issuedAt, input.dueAt ?? null, subtotalCents, taxCents, totalCents);
|
|
14474
|
+
const insertLine = db.query(`
|
|
14475
|
+
INSERT INTO invoice_lines (
|
|
14476
|
+
id, invoice_id, position, description, quantity, unit_price_cents, tax_rate_basis_points, line_total_cents
|
|
14477
|
+
)
|
|
14478
|
+
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)
|
|
14479
|
+
`);
|
|
14480
|
+
for (const line of preparedLines) {
|
|
14481
|
+
insertLine.run(line.id, invoiceId, line.position, line.description, line.quantity, line.unitPriceCents, line.taxRateBasisPoints, line.lineTotalCents);
|
|
13989
14482
|
}
|
|
14483
|
+
});
|
|
14484
|
+
return getInvoiceById(db, invoiceId);
|
|
14485
|
+
}
|
|
14486
|
+
function getInvoiceById(db, id) {
|
|
14487
|
+
const invoiceRow = db.query("SELECT * FROM invoices WHERE id = ?1").get(id);
|
|
14488
|
+
if (!invoiceRow) {
|
|
14489
|
+
return null;
|
|
14490
|
+
}
|
|
14491
|
+
const lines = db.query("SELECT * FROM invoice_lines WHERE invoice_id = ?1 ORDER BY position ASC").all(id).map(mapInvoiceLineRow);
|
|
14492
|
+
return {
|
|
14493
|
+
...mapInvoiceRow(invoiceRow),
|
|
14494
|
+
lines
|
|
14495
|
+
};
|
|
14496
|
+
}
|
|
14497
|
+
function listInvoices(db, options = {}) {
|
|
14498
|
+
const limit = options.limit ?? 50;
|
|
14499
|
+
const offset = options.offset ?? 0;
|
|
14500
|
+
if (options.status) {
|
|
14501
|
+
return db.query("SELECT * FROM invoices WHERE status = ?1 ORDER BY issued_at DESC, created_at DESC LIMIT ?2 OFFSET ?3").all(options.status, limit, offset).map(mapInvoiceRow);
|
|
13990
14502
|
}
|
|
13991
|
-
|
|
13992
|
-
return getBusinessProfile(id);
|
|
13993
|
-
sets.push("updated_at = datetime('now')");
|
|
13994
|
-
params.push(id);
|
|
13995
|
-
db.prepare(`UPDATE business_profiles SET ${sets.join(", ")} WHERE id = ?`).run(...params);
|
|
13996
|
-
return getBusinessProfile(id);
|
|
14503
|
+
return db.query("SELECT * FROM invoices ORDER BY issued_at DESC, created_at DESC LIMIT ?1 OFFSET ?2").all(limit, offset).map(mapInvoiceRow);
|
|
13997
14504
|
}
|
|
13998
|
-
function
|
|
13999
|
-
|
|
14505
|
+
function updateInvoiceStatus(db, id, status) {
|
|
14506
|
+
db.query("UPDATE invoices SET status = ?2, updated_at = datetime('now') WHERE id = ?1").run(id, status);
|
|
14507
|
+
const row = db.query("SELECT * FROM invoices WHERE id = ?1").get(id);
|
|
14508
|
+
return row ? mapInvoiceRow(row) : null;
|
|
14000
14509
|
}
|
|
14001
|
-
function
|
|
14002
|
-
|
|
14510
|
+
function deleteInvoice(db, id) {
|
|
14511
|
+
const result = db.query("DELETE FROM invoices WHERE id = ?1").run(id);
|
|
14512
|
+
return result.changes > 0;
|
|
14003
14513
|
}
|
|
14004
|
-
function
|
|
14005
|
-
const
|
|
14006
|
-
|
|
14514
|
+
function searchInvoices(db, query, options = {}) {
|
|
14515
|
+
const limit = options.limit ?? 20;
|
|
14516
|
+
const offset = options.offset ?? 0;
|
|
14517
|
+
const escapedPhrase = `"${query.replace(/"/g, '""')}"`;
|
|
14518
|
+
if (options.status) {
|
|
14519
|
+
return db.query(`
|
|
14520
|
+
SELECT i.*
|
|
14521
|
+
FROM invoices_fts f
|
|
14522
|
+
JOIN invoices i ON i.id = f.invoice_id
|
|
14523
|
+
WHERE invoices_fts MATCH ?1
|
|
14524
|
+
AND i.status = ?2
|
|
14525
|
+
ORDER BY bm25(invoices_fts)
|
|
14526
|
+
LIMIT ?3
|
|
14527
|
+
OFFSET ?4
|
|
14528
|
+
`).all(escapedPhrase, options.status, limit, offset).map(mapInvoiceRow);
|
|
14529
|
+
}
|
|
14530
|
+
return db.query(`
|
|
14531
|
+
SELECT i.*
|
|
14532
|
+
FROM invoices_fts f
|
|
14533
|
+
JOIN invoices i ON i.id = f.invoice_id
|
|
14534
|
+
WHERE invoices_fts MATCH ?1
|
|
14535
|
+
ORDER BY bm25(invoices_fts)
|
|
14536
|
+
LIMIT ?2
|
|
14537
|
+
OFFSET ?3
|
|
14538
|
+
`).all(escapedPhrase, limit, offset).map(mapInvoiceRow);
|
|
14539
|
+
}
|
|
14540
|
+
|
|
14541
|
+
// src/db/agents.ts
|
|
14542
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
14543
|
+
function mapAgent(row) {
|
|
14544
|
+
return {
|
|
14545
|
+
id: row.id,
|
|
14546
|
+
name: row.name,
|
|
14547
|
+
description: row.description ?? undefined,
|
|
14548
|
+
focus: row.focus ?? undefined,
|
|
14549
|
+
createdAt: row.created_at,
|
|
14550
|
+
lastSeenAt: row.last_seen_at
|
|
14551
|
+
};
|
|
14007
14552
|
}
|
|
14008
|
-
function
|
|
14009
|
-
const db =
|
|
14010
|
-
|
|
14011
|
-
|
|
14553
|
+
function registerAgent(db, input) {
|
|
14554
|
+
const existing = db.query("SELECT * FROM agents WHERE name = ?1").get(input.name);
|
|
14555
|
+
if (existing) {
|
|
14556
|
+
db.query("UPDATE agents SET description = COALESCE(?2, description), last_seen_at = datetime('now') WHERE name = ?1").run(input.name, input.description ?? null);
|
|
14557
|
+
return getAgentByName(db, input.name);
|
|
14558
|
+
}
|
|
14559
|
+
const id = randomUUID2();
|
|
14560
|
+
db.query("INSERT INTO agents (id, name, description) VALUES (?1, ?2, ?3)").run(id, input.name, input.description ?? null);
|
|
14561
|
+
return getAgentById(db, id);
|
|
14012
14562
|
}
|
|
14013
|
-
function
|
|
14014
|
-
const db =
|
|
14015
|
-
|
|
14016
|
-
return row ? rowToTaxRule(row) : null;
|
|
14563
|
+
function getAgentById(db, id) {
|
|
14564
|
+
const row = db.query("SELECT * FROM agents WHERE id = ?1").get(id);
|
|
14565
|
+
return row ? mapAgent(row) : null;
|
|
14017
14566
|
}
|
|
14018
|
-
function
|
|
14019
|
-
const db =
|
|
14020
|
-
return
|
|
14567
|
+
function getAgentByName(db, name) {
|
|
14568
|
+
const row = db.query("SELECT * FROM agents WHERE name = ?1").get(name);
|
|
14569
|
+
return row ? mapAgent(row) : null;
|
|
14021
14570
|
}
|
|
14022
|
-
function
|
|
14023
|
-
|
|
14024
|
-
|
|
14025
|
-
db.prepare(`INSERT INTO tax_rules (id, country, region, tax_name, rate, type, is_default, reverse_charge, description)
|
|
14026
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, input.country, input.region || null, input.tax_name, input.rate, input.type || "vat", input.is_default ? 1 : 0, input.reverse_charge ? 1 : 0, input.description || null);
|
|
14027
|
-
return getTaxRule(id);
|
|
14571
|
+
function heartbeatAgent(db, agentId) {
|
|
14572
|
+
db.query("UPDATE agents SET last_seen_at = datetime('now') WHERE id = ?1").run(agentId);
|
|
14573
|
+
return getAgentById(db, agentId);
|
|
14028
14574
|
}
|
|
14029
|
-
function
|
|
14030
|
-
|
|
14575
|
+
function setAgentFocus(db, agentId, focus) {
|
|
14576
|
+
db.query("UPDATE agents SET focus = ?2, last_seen_at = datetime('now') WHERE id = ?1").run(agentId, focus ?? null);
|
|
14577
|
+
return getAgentById(db, agentId);
|
|
14031
14578
|
}
|
|
14032
|
-
function
|
|
14033
|
-
|
|
14034
|
-
const issuerInEU = EU_COUNTRIES.includes(issuerCountry);
|
|
14035
|
-
const clientInEU = EU_COUNTRIES.includes(clientCountry);
|
|
14036
|
-
if (issuerInEU && clientInEU && issuerCountry !== clientCountry && clientVatNumber) {
|
|
14037
|
-
return { tax_rate: 0, tax_name: "Reverse Charge", reverse_charge: true };
|
|
14038
|
-
}
|
|
14039
|
-
const defaultRule = getDefaultTaxRule(issuerCountry);
|
|
14040
|
-
if (defaultRule) {
|
|
14041
|
-
return { tax_rate: defaultRule.rate, tax_name: defaultRule.tax_name, reverse_charge: false };
|
|
14042
|
-
}
|
|
14043
|
-
return { tax_rate: 0, tax_name: "Tax", reverse_charge: false };
|
|
14579
|
+
function listAgents(db) {
|
|
14580
|
+
return db.query("SELECT * FROM agents ORDER BY last_seen_at DESC").all().map(mapAgent);
|
|
14044
14581
|
}
|
|
14045
14582
|
|
|
14046
14583
|
// src/mcp/index.ts
|
|
14584
|
+
function getMcpVersion() {
|
|
14585
|
+
try {
|
|
14586
|
+
const __dir = dirname3(fileURLToPath(import.meta.url));
|
|
14587
|
+
const pkgPath = join7(__dir, "..", "..", "package.json");
|
|
14588
|
+
return JSON.parse(readFileSync2(pkgPath, "utf-8")).version || "0.0.0";
|
|
14589
|
+
} catch {
|
|
14590
|
+
return "0.0.0";
|
|
14591
|
+
}
|
|
14592
|
+
}
|
|
14047
14593
|
var server = new McpServer({
|
|
14048
14594
|
name: "invoices",
|
|
14049
|
-
version:
|
|
14595
|
+
version: getMcpVersion()
|
|
14596
|
+
});
|
|
14597
|
+
var statusSchema = exports_external.enum(["draft", "sent", "partially_paid", "paid", "void", "overdue"]);
|
|
14598
|
+
server.registerTool("health", {
|
|
14599
|
+
title: "Health",
|
|
14600
|
+
description: "Health check and current SQLite database path.",
|
|
14601
|
+
inputSchema: {}
|
|
14602
|
+
}, async () => {
|
|
14603
|
+
const db = openInvoiceDatabase();
|
|
14604
|
+
const path = db.filename;
|
|
14605
|
+
db.close();
|
|
14606
|
+
return { content: [{ type: "text", text: JSON.stringify({ ok: true, dbPath: path }) }] };
|
|
14607
|
+
});
|
|
14608
|
+
server.registerTool("migrate_database", {
|
|
14609
|
+
title: "Migrate Database",
|
|
14610
|
+
description: "Apply pending SQLite schema migrations.",
|
|
14611
|
+
inputSchema: {}
|
|
14612
|
+
}, async () => {
|
|
14613
|
+
const applied = migrateDatabase();
|
|
14614
|
+
return { content: [{ type: "text", text: JSON.stringify({ applied }) }] };
|
|
14615
|
+
});
|
|
14616
|
+
server.registerTool("create_party", {
|
|
14617
|
+
title: "Create Party",
|
|
14618
|
+
description: "Create invoice issuer/customer party.",
|
|
14619
|
+
inputSchema: {
|
|
14620
|
+
kind: exports_external.enum(["issuer", "customer"]),
|
|
14621
|
+
legal_name: exports_external.string().min(1),
|
|
14622
|
+
email: exports_external.string().email().optional(),
|
|
14623
|
+
tax_id: exports_external.string().optional(),
|
|
14624
|
+
country: exports_external.string().optional(),
|
|
14625
|
+
address: exports_external.string().optional()
|
|
14626
|
+
}
|
|
14627
|
+
}, async (input) => {
|
|
14628
|
+
const db = openInvoiceDatabase();
|
|
14629
|
+
try {
|
|
14630
|
+
const party = createParty(db, {
|
|
14631
|
+
kind: input.kind,
|
|
14632
|
+
legalName: input.legal_name,
|
|
14633
|
+
email: input.email,
|
|
14634
|
+
taxId: input.tax_id,
|
|
14635
|
+
country: input.country,
|
|
14636
|
+
address: input.address
|
|
14637
|
+
});
|
|
14638
|
+
return { content: [{ type: "text", text: JSON.stringify(party) }] };
|
|
14639
|
+
} finally {
|
|
14640
|
+
db.close();
|
|
14641
|
+
}
|
|
14642
|
+
});
|
|
14643
|
+
server.registerTool("list_parties", {
|
|
14644
|
+
title: "List Parties",
|
|
14645
|
+
description: "List registered invoice parties.",
|
|
14646
|
+
inputSchema: {
|
|
14647
|
+
kind: exports_external.enum(["issuer", "customer"]).optional()
|
|
14648
|
+
}
|
|
14649
|
+
}, async (input) => {
|
|
14650
|
+
const db = openInvoiceDatabase();
|
|
14651
|
+
try {
|
|
14652
|
+
const parties = listParties(db, input.kind);
|
|
14653
|
+
return { content: [{ type: "text", text: JSON.stringify(parties) }] };
|
|
14654
|
+
} finally {
|
|
14655
|
+
db.close();
|
|
14656
|
+
}
|
|
14050
14657
|
});
|
|
14051
14658
|
server.registerTool("create_invoice", {
|
|
14052
14659
|
title: "Create Invoice",
|
|
14053
|
-
description: "Create
|
|
14660
|
+
description: "Create invoice with lines and calculated totals.",
|
|
14054
14661
|
inputSchema: {
|
|
14055
|
-
|
|
14056
|
-
|
|
14057
|
-
|
|
14058
|
-
currency: exports_external.string().
|
|
14059
|
-
tax_rate: exports_external.number().optional(),
|
|
14060
|
-
tax_name: exports_external.string().optional(),
|
|
14061
|
-
discount: exports_external.number().optional(),
|
|
14062
|
-
reverse_charge: exports_external.boolean().optional(),
|
|
14063
|
-
language: exports_external.string().optional(),
|
|
14662
|
+
number: exports_external.string().min(1),
|
|
14663
|
+
issuer_id: exports_external.string().min(1),
|
|
14664
|
+
customer_id: exports_external.string().min(1),
|
|
14665
|
+
currency: exports_external.string().min(3),
|
|
14064
14666
|
notes: exports_external.string().optional(),
|
|
14065
|
-
|
|
14667
|
+
issued_at: exports_external.string().min(1),
|
|
14668
|
+
due_at: exports_external.string().optional(),
|
|
14669
|
+
lines: exports_external.array(exports_external.object({
|
|
14670
|
+
description: exports_external.string().min(1),
|
|
14671
|
+
quantity: exports_external.number().positive(),
|
|
14672
|
+
unit_price_cents: exports_external.number().int().nonnegative(),
|
|
14673
|
+
tax_rate_basis_points: exports_external.number().int().nonnegative().optional()
|
|
14674
|
+
}))
|
|
14675
|
+
}
|
|
14676
|
+
}, async (input) => {
|
|
14677
|
+
const db = openInvoiceDatabase();
|
|
14678
|
+
try {
|
|
14679
|
+
const invoice = createInvoice(db, {
|
|
14680
|
+
number: input.number,
|
|
14681
|
+
issuerId: input.issuer_id,
|
|
14682
|
+
customerId: input.customer_id,
|
|
14683
|
+
currency: input.currency,
|
|
14684
|
+
notes: input.notes,
|
|
14685
|
+
issuedAt: input.issued_at,
|
|
14686
|
+
dueAt: input.due_at,
|
|
14687
|
+
lines: input.lines.map((line) => ({
|
|
14688
|
+
description: line.description,
|
|
14689
|
+
quantity: line.quantity,
|
|
14690
|
+
unitPriceCents: line.unit_price_cents,
|
|
14691
|
+
taxRateBasisPoints: line.tax_rate_basis_points
|
|
14692
|
+
}))
|
|
14693
|
+
});
|
|
14694
|
+
return { content: [{ type: "text", text: JSON.stringify(invoice) }] };
|
|
14695
|
+
} finally {
|
|
14696
|
+
db.close();
|
|
14066
14697
|
}
|
|
14067
|
-
}, async (params) => {
|
|
14068
|
-
const invoice = createInvoice(params);
|
|
14069
|
-
return { content: [{ type: "text", text: JSON.stringify(invoice, null, 2) }] };
|
|
14070
14698
|
});
|
|
14071
14699
|
server.registerTool("get_invoice", {
|
|
14072
14700
|
title: "Get Invoice",
|
|
14073
|
-
description: "
|
|
14074
|
-
inputSchema: {
|
|
14075
|
-
|
|
14076
|
-
|
|
14077
|
-
|
|
14078
|
-
|
|
14079
|
-
|
|
14080
|
-
|
|
14701
|
+
description: "Fetch invoice details including lines.",
|
|
14702
|
+
inputSchema: {
|
|
14703
|
+
id: exports_external.string().min(1)
|
|
14704
|
+
}
|
|
14705
|
+
}, async (input) => {
|
|
14706
|
+
const db = openInvoiceDatabase();
|
|
14707
|
+
try {
|
|
14708
|
+
const invoice = getInvoiceById(db, input.id);
|
|
14709
|
+
return { content: [{ type: "text", text: JSON.stringify(invoice) }] };
|
|
14710
|
+
} finally {
|
|
14711
|
+
db.close();
|
|
14712
|
+
}
|
|
14081
14713
|
});
|
|
14082
14714
|
server.registerTool("list_invoices", {
|
|
14083
14715
|
title: "List Invoices",
|
|
14084
|
-
description: "List invoices with optional
|
|
14716
|
+
description: "List invoices with optional status filter.",
|
|
14085
14717
|
inputSchema: {
|
|
14086
|
-
status:
|
|
14087
|
-
|
|
14088
|
-
|
|
14089
|
-
|
|
14090
|
-
|
|
14091
|
-
|
|
14092
|
-
|
|
14093
|
-
|
|
14094
|
-
|
|
14095
|
-
|
|
14096
|
-
|
|
14718
|
+
status: statusSchema.optional(),
|
|
14719
|
+
limit: exports_external.number().int().positive().max(200).optional(),
|
|
14720
|
+
offset: exports_external.number().int().nonnegative().optional()
|
|
14721
|
+
}
|
|
14722
|
+
}, async (input) => {
|
|
14723
|
+
const db = openInvoiceDatabase();
|
|
14724
|
+
try {
|
|
14725
|
+
const invoices = listInvoices(db, {
|
|
14726
|
+
status: input.status,
|
|
14727
|
+
limit: input.limit,
|
|
14728
|
+
offset: input.offset
|
|
14729
|
+
});
|
|
14730
|
+
return { content: [{ type: "text", text: JSON.stringify(invoices) }] };
|
|
14731
|
+
} finally {
|
|
14732
|
+
db.close();
|
|
14733
|
+
}
|
|
14734
|
+
});
|
|
14735
|
+
server.registerTool("search_invoices", {
|
|
14736
|
+
title: "Search Invoices",
|
|
14737
|
+
description: "Full-text search over invoice number, issuer/customer names, and notes.",
|
|
14738
|
+
inputSchema: {
|
|
14739
|
+
query: exports_external.string().min(1),
|
|
14740
|
+
status: statusSchema.optional(),
|
|
14741
|
+
limit: exports_external.number().int().positive().max(200).optional(),
|
|
14742
|
+
offset: exports_external.number().int().nonnegative().optional()
|
|
14743
|
+
}
|
|
14744
|
+
}, async (input) => {
|
|
14745
|
+
const db = openInvoiceDatabase();
|
|
14746
|
+
try {
|
|
14747
|
+
const invoices = searchInvoices(db, input.query, {
|
|
14748
|
+
status: input.status,
|
|
14749
|
+
limit: input.limit,
|
|
14750
|
+
offset: input.offset
|
|
14751
|
+
});
|
|
14752
|
+
return { content: [{ type: "text", text: JSON.stringify(invoices) }] };
|
|
14753
|
+
} finally {
|
|
14754
|
+
db.close();
|
|
14755
|
+
}
|
|
14097
14756
|
});
|
|
14098
14757
|
server.registerTool("update_invoice_status", {
|
|
14099
14758
|
title: "Update Invoice Status",
|
|
14100
|
-
description: "
|
|
14759
|
+
description: "Update invoice lifecycle status.",
|
|
14101
14760
|
inputSchema: {
|
|
14102
|
-
id: exports_external.string(),
|
|
14103
|
-
status:
|
|
14761
|
+
id: exports_external.string().min(1),
|
|
14762
|
+
status: statusSchema
|
|
14104
14763
|
}
|
|
14105
|
-
}, async (
|
|
14106
|
-
const
|
|
14107
|
-
|
|
14108
|
-
|
|
14764
|
+
}, async (input) => {
|
|
14765
|
+
const db = openInvoiceDatabase();
|
|
14766
|
+
try {
|
|
14767
|
+
const invoice = updateInvoiceStatus(db, input.id, input.status);
|
|
14768
|
+
return { content: [{ type: "text", text: JSON.stringify(invoice) }] };
|
|
14769
|
+
} finally {
|
|
14770
|
+
db.close();
|
|
14109
14771
|
}
|
|
14110
|
-
return { content: [{ type: "text", text: JSON.stringify(invoice, null, 2) }] };
|
|
14111
14772
|
});
|
|
14112
14773
|
server.registerTool("delete_invoice", {
|
|
14113
14774
|
title: "Delete Invoice",
|
|
14114
|
-
description: "Delete
|
|
14115
|
-
inputSchema: { id: exports_external.string() }
|
|
14116
|
-
}, async ({ id }) => {
|
|
14117
|
-
const deleted = deleteInvoice(id);
|
|
14118
|
-
return { content: [{ type: "text", text: JSON.stringify({ id, deleted }) }] };
|
|
14119
|
-
});
|
|
14120
|
-
server.registerTool("add_line_item", {
|
|
14121
|
-
title: "Add Line Item",
|
|
14122
|
-
description: "Add a line item to an invoice. Automatically recalculates subtotal, tax, and total.",
|
|
14775
|
+
description: "Delete invoice and lines by id.",
|
|
14123
14776
|
inputSchema: {
|
|
14124
|
-
|
|
14125
|
-
|
|
14126
|
-
|
|
14127
|
-
|
|
14128
|
-
|
|
14129
|
-
|
|
14130
|
-
|
|
14131
|
-
|
|
14132
|
-
|
|
14133
|
-
|
|
14134
|
-
title: "Remove Line Item",
|
|
14135
|
-
description: "Remove a line item from an invoice. Recalculates totals.",
|
|
14136
|
-
inputSchema: { id: exports_external.string() }
|
|
14137
|
-
}, async ({ id }) => {
|
|
14138
|
-
const removed = removeLineItem(id);
|
|
14139
|
-
return { content: [{ type: "text", text: JSON.stringify({ id, removed }) }] };
|
|
14140
|
-
});
|
|
14141
|
-
server.registerTool("record_payment", {
|
|
14142
|
-
title: "Record Payment",
|
|
14143
|
-
description: "Record a payment against an invoice. Auto-marks invoice as paid when fully covered.",
|
|
14144
|
-
inputSchema: {
|
|
14145
|
-
invoice_id: exports_external.string(),
|
|
14146
|
-
amount: exports_external.number(),
|
|
14147
|
-
method: exports_external.string().optional(),
|
|
14148
|
-
reference: exports_external.string().optional(),
|
|
14149
|
-
notes: exports_external.string().optional()
|
|
14150
|
-
}
|
|
14151
|
-
}, async (params) => {
|
|
14152
|
-
const payment = recordPayment(params);
|
|
14153
|
-
return { content: [{ type: "text", text: JSON.stringify(payment, null, 2) }] };
|
|
14154
|
-
});
|
|
14155
|
-
server.registerTool("invoice_summary", {
|
|
14156
|
-
title: "Invoice Summary",
|
|
14157
|
-
description: "Get summary statistics: total invoices by status, outstanding and collected amounts.",
|
|
14158
|
-
inputSchema: {}
|
|
14159
|
-
}, async () => {
|
|
14160
|
-
const summary = getInvoiceSummary();
|
|
14161
|
-
return { content: [{ type: "text", text: JSON.stringify(summary, null, 2) }] };
|
|
14777
|
+
id: exports_external.string().min(1)
|
|
14778
|
+
}
|
|
14779
|
+
}, async (input) => {
|
|
14780
|
+
const db = openInvoiceDatabase();
|
|
14781
|
+
try {
|
|
14782
|
+
const deleted = deleteInvoice(db, input.id);
|
|
14783
|
+
return { content: [{ type: "text", text: JSON.stringify({ deleted }) }] };
|
|
14784
|
+
} finally {
|
|
14785
|
+
db.close();
|
|
14786
|
+
}
|
|
14162
14787
|
});
|
|
14163
|
-
server.registerTool("
|
|
14164
|
-
title: "
|
|
14165
|
-
description: "
|
|
14788
|
+
server.registerTool("register_agent", {
|
|
14789
|
+
title: "Register Agent",
|
|
14790
|
+
description: "Register or refresh an agent identity.",
|
|
14166
14791
|
inputSchema: {
|
|
14167
|
-
name: exports_external.string(),
|
|
14168
|
-
|
|
14169
|
-
|
|
14170
|
-
|
|
14171
|
-
|
|
14172
|
-
|
|
14173
|
-
|
|
14174
|
-
|
|
14175
|
-
|
|
14176
|
-
|
|
14177
|
-
|
|
14178
|
-
|
|
14179
|
-
|
|
14792
|
+
name: exports_external.string().min(1),
|
|
14793
|
+
description: exports_external.string().optional()
|
|
14794
|
+
}
|
|
14795
|
+
}, async (input) => {
|
|
14796
|
+
const db = openInvoiceDatabase();
|
|
14797
|
+
try {
|
|
14798
|
+
const agent = registerAgent(db, {
|
|
14799
|
+
name: input.name,
|
|
14800
|
+
description: input.description
|
|
14801
|
+
});
|
|
14802
|
+
return { content: [{ type: "text", text: JSON.stringify(agent) }] };
|
|
14803
|
+
} finally {
|
|
14804
|
+
db.close();
|
|
14180
14805
|
}
|
|
14181
|
-
}, async (params) => {
|
|
14182
|
-
const client = createClient(params);
|
|
14183
|
-
return { content: [{ type: "text", text: JSON.stringify(client, null, 2) }] };
|
|
14184
|
-
});
|
|
14185
|
-
server.registerTool("list_clients", {
|
|
14186
|
-
title: "List Clients",
|
|
14187
|
-
description: "List all clients. Optionally search by name or email.",
|
|
14188
|
-
inputSchema: { search: exports_external.string().optional() }
|
|
14189
|
-
}, async ({ search }) => {
|
|
14190
|
-
const clients = listClients(search);
|
|
14191
|
-
return {
|
|
14192
|
-
content: [{ type: "text", text: JSON.stringify({ clients, count: clients.length }, null, 2) }]
|
|
14193
|
-
};
|
|
14194
14806
|
});
|
|
14195
|
-
server.registerTool("
|
|
14196
|
-
title: "
|
|
14197
|
-
description: "Update
|
|
14807
|
+
server.registerTool("heartbeat", {
|
|
14808
|
+
title: "Heartbeat",
|
|
14809
|
+
description: "Update agent last_seen_at.",
|
|
14198
14810
|
inputSchema: {
|
|
14199
|
-
|
|
14200
|
-
name: exports_external.string().optional(),
|
|
14201
|
-
email: exports_external.string().optional(),
|
|
14202
|
-
phone: exports_external.string().optional(),
|
|
14203
|
-
country: exports_external.string().optional(),
|
|
14204
|
-
vat_number: exports_external.string().optional(),
|
|
14205
|
-
notes: exports_external.string().optional()
|
|
14811
|
+
agent_id: exports_external.string().min(1)
|
|
14206
14812
|
}
|
|
14207
|
-
}, async (
|
|
14208
|
-
const
|
|
14209
|
-
|
|
14210
|
-
|
|
14813
|
+
}, async (input) => {
|
|
14814
|
+
const db = openInvoiceDatabase();
|
|
14815
|
+
try {
|
|
14816
|
+
const agent = heartbeatAgent(db, input.agent_id);
|
|
14817
|
+
return { content: [{ type: "text", text: JSON.stringify(agent) }] };
|
|
14818
|
+
} finally {
|
|
14819
|
+
db.close();
|
|
14211
14820
|
}
|
|
14212
|
-
return { content: [{ type: "text", text: JSON.stringify(client, null, 2) }] };
|
|
14213
|
-
});
|
|
14214
|
-
server.registerTool("delete_client", {
|
|
14215
|
-
title: "Delete Client",
|
|
14216
|
-
description: "Delete a client.",
|
|
14217
|
-
inputSchema: { id: exports_external.string() }
|
|
14218
|
-
}, async ({ id }) => {
|
|
14219
|
-
const deleted = deleteClient(id);
|
|
14220
|
-
return { content: [{ type: "text", text: JSON.stringify({ id, deleted }) }] };
|
|
14221
14821
|
});
|
|
14222
|
-
server.registerTool("
|
|
14223
|
-
title: "
|
|
14224
|
-
description: "
|
|
14822
|
+
server.registerTool("set_focus", {
|
|
14823
|
+
title: "Set Focus",
|
|
14824
|
+
description: "Set or clear current agent focus.",
|
|
14225
14825
|
inputSchema: {
|
|
14226
|
-
|
|
14227
|
-
|
|
14228
|
-
|
|
14229
|
-
|
|
14230
|
-
|
|
14231
|
-
|
|
14232
|
-
|
|
14233
|
-
|
|
14234
|
-
|
|
14235
|
-
|
|
14236
|
-
|
|
14237
|
-
website: exports_external.string().optional(),
|
|
14238
|
-
bank_name: exports_external.string().optional(),
|
|
14239
|
-
bank_iban: exports_external.string().optional(),
|
|
14240
|
-
bank_swift: exports_external.string().optional(),
|
|
14241
|
-
bank_account: exports_external.string().optional(),
|
|
14242
|
-
logo_url: exports_external.string().optional(),
|
|
14243
|
-
is_default: exports_external.boolean().optional()
|
|
14244
|
-
}
|
|
14245
|
-
}, async (params) => {
|
|
14246
|
-
const biz = createBusinessProfile(params);
|
|
14247
|
-
return { content: [{ type: "text", text: JSON.stringify(biz, null, 2) }] };
|
|
14248
|
-
});
|
|
14249
|
-
server.registerTool("get_business_profile", {
|
|
14250
|
-
title: "Get Business Profile",
|
|
14251
|
-
description: "Get a business profile by ID.",
|
|
14252
|
-
inputSchema: { id: exports_external.string() }
|
|
14253
|
-
}, async ({ id }) => {
|
|
14254
|
-
const biz = getBusinessProfile(id);
|
|
14255
|
-
if (!biz) {
|
|
14256
|
-
return { content: [{ type: "text", text: `Business profile '${id}' not found.` }], isError: true };
|
|
14257
|
-
}
|
|
14258
|
-
return { content: [{ type: "text", text: JSON.stringify(biz, null, 2) }] };
|
|
14826
|
+
agent_id: exports_external.string().min(1),
|
|
14827
|
+
focus: exports_external.string().optional()
|
|
14828
|
+
}
|
|
14829
|
+
}, async (input) => {
|
|
14830
|
+
const db = openInvoiceDatabase();
|
|
14831
|
+
try {
|
|
14832
|
+
const agent = setAgentFocus(db, input.agent_id, input.focus);
|
|
14833
|
+
return { content: [{ type: "text", text: JSON.stringify(agent) }] };
|
|
14834
|
+
} finally {
|
|
14835
|
+
db.close();
|
|
14836
|
+
}
|
|
14259
14837
|
});
|
|
14260
|
-
server.registerTool("
|
|
14261
|
-
title: "
|
|
14262
|
-
description: "
|
|
14838
|
+
server.registerTool("list_agents", {
|
|
14839
|
+
title: "List Agents",
|
|
14840
|
+
description: "List registered agents and their metadata.",
|
|
14263
14841
|
inputSchema: {}
|
|
14264
14842
|
}, async () => {
|
|
14265
|
-
const
|
|
14266
|
-
|
|
14843
|
+
const db = openInvoiceDatabase();
|
|
14844
|
+
try {
|
|
14845
|
+
const agents = listAgents(db);
|
|
14846
|
+
return { content: [{ type: "text", text: JSON.stringify(agents) }] };
|
|
14847
|
+
} finally {
|
|
14848
|
+
db.close();
|
|
14849
|
+
}
|
|
14267
14850
|
});
|
|
14268
|
-
server.registerTool("
|
|
14269
|
-
title: "
|
|
14270
|
-
description: "
|
|
14271
|
-
inputSchema: {
|
|
14272
|
-
|
|
14273
|
-
|
|
14274
|
-
|
|
14851
|
+
server.registerTool("migrate_postgres", {
|
|
14852
|
+
title: "Migrate PostgreSQL",
|
|
14853
|
+
description: "Apply pending PostgreSQL migrations translated from local schema.",
|
|
14854
|
+
inputSchema: {
|
|
14855
|
+
connection_string: exports_external.string().optional()
|
|
14856
|
+
}
|
|
14857
|
+
}, async (input) => {
|
|
14858
|
+
const result = await migratePgDatabase(input.connection_string);
|
|
14859
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
14275
14860
|
});
|
|
14276
|
-
server.registerTool("
|
|
14277
|
-
title: "
|
|
14278
|
-
description: "
|
|
14861
|
+
server.registerTool("cloud_push", {
|
|
14862
|
+
title: "Cloud Push",
|
|
14863
|
+
description: "Push local SQLite tables to cloud PostgreSQL.",
|
|
14279
14864
|
inputSchema: {
|
|
14280
|
-
|
|
14281
|
-
|
|
14282
|
-
country: exports_external.string().optional(),
|
|
14283
|
-
email: exports_external.string().optional(),
|
|
14284
|
-
phone: exports_external.string().optional(),
|
|
14285
|
-
bank_iban: exports_external.string().optional(),
|
|
14286
|
-
is_default: exports_external.boolean().optional()
|
|
14865
|
+
tables: exports_external.string().optional(),
|
|
14866
|
+
connection_string: exports_external.string().optional()
|
|
14287
14867
|
}
|
|
14288
|
-
}, async (
|
|
14289
|
-
const
|
|
14290
|
-
|
|
14291
|
-
|
|
14868
|
+
}, async (input) => {
|
|
14869
|
+
const db = openInvoiceDatabase();
|
|
14870
|
+
try {
|
|
14871
|
+
const result = await cloudPush(db, input.tables, input.connection_string);
|
|
14872
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
14873
|
+
} finally {
|
|
14874
|
+
db.close();
|
|
14292
14875
|
}
|
|
14293
|
-
return { content: [{ type: "text", text: JSON.stringify(biz, null, 2) }] };
|
|
14294
|
-
});
|
|
14295
|
-
server.registerTool("delete_business_profile", {
|
|
14296
|
-
title: "Delete Business Profile",
|
|
14297
|
-
description: "Delete a business profile.",
|
|
14298
|
-
inputSchema: { id: exports_external.string() }
|
|
14299
|
-
}, async ({ id }) => {
|
|
14300
|
-
const deleted = deleteBusinessProfile(id);
|
|
14301
|
-
return { content: [{ type: "text", text: JSON.stringify({ id, deleted }) }] };
|
|
14302
|
-
});
|
|
14303
|
-
server.registerTool("list_tax_rules", {
|
|
14304
|
-
title: "List Tax Rules",
|
|
14305
|
-
description: "List tax rules. Optionally filter by country code.",
|
|
14306
|
-
inputSchema: { country: exports_external.string().optional() }
|
|
14307
|
-
}, async ({ country }) => {
|
|
14308
|
-
const rules = country ? getTaxRulesForCountry(country) : listAllTaxRules();
|
|
14309
|
-
return { content: [{ type: "text", text: JSON.stringify({ rules, count: rules.length }, null, 2) }] };
|
|
14310
14876
|
});
|
|
14311
|
-
server.registerTool("
|
|
14312
|
-
title: "
|
|
14313
|
-
description: "
|
|
14314
|
-
inputSchema: { country: exports_external.string() }
|
|
14315
|
-
}, async ({ country }) => {
|
|
14316
|
-
const rule = getDefaultTaxRule(country);
|
|
14317
|
-
return { content: [{ type: "text", text: JSON.stringify(rule, null, 2) }] };
|
|
14318
|
-
});
|
|
14319
|
-
server.registerTool("create_tax_rule", {
|
|
14320
|
-
title: "Create Tax Rule",
|
|
14321
|
-
description: "Create a custom tax rule for a country or region.",
|
|
14877
|
+
server.registerTool("cloud_pull", {
|
|
14878
|
+
title: "Cloud Pull",
|
|
14879
|
+
description: "Pull cloud PostgreSQL tables into local SQLite.",
|
|
14322
14880
|
inputSchema: {
|
|
14323
|
-
|
|
14324
|
-
|
|
14325
|
-
|
|
14326
|
-
|
|
14327
|
-
|
|
14328
|
-
|
|
14329
|
-
|
|
14330
|
-
|
|
14881
|
+
tables: exports_external.string().optional(),
|
|
14882
|
+
connection_string: exports_external.string().optional()
|
|
14883
|
+
}
|
|
14884
|
+
}, async (input) => {
|
|
14885
|
+
const db = openInvoiceDatabase();
|
|
14886
|
+
try {
|
|
14887
|
+
const result = await cloudPull(db, input.tables, input.connection_string);
|
|
14888
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
14889
|
+
} finally {
|
|
14890
|
+
db.close();
|
|
14331
14891
|
}
|
|
14332
|
-
}, async (params) => {
|
|
14333
|
-
const rule = createTaxRule(params);
|
|
14334
|
-
return { content: [{ type: "text", text: JSON.stringify(rule, null, 2) }] };
|
|
14335
|
-
});
|
|
14336
|
-
server.registerTool("delete_tax_rule", {
|
|
14337
|
-
title: "Delete Tax Rule",
|
|
14338
|
-
description: "Delete a tax rule.",
|
|
14339
|
-
inputSchema: { id: exports_external.string() }
|
|
14340
|
-
}, async ({ id }) => {
|
|
14341
|
-
const deleted = deleteTaxRule(id);
|
|
14342
|
-
return { content: [{ type: "text", text: JSON.stringify({ id, deleted }) }] };
|
|
14343
14892
|
});
|
|
14344
|
-
server.registerTool("
|
|
14345
|
-
title: "
|
|
14346
|
-
description: "
|
|
14893
|
+
server.registerTool("send_feedback", {
|
|
14894
|
+
title: "Send Feedback",
|
|
14895
|
+
description: "Send feedback to remote endpoint with local persistence fallback.",
|
|
14347
14896
|
inputSchema: {
|
|
14348
|
-
|
|
14349
|
-
|
|
14350
|
-
|
|
14897
|
+
message: exports_external.string().min(1),
|
|
14898
|
+
email: exports_external.string().email().optional(),
|
|
14899
|
+
version: exports_external.string().optional(),
|
|
14900
|
+
machine_id: exports_external.string().optional()
|
|
14901
|
+
}
|
|
14902
|
+
}, async (input) => {
|
|
14903
|
+
const db = openInvoiceDatabase();
|
|
14904
|
+
try {
|
|
14905
|
+
const result = await submitFeedback(db, {
|
|
14906
|
+
service: "invoices",
|
|
14907
|
+
message: input.message,
|
|
14908
|
+
email: input.email,
|
|
14909
|
+
version: input.version,
|
|
14910
|
+
machine_id: input.machine_id
|
|
14911
|
+
});
|
|
14912
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
14913
|
+
} finally {
|
|
14914
|
+
db.close();
|
|
14351
14915
|
}
|
|
14352
|
-
}, async ({ issuer_country, client_country, client_vat_number }) => {
|
|
14353
|
-
const result = determineTax(issuer_country, client_country, client_vat_number);
|
|
14354
|
-
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
14355
|
-
});
|
|
14356
|
-
async function main() {
|
|
14357
|
-
const transport = new StdioServerTransport;
|
|
14358
|
-
await server.connect(transport);
|
|
14359
|
-
console.error("invoices MCP server running on stdio");
|
|
14360
|
-
}
|
|
14361
|
-
main().catch((error) => {
|
|
14362
|
-
console.error("Fatal error:", error);
|
|
14363
|
-
process.exit(1);
|
|
14364
14916
|
});
|
|
14917
|
+
var transport = new StdioServerTransport;
|
|
14918
|
+
await server.connect(transport);
|