@resvary/sqlite 0.5.0 → 0.7.0

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/dist/credit.js CHANGED
@@ -1,16 +1,15 @@
1
- import { mkdirSync } from 'node:fs';
2
- import { dirname } from 'node:path';
3
1
  import { DatabaseSync } from 'node:sqlite';
2
+ import { creditUnitsToString, parseCreditUnits } from '@resvary/sdk/credits';
4
3
  import { parseReceiptStoreValue, serializeReceiptStoreValue } from '@resvary/sdk/receipts';
4
+ import { hardenSqliteDatabaseFiles, prepareSqliteDatabasePath } from './filesystem.js';
5
5
  export class SqliteCreditStore {
6
6
  db;
7
7
  transactionTail = Promise.resolve();
8
8
  constructor(config) {
9
- if (config.path !== ':memory:' && config.createDirectory !== false) {
10
- mkdirSync(dirname(config.path), { recursive: true });
11
- }
9
+ prepareSqliteDatabasePath(config.path, config.createDirectory !== false);
12
10
  this.db = new DatabaseSync(config.path);
13
11
  this.migrate();
12
+ hardenSqliteDatabaseFiles(config.path);
14
13
  }
15
14
  async transaction(handler) {
16
15
  let release;
@@ -106,6 +105,30 @@ export class SqliteCreditStore {
106
105
  listFundingTransactions(fundingIntentId) {
107
106
  return reader(this.db).listFundingTransactions(fundingIntentId);
108
107
  }
108
+ getGrantPolicy(id) {
109
+ return reader(this.db).getGrantPolicy(id);
110
+ }
111
+ listGrantPolicies(projectId) {
112
+ return reader(this.db).listGrantPolicies(projectId);
113
+ }
114
+ getCreditLot(id) {
115
+ return reader(this.db).getCreditLot(id);
116
+ }
117
+ listCreditLots(filter) {
118
+ return reader(this.db).listCreditLots(filter);
119
+ }
120
+ listCreditLotAllocations(reservationId) {
121
+ return reader(this.db).listCreditLotAllocations(reservationId);
122
+ }
123
+ getGrantPolicyApplication(id) {
124
+ return reader(this.db).getGrantPolicyApplication(id);
125
+ }
126
+ getGrantPolicyApplicationByIdentity(policyId, accountId, periodKey) {
127
+ return reader(this.db).getGrantPolicyApplicationByIdentity(policyId, accountId, periodKey);
128
+ }
129
+ listGrantPolicyApplications(filter) {
130
+ return reader(this.db).listGrantPolicyApplications(filter);
131
+ }
109
132
  claimOutboxEvents(input) {
110
133
  return this.transaction(async (tx) => {
111
134
  const events = (await tx.listOutboxEvents({ projectId: input.projectId }))
@@ -182,132 +205,133 @@ export class SqliteCreditStore {
182
205
  this.db.close();
183
206
  }
184
207
  migrate() {
185
- this.db.exec(`
186
- PRAGMA journal_mode = WAL;
187
- PRAGMA busy_timeout = 5000;
188
- PRAGMA foreign_keys = ON;
189
-
190
- CREATE TABLE IF NOT EXISTS resvary_schema_migrations (
191
- version INTEGER PRIMARY KEY,
192
- applied_at INTEGER NOT NULL
193
- );
194
-
195
- CREATE TABLE IF NOT EXISTS resvary_credit_accounts (
196
- id TEXT PRIMARY KEY,
197
- project_id TEXT NOT NULL,
198
- customer_id TEXT NOT NULL,
199
- updated_at INTEGER NOT NULL,
200
- payload TEXT NOT NULL,
201
- UNIQUE(project_id, customer_id)
202
- );
203
-
204
- CREATE TABLE IF NOT EXISTS resvary_credit_grants (
205
- id TEXT PRIMARY KEY,
206
- account_id TEXT NOT NULL,
207
- created_at INTEGER NOT NULL,
208
- payload TEXT NOT NULL
209
- );
210
- CREATE INDEX IF NOT EXISTS resvary_credit_grants_account ON resvary_credit_grants(account_id);
211
-
212
- CREATE TABLE IF NOT EXISTS resvary_meters (
213
- id TEXT PRIMARY KEY,
214
- project_id TEXT NOT NULL,
215
- meter_key TEXT NOT NULL,
216
- payload TEXT NOT NULL,
217
- UNIQUE(project_id, meter_key)
218
- );
219
-
220
- CREATE TABLE IF NOT EXISTS resvary_price_versions (
221
- id TEXT PRIMARY KEY,
222
- meter_id TEXT NOT NULL,
223
- version INTEGER NOT NULL,
224
- created_at INTEGER NOT NULL,
225
- payload TEXT NOT NULL,
226
- UNIQUE(meter_id, version)
227
- );
228
-
229
- CREATE TABLE IF NOT EXISTS resvary_credit_reservations (
230
- id TEXT PRIMARY KEY,
231
- account_id TEXT NOT NULL,
232
- project_id TEXT NOT NULL,
233
- customer_id TEXT NOT NULL,
234
- status TEXT NOT NULL,
235
- expires_at INTEGER NOT NULL,
236
- created_at INTEGER NOT NULL,
237
- payload TEXT NOT NULL
238
- );
239
- CREATE INDEX IF NOT EXISTS resvary_credit_reservations_open
240
- ON resvary_credit_reservations(project_id, customer_id, status, expires_at);
241
-
242
- CREATE TABLE IF NOT EXISTS resvary_usage_events (
243
- id TEXT PRIMARY KEY,
244
- account_id TEXT NOT NULL,
245
- received_at INTEGER NOT NULL,
246
- payload TEXT NOT NULL
247
- );
248
-
249
- CREATE TABLE IF NOT EXISTS resvary_usage_receipts (
250
- id TEXT PRIMARY KEY,
251
- account_id TEXT NOT NULL,
252
- reservation_id TEXT NOT NULL UNIQUE,
253
- usage_event_id TEXT NOT NULL UNIQUE,
254
- created_at INTEGER NOT NULL,
255
- payload TEXT NOT NULL
256
- );
257
- CREATE INDEX IF NOT EXISTS resvary_usage_receipts_account ON resvary_usage_receipts(account_id, created_at);
258
-
259
- CREATE TABLE IF NOT EXISTS resvary_ledger_entries (
260
- id TEXT PRIMARY KEY,
261
- account_id TEXT NOT NULL,
262
- created_at INTEGER NOT NULL,
263
- payload TEXT NOT NULL
264
- );
265
- CREATE INDEX IF NOT EXISTS resvary_ledger_entries_account ON resvary_ledger_entries(account_id, created_at);
266
-
267
- CREATE TABLE IF NOT EXISTS resvary_funding_intents (
268
- id TEXT PRIMARY KEY,
269
- project_id TEXT NOT NULL,
270
- customer_id TEXT NOT NULL,
271
- status TEXT NOT NULL,
272
- created_at INTEGER NOT NULL,
273
- payload TEXT NOT NULL
274
- );
275
-
276
- CREATE TABLE IF NOT EXISTS resvary_funding_transactions (
277
- id TEXT PRIMARY KEY,
278
- funding_intent_id TEXT NOT NULL,
279
- network TEXT NOT NULL,
280
- tx_hash_norm TEXT NOT NULL,
281
- created_at INTEGER NOT NULL,
282
- payload TEXT NOT NULL,
283
- UNIQUE(network, tx_hash_norm)
284
- );
285
-
286
- CREATE TABLE IF NOT EXISTS resvary_idempotency_keys (
287
- scope TEXT NOT NULL,
288
- key TEXT NOT NULL,
289
- created_at INTEGER NOT NULL,
290
- payload TEXT NOT NULL,
291
- PRIMARY KEY(scope, key)
292
- );
293
-
294
- CREATE TABLE IF NOT EXISTS resvary_outbox_events (
295
- id TEXT PRIMARY KEY,
296
- project_id TEXT NOT NULL,
297
- type TEXT NOT NULL,
298
- status TEXT NOT NULL,
299
- created_at INTEGER NOT NULL,
300
- payload TEXT NOT NULL
301
- );
302
- CREATE INDEX IF NOT EXISTS resvary_outbox_events_pending
303
- ON resvary_outbox_events(project_id, status, created_at);
304
-
305
- INSERT OR IGNORE INTO resvary_schema_migrations(version, applied_at)
306
- VALUES (1, CAST(strftime('%s', 'now') AS INTEGER) * 1000);
208
+ this.db.exec(`
209
+ PRAGMA journal_mode = WAL;
210
+ PRAGMA busy_timeout = 5000;
211
+ PRAGMA foreign_keys = ON;
212
+
213
+ CREATE TABLE IF NOT EXISTS resvary_schema_migrations (
214
+ version INTEGER PRIMARY KEY,
215
+ applied_at INTEGER NOT NULL
216
+ );
217
+
218
+ CREATE TABLE IF NOT EXISTS resvary_credit_accounts (
219
+ id TEXT PRIMARY KEY,
220
+ project_id TEXT NOT NULL,
221
+ customer_id TEXT NOT NULL,
222
+ updated_at INTEGER NOT NULL,
223
+ payload TEXT NOT NULL,
224
+ UNIQUE(project_id, customer_id)
225
+ );
226
+
227
+ CREATE TABLE IF NOT EXISTS resvary_credit_grants (
228
+ id TEXT PRIMARY KEY,
229
+ account_id TEXT NOT NULL,
230
+ created_at INTEGER NOT NULL,
231
+ payload TEXT NOT NULL
232
+ );
233
+ CREATE INDEX IF NOT EXISTS resvary_credit_grants_account ON resvary_credit_grants(account_id);
234
+
235
+ CREATE TABLE IF NOT EXISTS resvary_meters (
236
+ id TEXT PRIMARY KEY,
237
+ project_id TEXT NOT NULL,
238
+ meter_key TEXT NOT NULL,
239
+ payload TEXT NOT NULL,
240
+ UNIQUE(project_id, meter_key)
241
+ );
242
+
243
+ CREATE TABLE IF NOT EXISTS resvary_price_versions (
244
+ id TEXT PRIMARY KEY,
245
+ meter_id TEXT NOT NULL,
246
+ version INTEGER NOT NULL,
247
+ created_at INTEGER NOT NULL,
248
+ payload TEXT NOT NULL,
249
+ UNIQUE(meter_id, version)
250
+ );
251
+
252
+ CREATE TABLE IF NOT EXISTS resvary_credit_reservations (
253
+ id TEXT PRIMARY KEY,
254
+ account_id TEXT NOT NULL,
255
+ project_id TEXT NOT NULL,
256
+ customer_id TEXT NOT NULL,
257
+ status TEXT NOT NULL,
258
+ expires_at INTEGER NOT NULL,
259
+ created_at INTEGER NOT NULL,
260
+ payload TEXT NOT NULL
261
+ );
262
+ CREATE INDEX IF NOT EXISTS resvary_credit_reservations_open
263
+ ON resvary_credit_reservations(project_id, customer_id, status, expires_at);
264
+
265
+ CREATE TABLE IF NOT EXISTS resvary_usage_events (
266
+ id TEXT PRIMARY KEY,
267
+ account_id TEXT NOT NULL,
268
+ received_at INTEGER NOT NULL,
269
+ payload TEXT NOT NULL
270
+ );
271
+
272
+ CREATE TABLE IF NOT EXISTS resvary_usage_receipts (
273
+ id TEXT PRIMARY KEY,
274
+ account_id TEXT NOT NULL,
275
+ reservation_id TEXT NOT NULL UNIQUE,
276
+ usage_event_id TEXT NOT NULL UNIQUE,
277
+ created_at INTEGER NOT NULL,
278
+ payload TEXT NOT NULL
279
+ );
280
+ CREATE INDEX IF NOT EXISTS resvary_usage_receipts_account ON resvary_usage_receipts(account_id, created_at);
281
+
282
+ CREATE TABLE IF NOT EXISTS resvary_ledger_entries (
283
+ id TEXT PRIMARY KEY,
284
+ account_id TEXT NOT NULL,
285
+ created_at INTEGER NOT NULL,
286
+ payload TEXT NOT NULL
287
+ );
288
+ CREATE INDEX IF NOT EXISTS resvary_ledger_entries_account ON resvary_ledger_entries(account_id, created_at);
289
+
290
+ CREATE TABLE IF NOT EXISTS resvary_funding_intents (
291
+ id TEXT PRIMARY KEY,
292
+ project_id TEXT NOT NULL,
293
+ customer_id TEXT NOT NULL,
294
+ status TEXT NOT NULL,
295
+ created_at INTEGER NOT NULL,
296
+ payload TEXT NOT NULL
297
+ );
298
+
299
+ CREATE TABLE IF NOT EXISTS resvary_funding_transactions (
300
+ id TEXT PRIMARY KEY,
301
+ funding_intent_id TEXT NOT NULL,
302
+ network TEXT NOT NULL,
303
+ tx_hash_norm TEXT NOT NULL,
304
+ created_at INTEGER NOT NULL,
305
+ payload TEXT NOT NULL,
306
+ UNIQUE(network, tx_hash_norm)
307
+ );
308
+
309
+ CREATE TABLE IF NOT EXISTS resvary_idempotency_keys (
310
+ scope TEXT NOT NULL,
311
+ key TEXT NOT NULL,
312
+ created_at INTEGER NOT NULL,
313
+ payload TEXT NOT NULL,
314
+ PRIMARY KEY(scope, key)
315
+ );
316
+
317
+ CREATE TABLE IF NOT EXISTS resvary_outbox_events (
318
+ id TEXT PRIMARY KEY,
319
+ project_id TEXT NOT NULL,
320
+ type TEXT NOT NULL,
321
+ status TEXT NOT NULL,
322
+ created_at INTEGER NOT NULL,
323
+ payload TEXT NOT NULL
324
+ );
325
+ CREATE INDEX IF NOT EXISTS resvary_outbox_events_pending
326
+ ON resvary_outbox_events(project_id, status, created_at);
327
+
328
+ INSERT OR IGNORE INTO resvary_schema_migrations(version, applied_at)
329
+ VALUES (1, CAST(strftime('%s', 'now') AS INTEGER) * 1000);
307
330
  `);
308
331
  this.migrateFundingV2();
309
332
  this.migrateOutboxV3();
310
333
  this.migrateFundingTransactionHashV4();
334
+ this.migrateCreditLotsV5();
311
335
  }
312
336
  migrateFundingV2() {
313
337
  const row = this.db
@@ -317,51 +341,51 @@ export class SqliteCreditStore {
317
341
  return;
318
342
  this.db.exec('BEGIN IMMEDIATE;');
319
343
  try {
320
- this.db.exec(`
321
- ALTER TABLE resvary_funding_transactions
322
- ADD COLUMN rail TEXT NOT NULL DEFAULT 'arc_direct';
323
- ALTER TABLE resvary_funding_transactions
324
- ADD COLUMN external_payment_id_norm TEXT;
325
- UPDATE resvary_funding_transactions
326
- SET external_payment_id_norm = tx_hash_norm
327
- WHERE external_payment_id_norm IS NULL;
328
-
329
- ALTER TABLE resvary_funding_transactions
330
- RENAME TO resvary_funding_transactions_v1;
331
- CREATE TABLE resvary_funding_transactions (
332
- id TEXT PRIMARY KEY,
333
- funding_intent_id TEXT NOT NULL,
334
- rail TEXT NOT NULL,
335
- network TEXT NOT NULL,
336
- external_payment_id_norm TEXT NOT NULL,
337
- tx_hash_norm TEXT,
338
- created_at INTEGER NOT NULL,
339
- payload TEXT NOT NULL,
340
- UNIQUE(rail, network, external_payment_id_norm)
341
- );
342
- INSERT INTO resvary_funding_transactions(
343
- id,
344
- funding_intent_id,
345
- rail,
346
- network,
347
- external_payment_id_norm,
348
- tx_hash_norm,
349
- created_at,
350
- payload
351
- )
352
- SELECT
353
- id,
354
- funding_intent_id,
355
- rail,
356
- network,
357
- external_payment_id_norm,
358
- tx_hash_norm,
359
- created_at,
360
- payload
361
- FROM resvary_funding_transactions_v1;
362
- DROP TABLE resvary_funding_transactions_v1;
363
- CREATE INDEX resvary_funding_transactions_tx_hash
364
- ON resvary_funding_transactions(network, tx_hash_norm);
344
+ this.db.exec(`
345
+ ALTER TABLE resvary_funding_transactions
346
+ ADD COLUMN rail TEXT NOT NULL DEFAULT 'arc_direct';
347
+ ALTER TABLE resvary_funding_transactions
348
+ ADD COLUMN external_payment_id_norm TEXT;
349
+ UPDATE resvary_funding_transactions
350
+ SET external_payment_id_norm = tx_hash_norm
351
+ WHERE external_payment_id_norm IS NULL;
352
+
353
+ ALTER TABLE resvary_funding_transactions
354
+ RENAME TO resvary_funding_transactions_v1;
355
+ CREATE TABLE resvary_funding_transactions (
356
+ id TEXT PRIMARY KEY,
357
+ funding_intent_id TEXT NOT NULL,
358
+ rail TEXT NOT NULL,
359
+ network TEXT NOT NULL,
360
+ external_payment_id_norm TEXT NOT NULL,
361
+ tx_hash_norm TEXT,
362
+ created_at INTEGER NOT NULL,
363
+ payload TEXT NOT NULL,
364
+ UNIQUE(rail, network, external_payment_id_norm)
365
+ );
366
+ INSERT INTO resvary_funding_transactions(
367
+ id,
368
+ funding_intent_id,
369
+ rail,
370
+ network,
371
+ external_payment_id_norm,
372
+ tx_hash_norm,
373
+ created_at,
374
+ payload
375
+ )
376
+ SELECT
377
+ id,
378
+ funding_intent_id,
379
+ rail,
380
+ network,
381
+ external_payment_id_norm,
382
+ tx_hash_norm,
383
+ created_at,
384
+ payload
385
+ FROM resvary_funding_transactions_v1;
386
+ DROP TABLE resvary_funding_transactions_v1;
387
+ CREATE INDEX resvary_funding_transactions_tx_hash
388
+ ON resvary_funding_transactions(network, tx_hash_norm);
365
389
  `);
366
390
  const intents = this.db
367
391
  .prepare('SELECT id, payload FROM resvary_funding_intents')
@@ -391,10 +415,10 @@ export class SqliteCreditStore {
391
415
  },
392
416
  };
393
417
  this.db
394
- .prepare(`
395
- UPDATE resvary_funding_transactions
396
- SET rail = ?, external_payment_id_norm = ?, payload = ?
397
- WHERE id = ?
418
+ .prepare(`
419
+ UPDATE resvary_funding_transactions
420
+ SET rail = ?, external_payment_id_norm = ?, payload = ?
421
+ WHERE id = ?
398
422
  `)
399
423
  .run(migrated.rail, migrated.externalPaymentId.toLowerCase(), serializeReceiptStoreValue(migrated), transactionRow.id);
400
424
  }
@@ -416,14 +440,14 @@ export class SqliteCreditStore {
416
440
  return;
417
441
  this.db.exec('BEGIN IMMEDIATE;');
418
442
  try {
419
- this.db.exec(`
420
- ALTER TABLE resvary_outbox_events ADD COLUMN attempt_count INTEGER NOT NULL DEFAULT 0;
421
- ALTER TABLE resvary_outbox_events ADD COLUMN next_attempt_at INTEGER NOT NULL DEFAULT 0;
422
- ALTER TABLE resvary_outbox_events ADD COLUMN lease_owner TEXT;
423
- ALTER TABLE resvary_outbox_events ADD COLUMN lease_expires_at INTEGER;
424
- DROP INDEX IF EXISTS resvary_outbox_events_pending;
425
- CREATE INDEX resvary_outbox_events_due
426
- ON resvary_outbox_events(project_id, status, next_attempt_at, created_at);
443
+ this.db.exec(`
444
+ ALTER TABLE resvary_outbox_events ADD COLUMN attempt_count INTEGER NOT NULL DEFAULT 0;
445
+ ALTER TABLE resvary_outbox_events ADD COLUMN next_attempt_at INTEGER NOT NULL DEFAULT 0;
446
+ ALTER TABLE resvary_outbox_events ADD COLUMN lease_owner TEXT;
447
+ ALTER TABLE resvary_outbox_events ADD COLUMN lease_expires_at INTEGER;
448
+ DROP INDEX IF EXISTS resvary_outbox_events_pending;
449
+ CREATE INDEX resvary_outbox_events_due
450
+ ON resvary_outbox_events(project_id, status, next_attempt_at, created_at);
427
451
  `);
428
452
  const rows = this.db
429
453
  .prepare('SELECT id, created_at, payload FROM resvary_outbox_events')
@@ -436,8 +460,8 @@ export class SqliteCreditStore {
436
460
  nextAttemptAt: event.nextAttemptAt ?? item.created_at,
437
461
  };
438
462
  this.db
439
- .prepare(`UPDATE resvary_outbox_events
440
- SET attempt_count = ?, next_attempt_at = ?, lease_owner = ?, lease_expires_at = ?, payload = ?
463
+ .prepare(`UPDATE resvary_outbox_events
464
+ SET attempt_count = ?, next_attempt_at = ?, lease_owner = ?, lease_expires_at = ?, payload = ?
441
465
  WHERE id = ?`)
442
466
  .run(migrated.attemptCount, migrated.nextAttemptAt, migrated.leaseOwner ?? null, migrated.leaseExpiresAt ?? null, serializeReceiptStoreValue(migrated), item.id);
443
467
  }
@@ -460,21 +484,21 @@ export class SqliteCreditStore {
460
484
  this.db.exec('BEGIN IMMEDIATE;');
461
485
  try {
462
486
  const duplicate = this.db
463
- .prepare(`SELECT network, tx_hash_norm, COUNT(*) AS count
464
- FROM resvary_funding_transactions
465
- WHERE tx_hash_norm IS NOT NULL
466
- GROUP BY network, tx_hash_norm
467
- HAVING COUNT(*) > 1
487
+ .prepare(`SELECT network, tx_hash_norm, COUNT(*) AS count
488
+ FROM resvary_funding_transactions
489
+ WHERE tx_hash_norm IS NOT NULL
490
+ GROUP BY network, tx_hash_norm
491
+ HAVING COUNT(*) > 1
468
492
  LIMIT 1`)
469
493
  .get();
470
494
  if (duplicate) {
471
495
  throw new Error(`Cannot migrate SQLite funding transaction uniqueness: ${duplicate.count} records use ${duplicate.network}:${duplicate.tx_hash_norm}`);
472
496
  }
473
- this.db.exec(`
474
- DROP INDEX IF EXISTS resvary_funding_transactions_tx_hash;
475
- CREATE UNIQUE INDEX resvary_funding_transactions_tx_hash
476
- ON resvary_funding_transactions(network, tx_hash_norm)
477
- WHERE tx_hash_norm IS NOT NULL;
497
+ this.db.exec(`
498
+ DROP INDEX IF EXISTS resvary_funding_transactions_tx_hash;
499
+ CREATE UNIQUE INDEX resvary_funding_transactions_tx_hash
500
+ ON resvary_funding_transactions(network, tx_hash_norm)
501
+ WHERE tx_hash_norm IS NOT NULL;
478
502
  `);
479
503
  this.db
480
504
  .prepare('INSERT INTO resvary_schema_migrations(version, applied_at) VALUES (4, ?)')
@@ -486,6 +510,176 @@ export class SqliteCreditStore {
486
510
  throw error;
487
511
  }
488
512
  }
513
+ migrateCreditLotsV5() {
514
+ const row = this.db
515
+ .prepare('SELECT MAX(version) AS version FROM resvary_schema_migrations')
516
+ .get();
517
+ if ((row?.version ?? 0) >= 5)
518
+ return;
519
+ this.db.exec('BEGIN IMMEDIATE;');
520
+ try {
521
+ this.db.exec(`
522
+ CREATE TABLE resvary_grant_policies (
523
+ id TEXT PRIMARY KEY,
524
+ project_id TEXT NOT NULL,
525
+ policy_key TEXT NOT NULL,
526
+ version INTEGER NOT NULL,
527
+ created_at INTEGER NOT NULL,
528
+ payload TEXT NOT NULL,
529
+ UNIQUE(project_id, policy_key, version)
530
+ );
531
+ CREATE INDEX resvary_grant_policies_project
532
+ ON resvary_grant_policies(project_id, created_at);
533
+
534
+ CREATE TABLE resvary_credit_lots (
535
+ id TEXT PRIMARY KEY,
536
+ account_id TEXT NOT NULL,
537
+ project_id TEXT NOT NULL,
538
+ customer_id TEXT NOT NULL,
539
+ kind TEXT NOT NULL,
540
+ policy_id TEXT,
541
+ expires_at INTEGER,
542
+ created_at INTEGER NOT NULL,
543
+ payload TEXT NOT NULL
544
+ );
545
+ CREATE INDEX resvary_credit_lots_account
546
+ ON resvary_credit_lots(account_id, created_at);
547
+ CREATE INDEX resvary_credit_lots_expiry
548
+ ON resvary_credit_lots(project_id, expires_at)
549
+ WHERE expires_at IS NOT NULL;
550
+
551
+ CREATE TABLE resvary_credit_lot_allocations (
552
+ id TEXT PRIMARY KEY,
553
+ reservation_id TEXT NOT NULL,
554
+ lot_id TEXT NOT NULL,
555
+ account_id TEXT NOT NULL,
556
+ created_at INTEGER NOT NULL,
557
+ payload TEXT NOT NULL,
558
+ UNIQUE(reservation_id, lot_id)
559
+ );
560
+ CREATE INDEX resvary_credit_lot_allocations_reservation
561
+ ON resvary_credit_lot_allocations(reservation_id, created_at);
562
+
563
+ CREATE TABLE resvary_grant_policy_applications (
564
+ id TEXT PRIMARY KEY,
565
+ policy_id TEXT NOT NULL,
566
+ account_id TEXT NOT NULL,
567
+ project_id TEXT NOT NULL,
568
+ customer_id TEXT NOT NULL,
569
+ policy_type TEXT NOT NULL,
570
+ period_key TEXT NOT NULL,
571
+ created_at INTEGER NOT NULL,
572
+ payload TEXT NOT NULL,
573
+ UNIQUE(policy_id, account_id, period_key)
574
+ );
575
+ CREATE INDEX resvary_grant_policy_applications_customer
576
+ ON resvary_grant_policy_applications(project_id, customer_id, created_at);
577
+ `);
578
+ const accounts = this.db
579
+ .prepare('SELECT payload FROM resvary_credit_accounts ORDER BY id')
580
+ .all();
581
+ for (const accountRow of accounts) {
582
+ const account = parseReceiptStoreValue(accountRow.payload);
583
+ const posted = parseCreditUnits(account.postedUnits);
584
+ const reserved = parseCreditUnits(account.reservedUnits);
585
+ if (reserved > posted) {
586
+ throw new Error(`Cannot migrate SQLite credit lots: account invariant failed for ${account.id}`);
587
+ }
588
+ const reservations = this.db
589
+ .prepare(`SELECT payload FROM resvary_credit_reservations
590
+ WHERE account_id = ? AND status = 'open' ORDER BY created_at, id`)
591
+ .all(account.id);
592
+ const open = reservations.map((item) => parseReceiptStoreValue(item.payload));
593
+ const reservationTotal = open.reduce((total, reservation) => total + parseCreditUnits(reservation.reservedUnits), 0n);
594
+ if (reservationTotal !== reserved) {
595
+ throw new Error(`Cannot migrate SQLite credit lots: open reservations do not match account ${account.id}`);
596
+ }
597
+ if (posted === 0n)
598
+ continue;
599
+ const lotId = `lot_legacy_${account.id}`;
600
+ const available = posted - reserved;
601
+ const lot = {
602
+ id: lotId,
603
+ accountId: account.id,
604
+ projectId: account.projectId,
605
+ customerId: account.customerId,
606
+ kind: 'legacy',
607
+ originalAmount: creditUnitsToString(posted),
608
+ originalUnits: posted.toString(),
609
+ availableAmount: creditUnitsToString(available),
610
+ availableUnits: available.toString(),
611
+ reservedAmount: creditUnitsToString(reserved),
612
+ reservedUnits: reserved.toString(),
613
+ consumedAmount: '0',
614
+ consumedUnits: '0',
615
+ expiredAmount: '0',
616
+ expiredUnits: '0',
617
+ createdAt: account.createdAt,
618
+ updatedAt: account.updatedAt,
619
+ metadata: { migratedFrom: 'sqlite-v4' },
620
+ };
621
+ insert(this.db, 'resvary_credit_lots', [
622
+ 'id',
623
+ 'account_id',
624
+ 'project_id',
625
+ 'customer_id',
626
+ 'kind',
627
+ 'policy_id',
628
+ 'expires_at',
629
+ 'created_at',
630
+ ], [
631
+ lot.id,
632
+ lot.accountId,
633
+ lot.projectId,
634
+ lot.customerId,
635
+ lot.kind,
636
+ null,
637
+ null,
638
+ lot.createdAt,
639
+ ], lot);
640
+ for (const reservation of open) {
641
+ const units = parseCreditUnits(reservation.reservedUnits);
642
+ if (units === 0n)
643
+ continue;
644
+ const allocation = {
645
+ id: `cla_legacy_${reservation.id}`,
646
+ reservationId: reservation.id,
647
+ lotId,
648
+ accountId: account.id,
649
+ projectId: account.projectId,
650
+ customerId: account.customerId,
651
+ allocatedAmount: creditUnitsToString(units),
652
+ allocatedUnits: units.toString(),
653
+ reservedAmount: creditUnitsToString(units),
654
+ reservedUnits: units.toString(),
655
+ consumedAmount: '0',
656
+ consumedUnits: '0',
657
+ releasedAmount: '0',
658
+ releasedUnits: '0',
659
+ expiredAmount: '0',
660
+ expiredUnits: '0',
661
+ createdAt: reservation.createdAt,
662
+ updatedAt: account.updatedAt,
663
+ };
664
+ insert(this.db, 'resvary_credit_lot_allocations', ['id', 'reservation_id', 'lot_id', 'account_id', 'created_at'], [
665
+ allocation.id,
666
+ allocation.reservationId,
667
+ allocation.lotId,
668
+ allocation.accountId,
669
+ allocation.createdAt,
670
+ ], allocation);
671
+ }
672
+ }
673
+ this.db
674
+ .prepare('INSERT INTO resvary_schema_migrations(version, applied_at) VALUES (5, ?)')
675
+ .run(Date.now());
676
+ this.db.exec('COMMIT;');
677
+ }
678
+ catch (error) {
679
+ this.db.exec('ROLLBACK;');
680
+ throw error;
681
+ }
682
+ }
489
683
  }
490
684
  class SqliteCreditTransaction {
491
685
  db;
@@ -564,6 +758,30 @@ class SqliteCreditTransaction {
564
758
  listFundingTransactions(fundingIntentId) {
565
759
  return reader(this.db).listFundingTransactions(fundingIntentId);
566
760
  }
761
+ getGrantPolicy(id) {
762
+ return reader(this.db).getGrantPolicy(id);
763
+ }
764
+ listGrantPolicies(projectId) {
765
+ return reader(this.db).listGrantPolicies(projectId);
766
+ }
767
+ getCreditLot(id) {
768
+ return reader(this.db).getCreditLot(id);
769
+ }
770
+ listCreditLots(filter) {
771
+ return reader(this.db).listCreditLots(filter);
772
+ }
773
+ listCreditLotAllocations(reservationId) {
774
+ return reader(this.db).listCreditLotAllocations(reservationId);
775
+ }
776
+ getGrantPolicyApplication(id) {
777
+ return reader(this.db).getGrantPolicyApplication(id);
778
+ }
779
+ getGrantPolicyApplicationByIdentity(policyId, accountId, periodKey) {
780
+ return reader(this.db).getGrantPolicyApplicationByIdentity(policyId, accountId, periodKey);
781
+ }
782
+ listGrantPolicyApplications(filter) {
783
+ return reader(this.db).listGrantPolicyApplications(filter);
784
+ }
567
785
  async saveAccount(value) {
568
786
  upsert(this.db, 'resvary_credit_accounts', ['id', 'project_id', 'customer_id', 'updated_at'], [value.id, value.projectId, value.customerId, value.updatedAt], value);
569
787
  }
@@ -644,6 +862,54 @@ class SqliteCreditTransaction {
644
862
  value.createdAt,
645
863
  ], value);
646
864
  }
865
+ async saveGrantPolicy(value) {
866
+ insert(this.db, 'resvary_grant_policies', ['id', 'project_id', 'policy_key', 'version', 'created_at'], [value.id, value.projectId, value.key, value.version, value.createdAt], value);
867
+ }
868
+ async saveCreditLot(value) {
869
+ upsert(this.db, 'resvary_credit_lots', [
870
+ 'id',
871
+ 'account_id',
872
+ 'project_id',
873
+ 'customer_id',
874
+ 'kind',
875
+ 'policy_id',
876
+ 'expires_at',
877
+ 'created_at',
878
+ ], [
879
+ value.id,
880
+ value.accountId,
881
+ value.projectId,
882
+ value.customerId,
883
+ value.kind,
884
+ value.policyId ?? null,
885
+ value.expiresAt ?? null,
886
+ value.createdAt,
887
+ ], value);
888
+ }
889
+ async saveCreditLotAllocation(value) {
890
+ upsert(this.db, 'resvary_credit_lot_allocations', ['id', 'reservation_id', 'lot_id', 'account_id', 'created_at'], [value.id, value.reservationId, value.lotId, value.accountId, value.createdAt], value);
891
+ }
892
+ async saveGrantPolicyApplication(value) {
893
+ insert(this.db, 'resvary_grant_policy_applications', [
894
+ 'id',
895
+ 'policy_id',
896
+ 'account_id',
897
+ 'project_id',
898
+ 'customer_id',
899
+ 'policy_type',
900
+ 'period_key',
901
+ 'created_at',
902
+ ], [
903
+ value.id,
904
+ value.policyId,
905
+ value.accountId,
906
+ value.projectId,
907
+ value.customerId,
908
+ value.policyType,
909
+ value.periodKey,
910
+ value.createdAt,
911
+ ], value);
912
+ }
647
913
  }
648
914
  function reader(db) {
649
915
  return {
@@ -729,7 +995,7 @@ function reader(db) {
729
995
  return one(db, 'SELECT payload FROM resvary_funding_transactions WHERE id = ?', [id]);
730
996
  },
731
997
  async getFundingTransactionByExternalPayment(rail, network, externalPaymentId) {
732
- return one(db, `SELECT payload FROM resvary_funding_transactions
998
+ return one(db, `SELECT payload FROM resvary_funding_transactions
733
999
  WHERE rail = ? AND network = ? AND external_payment_id_norm = ?`, [rail, network, externalPaymentId.toLowerCase()]);
734
1000
  },
735
1001
  async getFundingTransactionByTxHash(network, txHash) {
@@ -740,6 +1006,44 @@ function reader(db) {
740
1006
  ? all(db, 'SELECT payload FROM resvary_funding_transactions WHERE funding_intent_id = ? ORDER BY created_at ASC', [fundingIntentId])
741
1007
  : all(db, 'SELECT payload FROM resvary_funding_transactions ORDER BY created_at ASC');
742
1008
  },
1009
+ async getGrantPolicy(id) {
1010
+ return one(db, 'SELECT payload FROM resvary_grant_policies WHERE id = ?', [
1011
+ id,
1012
+ ]);
1013
+ },
1014
+ async listGrantPolicies(projectId) {
1015
+ return projectId
1016
+ ? all(db, 'SELECT payload FROM resvary_grant_policies WHERE project_id = ? ORDER BY created_at, version', [projectId])
1017
+ : all(db, 'SELECT payload FROM resvary_grant_policies ORDER BY created_at, version');
1018
+ },
1019
+ async getCreditLot(id) {
1020
+ return one(db, 'SELECT payload FROM resvary_credit_lots WHERE id = ?', [id]);
1021
+ },
1022
+ async listCreditLots(filter = {}) {
1023
+ return all(db, 'SELECT payload FROM resvary_credit_lots ORDER BY created_at, id').filter((item) => matchesBalanceFilter(item, filter) &&
1024
+ (!filter.policyId || item.policyId === filter.policyId) &&
1025
+ (!filter.kind || item.kind === filter.kind) &&
1026
+ (filter.expiresBefore === undefined ||
1027
+ (item.expiresAt !== undefined && item.expiresAt <= filter.expiresBefore)));
1028
+ },
1029
+ async listCreditLotAllocations(reservationId) {
1030
+ return reservationId
1031
+ ? all(db, 'SELECT payload FROM resvary_credit_lot_allocations WHERE reservation_id = ? ORDER BY created_at, id', [reservationId])
1032
+ : all(db, 'SELECT payload FROM resvary_credit_lot_allocations ORDER BY created_at, id');
1033
+ },
1034
+ async getGrantPolicyApplication(id) {
1035
+ return one(db, 'SELECT payload FROM resvary_grant_policy_applications WHERE id = ?', [id]);
1036
+ },
1037
+ async getGrantPolicyApplicationByIdentity(policyId, accountId, periodKey) {
1038
+ return one(db, `SELECT payload FROM resvary_grant_policy_applications
1039
+ WHERE policy_id = ? AND account_id = ? AND period_key = ?`, [policyId, accountId, periodKey]);
1040
+ },
1041
+ async listGrantPolicyApplications(filter = {}) {
1042
+ return all(db, 'SELECT payload FROM resvary_grant_policy_applications ORDER BY created_at, id').filter((item) => matchesBalanceFilter(item, filter) &&
1043
+ (!filter.policyId || item.policyId === filter.policyId) &&
1044
+ (!filter.policyType || item.policyType === filter.policyType) &&
1045
+ (!filter.periodKey || item.periodKey === filter.periodKey));
1046
+ },
743
1047
  };
744
1048
  }
745
1049
  function insert(db, table, columns, values, payload, conflictColumns = ['id']) {