@fragno-dev/db 0.2.0 → 0.2.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.
Files changed (32) hide show
  1. package/.turbo/turbo-build.log +20 -20
  2. package/CHANGELOG.md +17 -0
  3. package/dist/db-fragment-definition-builder.d.ts +55 -1
  4. package/dist/db-fragment-definition-builder.d.ts.map +1 -1
  5. package/dist/db-fragment-definition-builder.js +49 -5
  6. package/dist/db-fragment-definition-builder.js.map +1 -1
  7. package/dist/fragments/internal-fragment.d.ts.map +1 -1
  8. package/dist/fragments/internal-fragment.js.map +1 -1
  9. package/dist/mod.d.ts.map +1 -1
  10. package/dist/query/unit-of-work/execute-unit-of-work.d.ts +37 -1
  11. package/dist/query/unit-of-work/execute-unit-of-work.d.ts.map +1 -1
  12. package/dist/query/unit-of-work/execute-unit-of-work.js +133 -1
  13. package/dist/query/unit-of-work/execute-unit-of-work.js.map +1 -1
  14. package/dist/query/unit-of-work/unit-of-work.d.ts +18 -3
  15. package/dist/query/unit-of-work/unit-of-work.d.ts.map +1 -1
  16. package/dist/query/unit-of-work/unit-of-work.js +25 -11
  17. package/dist/query/unit-of-work/unit-of-work.js.map +1 -1
  18. package/dist/schema/create.d.ts +0 -3
  19. package/dist/schema/create.d.ts.map +1 -1
  20. package/dist/schema/create.js +0 -4
  21. package/dist/schema/create.js.map +1 -1
  22. package/dist/sql-driver/dialects/durable-object-dialect.d.ts.map +1 -1
  23. package/package.json +3 -3
  24. package/src/adapters/drizzle/drizzle-adapter-pglite.test.ts +1 -0
  25. package/src/db-fragment-definition-builder.ts +187 -7
  26. package/src/fragments/internal-fragment.ts +0 -1
  27. package/src/hooks/hooks.test.ts +28 -16
  28. package/src/query/unit-of-work/execute-unit-of-work.test.ts +555 -1
  29. package/src/query/unit-of-work/execute-unit-of-work.ts +312 -4
  30. package/src/query/unit-of-work/unit-of-work-coordinator.test.ts +249 -1
  31. package/src/query/unit-of-work/unit-of-work.ts +39 -17
  32. package/src/schema/create.ts +0 -5
@@ -3,11 +3,18 @@ import { schema, idColumn, FragnoId } from "../../schema/create";
3
3
  import {
4
4
  createUnitOfWork,
5
5
  type TypedUnitOfWork,
6
+ type IUnitOfWork,
6
7
  type UOWCompiler,
7
8
  type UOWDecoder,
8
9
  type UOWExecutor,
9
10
  } from "./unit-of-work";
10
- import { executeUnitOfWork, executeRestrictedUnitOfWork } from "./execute-unit-of-work";
11
+ import {
12
+ executeUnitOfWork,
13
+ executeRestrictedUnitOfWork,
14
+ executeTxArray,
15
+ executeTxCallbacks,
16
+ executeServiceTx,
17
+ } from "./execute-unit-of-work";
11
18
  import {
12
19
  ExponentialBackoffRetryPolicy,
13
20
  LinearBackoffRetryPolicy,
@@ -1307,4 +1314,551 @@ describe("executeRestrictedUnitOfWork", () => {
1307
1314
  expect(await deferred.promise).toContain('relation "settings" does not exist');
1308
1315
  });
1309
1316
  });
1317
+
1318
+ describe("executeTxArray", () => {
1319
+ it("should execute multiple service promises and await them before mutations", async () => {
1320
+ const compiler = createMockCompiler();
1321
+ const executor: UOWExecutor<unknown, unknown> = {
1322
+ executeRetrievalPhase: async () => [],
1323
+ executeMutationPhase: async () => ({
1324
+ success: true,
1325
+ createdInternalIds: [],
1326
+ }),
1327
+ };
1328
+ const decoder = createMockDecoder();
1329
+
1330
+ let retrievalExecuted = false;
1331
+ let servicesResolved = false;
1332
+
1333
+ const result = await executeTxArray(
1334
+ () => [
1335
+ Promise.resolve().then(async () => {
1336
+ await new Promise((resolve) => setTimeout(resolve, 10));
1337
+ servicesResolved = true;
1338
+ return { result1: "value1" };
1339
+ }),
1340
+ Promise.resolve().then(async () => {
1341
+ await new Promise((resolve) => setTimeout(resolve, 10));
1342
+ return { result2: "value2" };
1343
+ }),
1344
+ ],
1345
+ {
1346
+ createUnitOfWork: () => {
1347
+ const uow = createUnitOfWork(compiler, executor, decoder);
1348
+ const originalExecuteRetrieve = uow.executeRetrieve.bind(uow);
1349
+ uow.executeRetrieve = async () => {
1350
+ retrievalExecuted = true;
1351
+ return originalExecuteRetrieve();
1352
+ };
1353
+ return uow;
1354
+ },
1355
+ },
1356
+ );
1357
+
1358
+ expect(retrievalExecuted).toBe(true);
1359
+ expect(servicesResolved).toBe(true);
1360
+ expect(result).toEqual([{ result1: "value1" }, { result2: "value2" }]);
1361
+ });
1362
+
1363
+ it("should retry on concurrency conflict", async () => {
1364
+ const compiler = createMockCompiler();
1365
+ let attemptCount = 0;
1366
+ const executor: UOWExecutor<unknown, unknown> = {
1367
+ executeRetrievalPhase: async () => [],
1368
+ executeMutationPhase: async () => {
1369
+ attemptCount++;
1370
+ if (attemptCount < 2) {
1371
+ return { success: false };
1372
+ }
1373
+ return {
1374
+ success: true,
1375
+ createdInternalIds: [],
1376
+ };
1377
+ },
1378
+ };
1379
+ const decoder = createMockDecoder();
1380
+
1381
+ const result = await executeTxArray(() => [Promise.resolve({ result: "value" })], {
1382
+ createUnitOfWork: () => createUnitOfWork(compiler, executor, decoder),
1383
+ retryPolicy: new ExponentialBackoffRetryPolicy({ maxRetries: 3, initialDelayMs: 1 }),
1384
+ });
1385
+
1386
+ expect(attemptCount).toBe(2);
1387
+ expect(result).toEqual([{ result: "value" }]);
1388
+ });
1389
+
1390
+ it("should throw if retries exhausted", async () => {
1391
+ const compiler = createMockCompiler();
1392
+ const executor: UOWExecutor<unknown, unknown> = {
1393
+ executeRetrievalPhase: async () => [],
1394
+ executeMutationPhase: async () => ({ success: false }),
1395
+ };
1396
+ const decoder = createMockDecoder();
1397
+
1398
+ await expect(
1399
+ executeTxArray(() => [Promise.resolve({ result: "value" })], {
1400
+ createUnitOfWork: () => createUnitOfWork(compiler, executor, decoder),
1401
+ retryPolicy: new NoRetryPolicy(),
1402
+ }),
1403
+ ).rejects.toThrow("optimistic concurrency conflict");
1404
+ });
1405
+ });
1406
+
1407
+ describe("executeTxCallbacks", () => {
1408
+ it("should execute retrieve and mutate callbacks in order", async () => {
1409
+ const compiler = createMockCompiler();
1410
+ const executor: UOWExecutor<unknown, unknown> = {
1411
+ executeRetrievalPhase: async () => [],
1412
+ executeMutationPhase: async () => ({
1413
+ success: true,
1414
+ createdInternalIds: [],
1415
+ }),
1416
+ };
1417
+ const decoder = createMockDecoder();
1418
+
1419
+ const executionOrder: string[] = [];
1420
+
1421
+ const result = await executeTxCallbacks(
1422
+ {
1423
+ retrieve: ({ forSchema }) => {
1424
+ executionOrder.push("retrieve");
1425
+ const uow = forSchema(testSchema);
1426
+ uow.find("users", (b) => b.whereIndex("idx_email"));
1427
+ return { servicePromise: Promise.resolve({ value: "result" }) };
1428
+ },
1429
+ mutate: ({ forSchema }, { servicePromise }) => {
1430
+ executionOrder.push("mutate");
1431
+ const uow = forSchema(testSchema);
1432
+ uow.create("users", { email: "test@example.com", name: "Test", balance: 0 });
1433
+ return servicePromise;
1434
+ },
1435
+ },
1436
+ {
1437
+ createUnitOfWork: () => createUnitOfWork(compiler, executor, decoder),
1438
+ },
1439
+ );
1440
+
1441
+ expect(executionOrder).toEqual(["retrieve", "mutate"]);
1442
+ expect(result).toEqual({ value: "result" });
1443
+ });
1444
+
1445
+ it("should handle retrieve-only transactions", async () => {
1446
+ const compiler = createMockCompiler();
1447
+ const executor: UOWExecutor<unknown, unknown> = {
1448
+ executeRetrievalPhase: async () => [
1449
+ [
1450
+ {
1451
+ id: FragnoId.fromExternal("1", 1),
1452
+ email: "test@example.com",
1453
+ name: "Test",
1454
+ balance: 0,
1455
+ },
1456
+ ],
1457
+ ],
1458
+ executeMutationPhase: async () => ({
1459
+ success: true,
1460
+ createdInternalIds: [],
1461
+ }),
1462
+ };
1463
+ const decoder = createMockDecoder();
1464
+
1465
+ const result = await executeTxCallbacks(
1466
+ {
1467
+ retrieve: ({ forSchema }) => {
1468
+ const uow = forSchema(testSchema);
1469
+ uow.find("users", (b) => b.whereIndex("idx_email"));
1470
+ return { users: [] };
1471
+ },
1472
+ },
1473
+ {
1474
+ createUnitOfWork: () => createUnitOfWork(compiler, executor, decoder),
1475
+ },
1476
+ );
1477
+
1478
+ expect(result).toEqual({ users: [] });
1479
+ });
1480
+
1481
+ it("should handle mutate-only transactions", async () => {
1482
+ const compiler = createMockCompiler();
1483
+ const executor: UOWExecutor<unknown, unknown> = {
1484
+ executeRetrievalPhase: async () => [],
1485
+ executeMutationPhase: async () => ({
1486
+ success: true,
1487
+ createdInternalIds: [BigInt(1)],
1488
+ }),
1489
+ };
1490
+ const decoder = createMockDecoder();
1491
+
1492
+ const result = await executeTxCallbacks(
1493
+ {
1494
+ mutate: ({ forSchema }) => {
1495
+ const uow = forSchema(testSchema);
1496
+ uow.create("users", { email: "test@example.com", name: "Test", balance: 0 });
1497
+ return { created: true };
1498
+ },
1499
+ },
1500
+ {
1501
+ createUnitOfWork: () => createUnitOfWork(compiler, executor, decoder),
1502
+ },
1503
+ );
1504
+
1505
+ expect(result).toEqual({ created: true });
1506
+ });
1507
+
1508
+ it("should await promises returned from mutate callback", async () => {
1509
+ const compiler = createMockCompiler();
1510
+ const executor: UOWExecutor<unknown, unknown> = {
1511
+ executeRetrievalPhase: async () => [],
1512
+ executeMutationPhase: async () => ({
1513
+ success: true,
1514
+ createdInternalIds: [],
1515
+ }),
1516
+ };
1517
+ const decoder = createMockDecoder();
1518
+
1519
+ const result = await executeTxCallbacks(
1520
+ {
1521
+ mutate: () => {
1522
+ return Promise.resolve({ value: "async result" });
1523
+ },
1524
+ },
1525
+ {
1526
+ createUnitOfWork: () => createUnitOfWork(compiler, executor, decoder),
1527
+ },
1528
+ );
1529
+
1530
+ expect(result).toEqual({ value: "async result" });
1531
+ });
1532
+
1533
+ it("should retry on concurrency conflict", async () => {
1534
+ const compiler = createMockCompiler();
1535
+ let attemptCount = 0;
1536
+ const executor: UOWExecutor<unknown, unknown> = {
1537
+ executeRetrievalPhase: async () => [],
1538
+ executeMutationPhase: async () => {
1539
+ attemptCount++;
1540
+ if (attemptCount < 2) {
1541
+ return { success: false };
1542
+ }
1543
+ return {
1544
+ success: true,
1545
+ createdInternalIds: [],
1546
+ };
1547
+ },
1548
+ };
1549
+ const decoder = createMockDecoder();
1550
+
1551
+ const result = await executeTxCallbacks(
1552
+ {
1553
+ mutate: () => ({ value: "result" }),
1554
+ },
1555
+ {
1556
+ createUnitOfWork: () => createUnitOfWork(compiler, executor, decoder),
1557
+ retryPolicy: new ExponentialBackoffRetryPolicy({ maxRetries: 3, initialDelayMs: 1 }),
1558
+ },
1559
+ );
1560
+
1561
+ expect(attemptCount).toBe(2);
1562
+ expect(result).toEqual({ value: "result" });
1563
+ });
1564
+ });
1565
+
1566
+ describe("executeServiceTx", () => {
1567
+ it("should execute service transaction with retrieve and mutate", async () => {
1568
+ const compiler = createMockCompiler();
1569
+ const executor: UOWExecutor<unknown, unknown> = {
1570
+ executeRetrievalPhase: async () => [
1571
+ [
1572
+ {
1573
+ id: FragnoId.fromExternal("1", 1),
1574
+ email: "test@example.com",
1575
+ name: "Test",
1576
+ balance: 100,
1577
+ },
1578
+ ],
1579
+ ],
1580
+ executeMutationPhase: async () => ({
1581
+ success: true,
1582
+ createdInternalIds: [],
1583
+ }),
1584
+ };
1585
+ const decoder = createMockDecoder();
1586
+
1587
+ const baseUow = createUnitOfWork(compiler, executor, decoder);
1588
+ const restrictedUow = baseUow.restrict();
1589
+
1590
+ // Start service tx
1591
+ const servicePromise = executeServiceTx(
1592
+ testSchema,
1593
+ {
1594
+ retrieve: (uow) => {
1595
+ return uow.findFirst("users", (b) => b.whereIndex("idx_email"));
1596
+ },
1597
+ mutate: async (uow, [user]) => {
1598
+ if (!user) {
1599
+ return { ok: false };
1600
+ }
1601
+ await new Promise((resolve) => setTimeout(resolve, 10)); // Async work
1602
+ uow.update("users", user.id, (b) => b.set({ balance: user.balance - 10 }));
1603
+ return { ok: true, newBalance: user.balance - 10 };
1604
+ },
1605
+ },
1606
+ restrictedUow,
1607
+ );
1608
+
1609
+ // Simulate handler executing phases concurrently with service
1610
+ // Yield to let service start awaiting retrievalPhase
1611
+ await new Promise((resolve) => setImmediate(resolve));
1612
+
1613
+ // Execute retrieve phase
1614
+ await baseUow.executeRetrieve();
1615
+
1616
+ // Wait for service mutate callback to schedule mutations
1617
+ await new Promise((resolve) => setTimeout(resolve, 20));
1618
+
1619
+ // Execute mutation phase
1620
+ await baseUow.executeMutations();
1621
+
1622
+ // Wait for service to complete
1623
+ const serviceResult = await servicePromise;
1624
+ expect(serviceResult).toEqual({ ok: true, newBalance: 90 });
1625
+ });
1626
+
1627
+ it("should handle async mutate callback", async () => {
1628
+ const compiler = createMockCompiler();
1629
+ const executor: UOWExecutor<unknown, unknown> = {
1630
+ executeRetrievalPhase: async () => [[]],
1631
+ executeMutationPhase: async () => ({
1632
+ success: true,
1633
+ createdInternalIds: [BigInt(1)],
1634
+ }),
1635
+ };
1636
+ const decoder = createMockDecoder();
1637
+
1638
+ const baseUow = createUnitOfWork(compiler, executor, decoder);
1639
+ const restrictedUow = baseUow.restrict();
1640
+
1641
+ // Simulate handler executing phases concurrently with service
1642
+ const handlerSimulation = (async () => {
1643
+ // Yield to let service start
1644
+ await Promise.resolve();
1645
+ // Execute retrieve phase
1646
+ await baseUow.executeRetrieve();
1647
+ // Wait for service mutate callback to schedule mutations
1648
+ await new Promise((resolve) => setTimeout(resolve, 20));
1649
+ // Execute mutation phase
1650
+ await baseUow.executeMutations();
1651
+ })();
1652
+
1653
+ // Start service tx
1654
+ const servicePromise = executeServiceTx(
1655
+ testSchema,
1656
+ {
1657
+ retrieve: (uow) => {
1658
+ return uow.find("users", (b) => b.whereIndex("idx_email"));
1659
+ },
1660
+ mutate: async (uow) => {
1661
+ await new Promise((resolve) => setTimeout(resolve, 10));
1662
+ uow.create("users", { email: "new@example.com", name: "New", balance: 0 });
1663
+ return { created: true };
1664
+ },
1665
+ },
1666
+ restrictedUow,
1667
+ );
1668
+
1669
+ // Wait for both handler and service to complete
1670
+ await Promise.all([handlerSimulation, servicePromise]);
1671
+
1672
+ const serviceResult = await servicePromise;
1673
+ expect(serviceResult).toEqual({ created: true });
1674
+ });
1675
+
1676
+ it("should prevent anti-pattern: service async work completes before mutations execute", async () => {
1677
+ const compiler = createMockCompiler();
1678
+ const executor: UOWExecutor<unknown, unknown> = {
1679
+ executeRetrievalPhase: async () => [[]],
1680
+ executeMutationPhase: async () => ({
1681
+ success: true,
1682
+ createdInternalIds: [BigInt(1)],
1683
+ }),
1684
+ };
1685
+ const decoder = createMockDecoder();
1686
+
1687
+ const baseUow = createUnitOfWork(compiler, executor, decoder);
1688
+ const restrictedUow = baseUow.restrict();
1689
+
1690
+ let asyncWorkCompleted = false;
1691
+ let mutationScheduled = false;
1692
+
1693
+ // Simulate handler executing phases concurrently with service
1694
+ const handlerSimulation = (async () => {
1695
+ // Yield to let service start
1696
+ await Promise.resolve();
1697
+ // Execute retrieve phase
1698
+ await baseUow.executeRetrieve();
1699
+ // Wait for service mutate callback to schedule mutations (including async work)
1700
+ await new Promise((resolve) => setTimeout(resolve, 30));
1701
+ // Execute mutation phase
1702
+ await baseUow.executeMutations();
1703
+ })();
1704
+
1705
+ // Start service tx
1706
+ const servicePromise = executeServiceTx(
1707
+ testSchema,
1708
+ {
1709
+ retrieve: (uow) => {
1710
+ return uow.find("users", (b) => b.whereIndex("idx_email"));
1711
+ },
1712
+ mutate: async (uow) => {
1713
+ // Simulate async work (like hashing backup codes)
1714
+ await new Promise((resolve) => setTimeout(resolve, 20));
1715
+ asyncWorkCompleted = true;
1716
+
1717
+ // Schedule mutation
1718
+ uow.create("users", { email: "test@example.com", name: "Test", balance: 0 });
1719
+ mutationScheduled = true;
1720
+ return { success: true };
1721
+ },
1722
+ },
1723
+ restrictedUow,
1724
+ );
1725
+
1726
+ // Wait for both handler and service to complete
1727
+ await Promise.all([handlerSimulation, servicePromise]);
1728
+
1729
+ expect(asyncWorkCompleted).toBe(true);
1730
+ expect(mutationScheduled).toBe(true);
1731
+ });
1732
+ });
1733
+
1734
+ describe("executeTxArray with executeServiceTx", () => {
1735
+ it("should execute a single service promise created with executeServiceTx", async () => {
1736
+ const compiler = createMockCompiler();
1737
+ const executor: UOWExecutor<unknown, unknown> = {
1738
+ executeRetrievalPhase: async () => [
1739
+ [
1740
+ {
1741
+ id: FragnoId.fromExternal("1", 1),
1742
+ email: "user1@example.com",
1743
+ name: "User 1",
1744
+ balance: 100,
1745
+ },
1746
+ ],
1747
+ ],
1748
+ executeMutationPhase: async () => ({
1749
+ success: true,
1750
+ createdInternalIds: [],
1751
+ }),
1752
+ };
1753
+ const decoder = createMockDecoder();
1754
+
1755
+ let currentUow: IUnitOfWork | null = null;
1756
+
1757
+ // Execute the service promise using executeTxArray
1758
+ const result = await executeTxArray(
1759
+ () => [
1760
+ executeServiceTx(
1761
+ testSchema,
1762
+ {
1763
+ retrieve: (uow) => {
1764
+ return uow.findFirst("users", (b) => b.whereIndex("idx_email"));
1765
+ },
1766
+ mutate: async (uow, [user]) => {
1767
+ if (!user) {
1768
+ return { ok: false };
1769
+ }
1770
+ // simulate async work
1771
+ await new Promise((resolve) => setTimeout(resolve, 10));
1772
+
1773
+ uow.update("users", user.id, (b) => b.set({ balance: user.balance + 50 }));
1774
+ return { ok: true, newBalance: user.balance + 50 };
1775
+ },
1776
+ },
1777
+ currentUow!,
1778
+ ),
1779
+ ],
1780
+ {
1781
+ createUnitOfWork: () => {
1782
+ currentUow = createUnitOfWork(compiler, executor, decoder);
1783
+ return currentUow;
1784
+ },
1785
+ },
1786
+ );
1787
+
1788
+ expect(result).toHaveLength(1);
1789
+ expect(result[0]).toEqual({ ok: true, newBalance: 150 });
1790
+ });
1791
+
1792
+ it("should retry and eventually succeed when mutations fail on first attempts", async () => {
1793
+ const compiler = createMockCompiler();
1794
+ let executionAttemptCount = 0;
1795
+ let factoryCallCount = 0;
1796
+
1797
+ const executor: UOWExecutor<unknown, unknown> = {
1798
+ executeRetrievalPhase: async () => [
1799
+ [
1800
+ {
1801
+ id: FragnoId.fromExternal("1", 1),
1802
+ email: "user1@example.com",
1803
+ name: "User 1",
1804
+ balance: 100,
1805
+ },
1806
+ ],
1807
+ ],
1808
+ executeMutationPhase: async () => {
1809
+ executionAttemptCount++;
1810
+ // Fail on first 2 attempts, succeed on 3rd
1811
+ if (executionAttemptCount < 3) {
1812
+ return { success: false };
1813
+ }
1814
+ return {
1815
+ success: true,
1816
+ createdInternalIds: [],
1817
+ };
1818
+ },
1819
+ };
1820
+ const decoder = createMockDecoder();
1821
+
1822
+ let currentUow: IUnitOfWork | null = null;
1823
+
1824
+ const result = await executeTxArray(
1825
+ () => {
1826
+ factoryCallCount++;
1827
+ return [
1828
+ executeServiceTx(
1829
+ testSchema,
1830
+ {
1831
+ retrieve: (uow) => {
1832
+ return uow.findFirst("users", (b) => b.whereIndex("idx_email"));
1833
+ },
1834
+ mutate: async (uow, [user]) => {
1835
+ if (!user) {
1836
+ return { ok: false };
1837
+ }
1838
+ // simulate async work
1839
+ await new Promise((resolve) => setTimeout(resolve, 10));
1840
+
1841
+ uow.update("users", user.id, (b) => b.set({ balance: user.balance + 50 }));
1842
+ return { ok: true, newBalance: user.balance + 50, attempt: factoryCallCount };
1843
+ },
1844
+ },
1845
+ currentUow!,
1846
+ ),
1847
+ ];
1848
+ },
1849
+ {
1850
+ createUnitOfWork: () => {
1851
+ currentUow = createUnitOfWork(compiler, executor, decoder);
1852
+ return currentUow;
1853
+ },
1854
+ retryPolicy: new ExponentialBackoffRetryPolicy({ maxRetries: 3, initialDelayMs: 1 }),
1855
+ },
1856
+ );
1857
+
1858
+ expect(result).toHaveLength(1);
1859
+ expect(result[0]).toEqual({ ok: true, newBalance: 150, attempt: 3 });
1860
+ expect(factoryCallCount).toBe(3); // Factory called 3 times (once per attempt)
1861
+ expect(executionAttemptCount).toBe(3); // 3 execution attempts total
1862
+ });
1863
+ });
1310
1864
  });