@fragno-dev/db 0.2.1 → 0.3.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/.turbo/turbo-build.log +206 -140
- package/CHANGELOG.md +67 -0
- package/README.md +30 -9
- package/dist/adapters/adapters.d.ts +23 -21
- package/dist/adapters/adapters.d.ts.map +1 -1
- package/dist/adapters/adapters.js.map +1 -1
- package/dist/adapters/generic-sql/driver-config.d.ts +16 -1
- package/dist/adapters/generic-sql/driver-config.d.ts.map +1 -1
- package/dist/adapters/generic-sql/driver-config.js +23 -1
- package/dist/adapters/generic-sql/driver-config.js.map +1 -1
- package/dist/adapters/generic-sql/generic-sql-adapter.d.ts +27 -9
- package/dist/adapters/generic-sql/generic-sql-adapter.d.ts.map +1 -1
- package/dist/adapters/generic-sql/generic-sql-adapter.js +55 -16
- package/dist/adapters/generic-sql/generic-sql-adapter.js.map +1 -1
- package/dist/adapters/generic-sql/generic-sql-uow-executor.js +129 -3
- package/dist/adapters/generic-sql/generic-sql-uow-executor.js.map +1 -1
- package/dist/adapters/generic-sql/migration/dialect/mysql.js +24 -5
- package/dist/adapters/generic-sql/migration/dialect/mysql.js.map +1 -1
- package/dist/adapters/generic-sql/migration/dialect/postgres.js +6 -5
- package/dist/adapters/generic-sql/migration/dialect/postgres.js.map +1 -1
- package/dist/adapters/generic-sql/migration/dialect/sqlite.js +21 -10
- package/dist/adapters/generic-sql/migration/dialect/sqlite.js.map +1 -1
- package/dist/adapters/generic-sql/migration/prepared-migrations.d.ts.map +1 -1
- package/dist/adapters/generic-sql/migration/prepared-migrations.js +8 -8
- package/dist/adapters/generic-sql/migration/prepared-migrations.js.map +1 -1
- package/dist/adapters/generic-sql/migration/sql-generator.js +74 -51
- package/dist/adapters/generic-sql/migration/sql-generator.js.map +1 -1
- package/dist/adapters/generic-sql/query/create-sql-query-compiler.js +6 -5
- package/dist/adapters/generic-sql/query/create-sql-query-compiler.js.map +1 -1
- package/dist/adapters/generic-sql/query/cursor-utils.js +42 -4
- package/dist/adapters/generic-sql/query/cursor-utils.js.map +1 -1
- package/dist/adapters/generic-sql/query/generic-sql-uow-operation-compiler.js +25 -17
- package/dist/adapters/generic-sql/query/generic-sql-uow-operation-compiler.js.map +1 -1
- package/dist/adapters/generic-sql/query/select-builder.js +5 -3
- package/dist/adapters/generic-sql/query/select-builder.js.map +1 -1
- package/dist/adapters/generic-sql/query/sql-query-compiler.js +15 -12
- package/dist/adapters/generic-sql/query/sql-query-compiler.js.map +1 -1
- package/dist/adapters/generic-sql/query/where-builder.js +38 -28
- package/dist/adapters/generic-sql/query/where-builder.js.map +1 -1
- package/dist/adapters/generic-sql/sqlite-storage.d.ts +13 -0
- package/dist/adapters/generic-sql/sqlite-storage.d.ts.map +1 -0
- package/dist/adapters/generic-sql/sqlite-storage.js +15 -0
- package/dist/adapters/generic-sql/sqlite-storage.js.map +1 -0
- package/dist/adapters/generic-sql/uow-decoder.js +7 -3
- package/dist/adapters/generic-sql/uow-decoder.js.map +1 -1
- package/dist/adapters/generic-sql/uow-encoder.js +28 -8
- package/dist/adapters/generic-sql/uow-encoder.js.map +1 -1
- package/dist/adapters/in-memory/condition-evaluator.js +131 -0
- package/dist/adapters/in-memory/condition-evaluator.js.map +1 -0
- package/dist/adapters/in-memory/errors.d.ts +13 -0
- package/dist/adapters/in-memory/errors.d.ts.map +1 -0
- package/dist/adapters/in-memory/errors.js +23 -0
- package/dist/adapters/in-memory/errors.js.map +1 -0
- package/dist/adapters/in-memory/in-memory-adapter.d.ts +27 -0
- package/dist/adapters/in-memory/in-memory-adapter.d.ts.map +1 -0
- package/dist/adapters/in-memory/in-memory-adapter.js +176 -0
- package/dist/adapters/in-memory/in-memory-adapter.js.map +1 -0
- package/dist/adapters/in-memory/in-memory-uow.js +648 -0
- package/dist/adapters/in-memory/in-memory-uow.js.map +1 -0
- package/dist/adapters/in-memory/index.d.ts +4 -0
- package/dist/adapters/in-memory/index.js +4 -0
- package/dist/adapters/in-memory/options.d.ts +28 -0
- package/dist/adapters/in-memory/options.d.ts.map +1 -0
- package/dist/adapters/in-memory/options.js +61 -0
- package/dist/adapters/in-memory/options.js.map +1 -0
- package/dist/adapters/in-memory/reference-resolution.js +26 -0
- package/dist/adapters/in-memory/reference-resolution.js.map +1 -0
- package/dist/adapters/in-memory/sorted-array-index.js +129 -0
- package/dist/adapters/in-memory/sorted-array-index.js.map +1 -0
- package/dist/adapters/in-memory/store.js +71 -0
- package/dist/adapters/in-memory/store.js.map +1 -0
- package/dist/adapters/in-memory/value-comparison.js +28 -0
- package/dist/adapters/in-memory/value-comparison.js.map +1 -0
- package/dist/adapters/shared/from-unit-of-work-compiler.js.map +1 -1
- package/dist/adapters/shared/uow-operation-compiler.js +11 -11
- package/dist/adapters/shared/uow-operation-compiler.js.map +1 -1
- package/dist/adapters/sql/index.d.ts +5 -0
- package/dist/adapters/sql/index.js +4 -0
- package/dist/db-fragment-definition-builder.d.ts +45 -96
- package/dist/db-fragment-definition-builder.d.ts.map +1 -1
- package/dist/db-fragment-definition-builder.js +121 -99
- package/dist/db-fragment-definition-builder.js.map +1 -1
- package/dist/dispatchers/cloudflare-do/index.d.ts +26 -0
- package/dist/dispatchers/cloudflare-do/index.d.ts.map +1 -0
- package/dist/dispatchers/cloudflare-do/index.js +63 -0
- package/dist/dispatchers/cloudflare-do/index.js.map +1 -0
- package/dist/dispatchers/node/index.d.ts +17 -0
- package/dist/dispatchers/node/index.d.ts.map +1 -0
- package/dist/dispatchers/node/index.js +59 -0
- package/dist/dispatchers/node/index.js.map +1 -0
- package/dist/fragments/internal-fragment.d.ts +172 -9
- package/dist/fragments/internal-fragment.d.ts.map +1 -1
- package/dist/fragments/internal-fragment.js +193 -74
- package/dist/fragments/internal-fragment.js.map +1 -1
- package/dist/fragments/internal-fragment.routes.js +29 -0
- package/dist/fragments/internal-fragment.routes.js.map +1 -0
- package/dist/fragments/internal-fragment.schema.d.ts +9 -0
- package/dist/fragments/internal-fragment.schema.d.ts.map +1 -0
- package/dist/fragments/internal-fragment.schema.js +22 -0
- package/dist/fragments/internal-fragment.schema.js.map +1 -0
- package/dist/hooks/durable-hooks-processor.d.ts +14 -0
- package/dist/hooks/durable-hooks-processor.d.ts.map +1 -0
- package/dist/hooks/durable-hooks-processor.js +32 -0
- package/dist/hooks/durable-hooks-processor.js.map +1 -0
- package/dist/hooks/hooks.d.ts +47 -4
- package/dist/hooks/hooks.d.ts.map +1 -1
- package/dist/hooks/hooks.js +106 -39
- package/dist/hooks/hooks.js.map +1 -1
- package/dist/migration-engine/auto-from-schema.js +14 -11
- package/dist/migration-engine/auto-from-schema.js.map +1 -1
- package/dist/migration-engine/generation-engine.d.ts +16 -10
- package/dist/migration-engine/generation-engine.d.ts.map +1 -1
- package/dist/migration-engine/generation-engine.js +72 -33
- package/dist/migration-engine/generation-engine.js.map +1 -1
- package/dist/migration-engine/shared.js.map +1 -1
- package/dist/mod.d.ts +17 -10
- package/dist/mod.d.ts.map +1 -1
- package/dist/mod.js +14 -8
- package/dist/mod.js.map +1 -1
- package/dist/naming/sql-naming.d.ts +19 -0
- package/dist/naming/sql-naming.d.ts.map +1 -0
- package/dist/naming/sql-naming.js +116 -0
- package/dist/naming/sql-naming.js.map +1 -0
- package/dist/node_modules/.pnpm/{rou3@0.7.10 → rou3@0.7.12}/node_modules/rou3/dist/index.js +8 -5
- package/dist/node_modules/.pnpm/rou3@0.7.12/node_modules/rou3/dist/index.js.map +1 -0
- package/dist/outbox/outbox-builder.js +156 -0
- package/dist/outbox/outbox-builder.js.map +1 -0
- package/dist/outbox/outbox.d.ts +52 -0
- package/dist/outbox/outbox.d.ts.map +1 -0
- package/dist/outbox/outbox.js +37 -0
- package/dist/outbox/outbox.js.map +1 -0
- package/dist/packages/fragno/dist/api/fragment-definition-builder.js +3 -2
- package/dist/packages/fragno/dist/api/fragment-definition-builder.js.map +1 -1
- package/dist/packages/fragno/dist/api/fragment-instantiator.js +164 -20
- package/dist/packages/fragno/dist/api/fragment-instantiator.js.map +1 -1
- package/dist/packages/fragno/dist/api/request-input-context.js +67 -0
- package/dist/packages/fragno/dist/api/request-input-context.js.map +1 -1
- package/dist/packages/fragno/dist/api/route.js +14 -1
- package/dist/packages/fragno/dist/api/route.js.map +1 -1
- package/dist/packages/fragno/dist/internal/trace-context.js +12 -0
- package/dist/packages/fragno/dist/internal/trace-context.js.map +1 -0
- package/dist/query/column-defaults.js +20 -4
- package/dist/query/column-defaults.js.map +1 -1
- package/dist/query/cursor.d.ts +3 -1
- package/dist/query/cursor.d.ts.map +1 -1
- package/dist/query/cursor.js +45 -14
- package/dist/query/cursor.js.map +1 -1
- package/dist/query/db-now.d.ts +8 -0
- package/dist/query/db-now.d.ts.map +1 -0
- package/dist/query/db-now.js +7 -0
- package/dist/query/db-now.js.map +1 -0
- package/dist/query/serialize/create-sql-serializer.js +3 -2
- package/dist/query/serialize/create-sql-serializer.js.map +1 -1
- package/dist/query/serialize/dialect/mysql-serializer.js +12 -6
- package/dist/query/serialize/dialect/mysql-serializer.js.map +1 -1
- package/dist/query/serialize/dialect/postgres-serializer.js +25 -7
- package/dist/query/serialize/dialect/postgres-serializer.js.map +1 -1
- package/dist/query/serialize/dialect/sqlite-serializer.js +55 -11
- package/dist/query/serialize/dialect/sqlite-serializer.js.map +1 -1
- package/dist/query/serialize/sql-serializer.js +2 -2
- package/dist/query/serialize/sql-serializer.js.map +1 -1
- package/dist/query/simple-query-interface.d.ts +6 -1
- package/dist/query/simple-query-interface.d.ts.map +1 -1
- package/dist/query/unit-of-work/execute-unit-of-work.d.ts +351 -100
- package/dist/query/unit-of-work/execute-unit-of-work.d.ts.map +1 -1
- package/dist/query/unit-of-work/execute-unit-of-work.js +440 -267
- package/dist/query/unit-of-work/execute-unit-of-work.js.map +1 -1
- package/dist/query/unit-of-work/unit-of-work.d.ts +67 -22
- package/dist/query/unit-of-work/unit-of-work.d.ts.map +1 -1
- package/dist/query/unit-of-work/unit-of-work.js +110 -13
- package/dist/query/unit-of-work/unit-of-work.js.map +1 -1
- package/dist/query/value-decoding.js +8 -5
- package/dist/query/value-decoding.js.map +1 -1
- package/dist/query/value-encoding.js +29 -9
- package/dist/query/value-encoding.js.map +1 -1
- package/dist/schema/create.d.ts +40 -14
- package/dist/schema/create.d.ts.map +1 -1
- package/dist/schema/create.js +82 -42
- package/dist/schema/create.js.map +1 -1
- package/dist/schema/generate-id.d.ts +20 -0
- package/dist/schema/generate-id.d.ts.map +1 -0
- package/dist/schema/generate-id.js +28 -0
- package/dist/schema/generate-id.js.map +1 -0
- package/dist/schema/type-conversion/create-sql-type-mapper.js +3 -2
- package/dist/schema/type-conversion/create-sql-type-mapper.js.map +1 -1
- package/dist/schema/type-conversion/dialect/sqlite.js +9 -0
- package/dist/schema/type-conversion/dialect/sqlite.js.map +1 -1
- package/dist/schema/validator.d.ts +10 -0
- package/dist/schema/validator.d.ts.map +1 -0
- package/dist/schema/validator.js +123 -0
- package/dist/schema/validator.js.map +1 -0
- package/dist/schema-output/drizzle.d.ts +30 -0
- package/dist/schema-output/drizzle.d.ts.map +1 -0
- package/dist/{adapters/drizzle/generate.js → schema-output/drizzle.js} +82 -56
- package/dist/schema-output/drizzle.js.map +1 -0
- package/dist/schema-output/prisma.d.ts +17 -0
- package/dist/schema-output/prisma.d.ts.map +1 -0
- package/dist/schema-output/prisma.js +296 -0
- package/dist/schema-output/prisma.js.map +1 -0
- package/dist/util/default-database-adapter.js +61 -0
- package/dist/util/default-database-adapter.js.map +1 -0
- package/dist/with-database.d.ts +1 -1
- package/dist/with-database.d.ts.map +1 -1
- package/dist/with-database.js +12 -3
- package/dist/with-database.js.map +1 -1
- package/package.json +43 -28
- package/src/adapters/adapters.ts +30 -24
- package/src/adapters/drizzle/migrate-drizzle.test.ts +54 -33
- package/src/adapters/drizzle/migration-parity-drizzle-kit.test.ts +599 -0
- package/src/adapters/drizzle/test-utils.ts +12 -8
- package/src/adapters/generic-sql/driver-config.ts +38 -0
- package/src/adapters/generic-sql/generic-sql-adapter.test.ts +5 -5
- package/src/adapters/generic-sql/generic-sql-adapter.ts +110 -24
- package/src/adapters/generic-sql/generic-sql-uow-executor.test.ts +54 -0
- package/src/adapters/generic-sql/generic-sql-uow-executor.ts +231 -3
- package/src/adapters/generic-sql/migration/adapter-migration-parity.test.ts +118 -0
- package/src/adapters/generic-sql/migration/dialect/mysql.test.ts +26 -8
- package/src/adapters/generic-sql/migration/dialect/mysql.ts +46 -8
- package/src/adapters/generic-sql/migration/dialect/postgres.test.ts +25 -7
- package/src/adapters/generic-sql/migration/dialect/postgres.ts +8 -4
- package/src/adapters/generic-sql/migration/dialect/sqlite.test.ts +47 -8
- package/src/adapters/generic-sql/migration/dialect/sqlite.ts +27 -12
- package/src/adapters/generic-sql/migration/prepared-migrations.test.ts +128 -39
- package/src/adapters/generic-sql/migration/prepared-migrations.ts +15 -8
- package/src/adapters/generic-sql/migration/sql-generator.ts +142 -65
- package/src/adapters/generic-sql/query/create-sql-query-compiler.ts +9 -6
- package/src/adapters/generic-sql/query/cursor-utils.test.ts +271 -0
- package/src/adapters/generic-sql/query/cursor-utils.ts +41 -6
- package/src/adapters/generic-sql/query/generic-sql-uow-operation-compiler.test.ts +27 -27
- package/src/adapters/generic-sql/query/generic-sql-uow-operation-compiler.ts +38 -24
- package/src/adapters/generic-sql/query/select-builder.test.ts +15 -11
- package/src/adapters/generic-sql/query/select-builder.ts +6 -2
- package/src/adapters/generic-sql/query/sql-query-compiler.test.ts +52 -2
- package/src/adapters/generic-sql/query/sql-query-compiler.ts +50 -15
- package/src/adapters/generic-sql/query/where-builder.test.ts +91 -17
- package/src/adapters/generic-sql/query/where-builder.ts +90 -38
- package/src/adapters/{kysely/kysely-adapter-pglite.test.ts → generic-sql/sql-adapter-pglite-migrations.test.ts} +6 -6
- package/src/adapters/generic-sql/sql-adapter-pglite-pagination.test.ts +806 -0
- package/src/adapters/{drizzle/drizzle-adapter-pglite.test.ts → generic-sql/sql-adapter-pglite-queries.test.ts} +11 -11
- package/src/adapters/generic-sql/{test/generic-drizzle-adapter-sqlite3.test.ts → sql-adapter-sqlite3-driver.test.ts} +49 -35
- package/src/adapters/{drizzle/drizzle-adapter-sqlite3.test.ts → generic-sql/sql-adapter-sqlite3-uow.test.ts} +48 -32
- package/src/adapters/{kysely/kysely-adapter-sqlocal.test.ts → generic-sql/sql-adapter-sqlocal.test.ts} +6 -6
- package/src/adapters/generic-sql/sqlite-storage.ts +20 -0
- package/src/adapters/generic-sql/uow-decoder.test.ts +1 -1
- package/src/adapters/generic-sql/uow-decoder.ts +21 -3
- package/src/adapters/generic-sql/uow-encoder.test.ts +33 -2
- package/src/adapters/generic-sql/uow-encoder.ts +50 -11
- package/src/adapters/in-memory/condition-evaluator.test.ts +193 -0
- package/src/adapters/in-memory/condition-evaluator.ts +275 -0
- package/src/adapters/in-memory/errors.ts +20 -0
- package/src/adapters/in-memory/in-memory-adapter.ts +277 -0
- package/src/adapters/in-memory/in-memory-uow.mutations.test.ts +296 -0
- package/src/adapters/in-memory/in-memory-uow.retrieval.test.ts +100 -0
- package/src/adapters/in-memory/in-memory-uow.ts +1348 -0
- package/src/adapters/in-memory/index.ts +3 -0
- package/src/adapters/in-memory/options.test.ts +41 -0
- package/src/adapters/in-memory/options.ts +87 -0
- package/src/adapters/in-memory/reference-resolution.test.ts +50 -0
- package/src/adapters/in-memory/reference-resolution.ts +67 -0
- package/src/adapters/in-memory/sorted-array-index.test.ts +123 -0
- package/src/adapters/in-memory/sorted-array-index.ts +228 -0
- package/src/adapters/in-memory/store.test.ts +68 -0
- package/src/adapters/in-memory/store.ts +145 -0
- package/src/adapters/in-memory/value-comparison.ts +53 -0
- package/src/adapters/in-memory/value-normalization.test.ts +57 -0
- package/src/adapters/prisma/prisma-adapter-sqlite3.test.ts +1163 -0
- package/src/adapters/shared/from-unit-of-work-compiler.ts +3 -1
- package/src/adapters/shared/uow-operation-compiler.ts +26 -16
- package/src/adapters/sql/index.ts +12 -0
- package/src/db-fragment-definition-builder.test.ts +88 -54
- package/src/db-fragment-definition-builder.ts +201 -322
- package/src/db-fragment-instantiator.test.ts +169 -101
- package/src/db-fragment-integration.test.ts +301 -149
- package/src/dispatchers/cloudflare-do/index.test.ts +73 -0
- package/src/dispatchers/cloudflare-do/index.ts +104 -0
- package/src/dispatchers/node/index.test.ts +91 -0
- package/src/dispatchers/node/index.ts +87 -0
- package/src/fragments/internal-fragment.routes.ts +42 -0
- package/src/fragments/internal-fragment.schema.ts +51 -0
- package/src/fragments/internal-fragment.test.ts +730 -274
- package/src/fragments/internal-fragment.ts +447 -154
- package/src/hooks/durable-hooks-processor.test.ts +117 -0
- package/src/hooks/durable-hooks-processor.ts +67 -0
- package/src/hooks/hooks.test.ts +411 -259
- package/src/hooks/hooks.ts +265 -66
- package/src/migration-engine/auto-from-schema.test.ts +14 -14
- package/src/migration-engine/auto-from-schema.ts +5 -2
- package/src/migration-engine/create.test.ts +2 -2
- package/src/migration-engine/generation-engine.test.ts +229 -104
- package/src/migration-engine/generation-engine.ts +94 -64
- package/src/migration-engine/shared.ts +1 -0
- package/src/mod.ts +78 -30
- package/src/naming/sql-naming.ts +180 -0
- package/src/outbox/outbox-builder.ts +241 -0
- package/src/outbox/outbox.test.ts +253 -0
- package/src/outbox/outbox.ts +137 -0
- package/src/query/column-defaults.ts +41 -3
- package/src/query/condition-builder.test.ts +3 -3
- package/src/query/cursor.test.ts +116 -18
- package/src/query/cursor.ts +75 -26
- package/src/query/db-now.ts +6 -0
- package/src/query/query-type.test.ts +2 -2
- package/src/query/serialize/create-sql-serializer.ts +7 -2
- package/src/query/serialize/dialect/mysql-serializer.ts +12 -4
- package/src/query/serialize/dialect/postgres-serializer.ts +34 -4
- package/src/query/serialize/dialect/sqlite-serializer.test.ts +51 -1
- package/src/query/serialize/dialect/sqlite-serializer.ts +92 -9
- package/src/query/serialize/sql-serializer.ts +4 -4
- package/src/query/simple-query-interface.ts +5 -0
- package/src/query/unit-of-work/execute-unit-of-work.test.ts +1512 -1458
- package/src/query/unit-of-work/execute-unit-of-work.ts +1708 -596
- package/src/query/unit-of-work/tx-builder.test.ts +1041 -0
- package/src/query/unit-of-work/unit-of-work-coordinator.test.ts +32 -32
- package/src/query/unit-of-work/unit-of-work-types.test.ts +1 -1
- package/src/query/unit-of-work/unit-of-work.test.ts +231 -36
- package/src/query/unit-of-work/unit-of-work.ts +229 -31
- package/src/query/value-decoding.test.ts +13 -2
- package/src/query/value-decoding.ts +17 -4
- package/src/query/value-encoding.test.ts +85 -2
- package/src/query/value-encoding.ts +56 -6
- package/src/schema/create.test.ts +129 -42
- package/src/schema/create.ts +187 -47
- package/src/schema/generate-id.test.ts +57 -0
- package/src/schema/generate-id.ts +38 -0
- package/src/schema/serialize.test.ts +14 -2
- package/src/schema/type-conversion/create-sql-type-mapper.ts +7 -2
- package/src/schema/type-conversion/dialect/sqlite.ts +18 -0
- package/src/schema/type-conversion/type-mapping.test.ts +25 -1
- package/src/schema/validator.test.ts +197 -0
- package/src/schema/validator.ts +231 -0
- package/src/{adapters/drizzle/generate.test.ts → schema-output/drizzle.test.ts} +179 -129
- package/src/{adapters/drizzle/generate.ts → schema-output/drizzle.ts} +143 -93
- package/src/schema-output/prisma.test.ts +536 -0
- package/src/schema-output/prisma.ts +573 -0
- package/src/util/default-database-adapter.ts +106 -0
- package/src/with-database.ts +22 -3
- package/tsdown.config.ts +6 -4
- package/dist/adapters/drizzle/drizzle-adapter.d.ts +0 -20
- package/dist/adapters/drizzle/drizzle-adapter.d.ts.map +0 -1
- package/dist/adapters/drizzle/drizzle-adapter.js +0 -27
- package/dist/adapters/drizzle/drizzle-adapter.js.map +0 -1
- package/dist/adapters/drizzle/generate.d.ts +0 -30
- package/dist/adapters/drizzle/generate.d.ts.map +0 -1
- package/dist/adapters/drizzle/generate.js.map +0 -1
- package/dist/adapters/kysely/kysely-adapter.d.ts +0 -19
- package/dist/adapters/kysely/kysely-adapter.d.ts.map +0 -1
- package/dist/adapters/kysely/kysely-adapter.js +0 -17
- package/dist/adapters/kysely/kysely-adapter.js.map +0 -1
- package/dist/adapters/shared/table-name-mapper.d.ts +0 -12
- package/dist/adapters/shared/table-name-mapper.d.ts.map +0 -1
- package/dist/adapters/shared/table-name-mapper.js +0 -43
- package/dist/adapters/shared/table-name-mapper.js.map +0 -1
- package/dist/node_modules/.pnpm/rou3@0.7.10/node_modules/rou3/dist/index.js.map +0 -1
- package/dist/schema-generator/schema-generator.d.ts +0 -15
- package/dist/schema-generator/schema-generator.d.ts.map +0 -1
- package/src/adapters/drizzle/drizzle-adapter.ts +0 -39
- package/src/adapters/kysely/kysely-adapter.ts +0 -27
- package/src/adapters/shared/table-name-mapper.ts +0 -50
- package/src/schema-generator/schema-generator.ts +0 -12
- package/src/shared/config.ts +0 -10
- package/src/shared/connection-pool.ts +0 -24
- package/src/shared/prisma.ts +0 -45
|
@@ -1,29 +1,26 @@
|
|
|
1
|
-
import { describe, it, expect,
|
|
1
|
+
import { describe, it, expect, expectTypeOf } from "vitest";
|
|
2
2
|
import { schema, idColumn, FragnoId } from "../../schema/create";
|
|
3
3
|
import {
|
|
4
4
|
createUnitOfWork,
|
|
5
|
-
type TypedUnitOfWork,
|
|
6
5
|
type IUnitOfWork,
|
|
7
6
|
type UOWCompiler,
|
|
8
7
|
type UOWDecoder,
|
|
9
8
|
type UOWExecutor,
|
|
10
9
|
} from "./unit-of-work";
|
|
11
10
|
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
executeServiceTx,
|
|
11
|
+
createServiceTxBuilder,
|
|
12
|
+
createHandlerTxBuilder,
|
|
13
|
+
isTxResult,
|
|
14
|
+
ConcurrencyConflictError,
|
|
17
15
|
} from "./execute-unit-of-work";
|
|
18
16
|
import {
|
|
19
17
|
ExponentialBackoffRetryPolicy,
|
|
20
18
|
LinearBackoffRetryPolicy,
|
|
21
19
|
NoRetryPolicy,
|
|
22
20
|
} from "./retry-policy";
|
|
23
|
-
import type { AwaitedPromisesInObject } from "./execute-unit-of-work";
|
|
21
|
+
import type { AwaitedPromisesInObject, TxResult } from "./execute-unit-of-work";
|
|
24
22
|
|
|
25
|
-
|
|
26
|
-
const testSchema = schema((s) =>
|
|
23
|
+
const testSchema = schema("test", (s) =>
|
|
27
24
|
s.addTable("users", (t) =>
|
|
28
25
|
t
|
|
29
26
|
.addColumn("id", idColumn())
|
|
@@ -34,7 +31,6 @@ const testSchema = schema((s) =>
|
|
|
34
31
|
),
|
|
35
32
|
);
|
|
36
33
|
|
|
37
|
-
// Type tests for AwaitedPromisesInObject
|
|
38
34
|
describe("AwaitedPromisesInObject type tests", () => {
|
|
39
35
|
it("should unwrap promises in objects", () => {
|
|
40
36
|
type Input = { a: Promise<string>; b: number };
|
|
@@ -132,1329 +128,1155 @@ function createMockDecoder(): UOWDecoder {
|
|
|
132
128
|
};
|
|
133
129
|
}
|
|
134
130
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
131
|
+
describe("Unified Tx API", () => {
|
|
132
|
+
describe("isTxResult", () => {
|
|
133
|
+
it("should return true for TxResult objects", () => {
|
|
134
|
+
const compiler = createMockCompiler();
|
|
135
|
+
const executor: UOWExecutor<unknown, unknown> = {
|
|
136
|
+
executeRetrievalPhase: async () => [],
|
|
137
|
+
executeMutationPhase: async () => ({ success: true, createdInternalIds: [] }),
|
|
138
|
+
};
|
|
139
|
+
const decoder = createMockDecoder();
|
|
140
|
+
const baseUow = createUnitOfWork(compiler, executor, decoder);
|
|
140
141
|
|
|
141
|
-
|
|
142
|
-
|
|
142
|
+
const txResult = createServiceTxBuilder(testSchema, baseUow)
|
|
143
|
+
.retrieve((uow) => uow.find("users", (b) => b.whereIndex("idx_email")))
|
|
144
|
+
.build();
|
|
143
145
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
146
|
+
expect(isTxResult(txResult)).toBe(true);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("should return false for non-TxResult objects", () => {
|
|
150
|
+
expect(isTxResult(null)).toBe(false);
|
|
151
|
+
expect(isTxResult(undefined)).toBe(false);
|
|
152
|
+
expect(isTxResult({})).toBe(false);
|
|
153
|
+
expect(isTxResult({ _internal: {} })).toBe(false);
|
|
154
|
+
expect(isTxResult(Promise.resolve())).toBe(false);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
describe("createServiceTx", () => {
|
|
159
|
+
it("should create a TxResult with retrieve callback", () => {
|
|
160
|
+
const compiler = createMockCompiler();
|
|
161
|
+
const executor: UOWExecutor<unknown, unknown> = {
|
|
162
|
+
executeRetrievalPhase: async () => [
|
|
148
163
|
[
|
|
149
164
|
{
|
|
150
|
-
id: FragnoId.fromExternal("
|
|
165
|
+
id: FragnoId.fromExternal("1", 1),
|
|
151
166
|
email: "test@example.com",
|
|
152
|
-
name: "Test
|
|
167
|
+
name: "Test",
|
|
153
168
|
balance: 100,
|
|
154
169
|
},
|
|
155
170
|
],
|
|
156
|
-
]
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
return { ...result, createdInternalIds: [] };
|
|
162
|
-
},
|
|
163
|
-
};
|
|
164
|
-
|
|
165
|
-
return createUnitOfWork(createMockCompiler(), executor, createMockDecoder()).forSchema(
|
|
166
|
-
testSchema,
|
|
167
|
-
);
|
|
168
|
-
};
|
|
169
|
-
return { factory, callCount };
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
describe("executeUnitOfWork", () => {
|
|
173
|
-
describe("validation", () => {
|
|
174
|
-
it("should throw error when neither retrieve nor mutate is provided", async () => {
|
|
175
|
-
const { factory } = createMockUOWFactory([{ success: true }]);
|
|
176
|
-
|
|
177
|
-
await expect(executeUnitOfWork({}, { createUnitOfWork: factory })).rejects.toThrow(
|
|
178
|
-
"At least one of 'retrieve' or 'mutate' callbacks must be provided",
|
|
179
|
-
);
|
|
180
|
-
});
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
describe("success scenarios", () => {
|
|
184
|
-
it("should succeed on first attempt without retries", async () => {
|
|
185
|
-
const { factory } = createMockUOWFactory([{ success: true }]);
|
|
186
|
-
const onSuccess = vi.fn();
|
|
187
|
-
|
|
188
|
-
const result = await executeUnitOfWork(
|
|
189
|
-
{
|
|
190
|
-
retrieve: (uow) => uow.find("users", (b) => b.whereIndex("primary")),
|
|
191
|
-
mutate: (uow, [users]) => {
|
|
192
|
-
const newBalance = users[0].balance + 100;
|
|
193
|
-
uow.update("users", users[0].id, (b) => b.set({ balance: newBalance }).check());
|
|
194
|
-
return { newBalance };
|
|
195
|
-
},
|
|
196
|
-
onSuccess,
|
|
197
|
-
},
|
|
198
|
-
{ createUnitOfWork: factory },
|
|
199
|
-
);
|
|
200
|
-
|
|
201
|
-
assert(result.success);
|
|
202
|
-
expect(result.mutationResult).toEqual({ newBalance: 200 });
|
|
203
|
-
expect(onSuccess).toHaveBeenCalledExactlyOnceWith({
|
|
204
|
-
results: expect.any(Array),
|
|
205
|
-
mutationResult: { newBalance: 200 },
|
|
206
|
-
createdIds: [],
|
|
207
|
-
nonce: expect.any(String),
|
|
208
|
-
});
|
|
209
|
-
});
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
describe("retry scenarios", () => {
|
|
213
|
-
it("should retry on conflict with eventual success", async () => {
|
|
214
|
-
const { factory, callCount } = createMockUOWFactory([
|
|
215
|
-
{ success: false },
|
|
216
|
-
{ success: false },
|
|
217
|
-
{ success: true },
|
|
218
|
-
]);
|
|
219
|
-
|
|
220
|
-
const result = await executeUnitOfWork(
|
|
221
|
-
{
|
|
222
|
-
retrieve: (uow) => uow.find("users", (b) => b.whereIndex("primary")),
|
|
223
|
-
mutate: async (uow, [users]) => {
|
|
224
|
-
uow.update("users", users[0].id, (b) => b.set({ balance: 200 }).check());
|
|
225
|
-
},
|
|
226
|
-
},
|
|
227
|
-
{
|
|
228
|
-
createUnitOfWork: factory,
|
|
229
|
-
retryPolicy: new ExponentialBackoffRetryPolicy({ maxRetries: 3, initialDelayMs: 1 }),
|
|
230
|
-
},
|
|
231
|
-
);
|
|
232
|
-
|
|
233
|
-
expect(result.success).toBe(true);
|
|
234
|
-
expect(callCount.value).toBe(3); // Initial + 2 retries
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
it("should fail when max retries exceeded", async () => {
|
|
238
|
-
const { factory, callCount } = createMockUOWFactory([
|
|
239
|
-
{ success: false },
|
|
240
|
-
{ success: false },
|
|
241
|
-
{ success: false },
|
|
242
|
-
{ success: false },
|
|
243
|
-
]);
|
|
244
|
-
|
|
245
|
-
const result = await executeUnitOfWork(
|
|
246
|
-
{
|
|
247
|
-
retrieve: (uow) => uow.find("users", (b) => b.whereIndex("primary")),
|
|
248
|
-
mutate: async (uow, [users]) => {
|
|
249
|
-
uow.update("users", users[0].id, (b) => b.set({ balance: 200 }));
|
|
250
|
-
},
|
|
251
|
-
},
|
|
252
|
-
{
|
|
253
|
-
createUnitOfWork: factory,
|
|
254
|
-
retryPolicy: new ExponentialBackoffRetryPolicy({ maxRetries: 2, initialDelayMs: 1 }),
|
|
255
|
-
},
|
|
256
|
-
);
|
|
257
|
-
|
|
258
|
-
assert(!result.success);
|
|
259
|
-
expect(result.reason).toBe("conflict");
|
|
260
|
-
expect(callCount.value).toBe(3); // Initial + 2 retries
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
it("should create fresh UOW on each retry attempt", async () => {
|
|
264
|
-
const { factory, callCount } = createMockUOWFactory([
|
|
265
|
-
{ success: false },
|
|
266
|
-
{ success: false },
|
|
267
|
-
{ success: true },
|
|
268
|
-
]);
|
|
269
|
-
|
|
270
|
-
await executeUnitOfWork(
|
|
271
|
-
{
|
|
272
|
-
retrieve: (uow) => uow.find("users", (b) => b.whereIndex("primary")),
|
|
273
|
-
mutate: async (uow, [users]) => {
|
|
274
|
-
uow.update("users", users[0].id, (b) => b.set({ balance: 200 }));
|
|
275
|
-
},
|
|
276
|
-
},
|
|
277
|
-
{
|
|
278
|
-
createUnitOfWork: factory,
|
|
279
|
-
retryPolicy: new LinearBackoffRetryPolicy({ maxRetries: 3, delayMs: 1 }),
|
|
280
|
-
},
|
|
281
|
-
);
|
|
282
|
-
|
|
283
|
-
expect(callCount.value).toBe(3); // Each attempt creates a new UOW
|
|
284
|
-
});
|
|
285
|
-
});
|
|
286
|
-
|
|
287
|
-
describe("AbortSignal handling", () => {
|
|
288
|
-
it("should abort when signal is aborted before execution", async () => {
|
|
289
|
-
const { factory } = createMockUOWFactory([{ success: false }]);
|
|
290
|
-
const controller = new AbortController();
|
|
291
|
-
controller.abort();
|
|
292
|
-
|
|
293
|
-
const result = await executeUnitOfWork(
|
|
294
|
-
{
|
|
295
|
-
retrieve: (uow) => uow.find("users", (b) => b.whereIndex("primary")),
|
|
296
|
-
mutate: async (uow, [users]) => {
|
|
297
|
-
uow.update("users", users[0].id, (b) => b.set({ balance: 200 }));
|
|
298
|
-
},
|
|
299
|
-
},
|
|
300
|
-
{
|
|
301
|
-
createUnitOfWork: factory,
|
|
302
|
-
retryPolicy: new ExponentialBackoffRetryPolicy({ maxRetries: 5, initialDelayMs: 1 }),
|
|
303
|
-
signal: controller.signal,
|
|
304
|
-
},
|
|
305
|
-
);
|
|
306
|
-
|
|
307
|
-
assert(!result.success);
|
|
308
|
-
expect(result.reason).toBe("aborted");
|
|
309
|
-
});
|
|
310
|
-
|
|
311
|
-
it("should abort when signal is aborted during retry", async () => {
|
|
312
|
-
const { factory } = createMockUOWFactory([{ success: false }, { success: false }]);
|
|
313
|
-
const controller = new AbortController();
|
|
314
|
-
|
|
315
|
-
// Abort after first attempt
|
|
316
|
-
setTimeout(() => controller.abort(), 50);
|
|
317
|
-
|
|
318
|
-
const result = await executeUnitOfWork(
|
|
319
|
-
{
|
|
320
|
-
retrieve: (uow) => uow.find("users", (b) => b.whereIndex("primary")),
|
|
321
|
-
mutate: async (uow, [users]) => {
|
|
322
|
-
uow.update("users", users[0].id, (b) => b.set({ balance: 200 }));
|
|
323
|
-
},
|
|
324
|
-
},
|
|
325
|
-
{
|
|
326
|
-
createUnitOfWork: factory,
|
|
327
|
-
retryPolicy: new LinearBackoffRetryPolicy({ maxRetries: 5, delayMs: 100 }),
|
|
328
|
-
signal: controller.signal,
|
|
329
|
-
},
|
|
330
|
-
);
|
|
331
|
-
|
|
332
|
-
assert(!result.success);
|
|
333
|
-
expect(result.reason).toBe("aborted");
|
|
334
|
-
});
|
|
335
|
-
});
|
|
336
|
-
|
|
337
|
-
describe("onSuccess callback", () => {
|
|
338
|
-
it("should pass mutation result to onSuccess callback", async () => {
|
|
339
|
-
const { factory } = createMockUOWFactory([{ success: true }]);
|
|
340
|
-
const onSuccess = vi.fn();
|
|
341
|
-
|
|
342
|
-
await executeUnitOfWork(
|
|
343
|
-
{
|
|
344
|
-
retrieve: (uow) => uow.find("users", (b) => b.whereIndex("primary")),
|
|
345
|
-
mutate: async () => {
|
|
346
|
-
return { updatedCount: 5 };
|
|
347
|
-
},
|
|
348
|
-
onSuccess,
|
|
349
|
-
},
|
|
350
|
-
{ createUnitOfWork: factory },
|
|
351
|
-
);
|
|
352
|
-
|
|
353
|
-
expect(onSuccess).toHaveBeenCalledTimes(1);
|
|
354
|
-
expect(onSuccess).toHaveBeenCalledWith({
|
|
355
|
-
results: expect.any(Array),
|
|
356
|
-
mutationResult: { updatedCount: 5 },
|
|
357
|
-
createdIds: [],
|
|
358
|
-
nonce: expect.any(String),
|
|
359
|
-
});
|
|
360
|
-
});
|
|
361
|
-
|
|
362
|
-
it("should only execute onSuccess callback on success", async () => {
|
|
363
|
-
const { factory } = createMockUOWFactory([{ success: false }]);
|
|
364
|
-
const onSuccess = vi.fn();
|
|
365
|
-
|
|
366
|
-
const result = await executeUnitOfWork(
|
|
367
|
-
{
|
|
368
|
-
retrieve: (uow) => uow.find("users", (b) => b.whereIndex("primary")),
|
|
369
|
-
mutate: async (uow, [users]) => {
|
|
370
|
-
uow.update("users", users[0].id, (b) => b.set({ balance: 200 }));
|
|
371
|
-
},
|
|
372
|
-
onSuccess,
|
|
373
|
-
},
|
|
374
|
-
{
|
|
375
|
-
createUnitOfWork: factory,
|
|
376
|
-
retryPolicy: new NoRetryPolicy(),
|
|
377
|
-
},
|
|
378
|
-
);
|
|
379
|
-
|
|
380
|
-
assert(!result.success);
|
|
381
|
-
expect(result.reason).toBe("conflict");
|
|
382
|
-
expect(onSuccess).not.toHaveBeenCalled();
|
|
383
|
-
});
|
|
384
|
-
|
|
385
|
-
it("should execute onSuccess only once even after retries", async () => {
|
|
386
|
-
const { factory } = createMockUOWFactory([
|
|
387
|
-
{ success: false },
|
|
388
|
-
{ success: false },
|
|
389
|
-
{ success: true },
|
|
390
|
-
]);
|
|
391
|
-
const onSuccess = vi.fn();
|
|
392
|
-
|
|
393
|
-
await executeUnitOfWork(
|
|
394
|
-
{
|
|
395
|
-
retrieve: (uow) => uow.find("users", (b) => b.whereIndex("primary")),
|
|
396
|
-
mutate: async (uow, [users]) => {
|
|
397
|
-
uow.update("users", users[0].id, (b) => b.set({ balance: 200 }));
|
|
398
|
-
},
|
|
399
|
-
onSuccess,
|
|
400
|
-
},
|
|
401
|
-
{
|
|
402
|
-
createUnitOfWork: factory,
|
|
403
|
-
retryPolicy: new ExponentialBackoffRetryPolicy({ maxRetries: 3, initialDelayMs: 1 }),
|
|
404
|
-
},
|
|
405
|
-
);
|
|
406
|
-
|
|
407
|
-
expect(onSuccess).toHaveBeenCalledTimes(1);
|
|
408
|
-
});
|
|
409
|
-
|
|
410
|
-
it("should handle async onSuccess callback", async () => {
|
|
411
|
-
const { factory } = createMockUOWFactory([{ success: true }]);
|
|
412
|
-
const onSuccess = vi.fn(async () => {
|
|
413
|
-
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
414
|
-
});
|
|
415
|
-
|
|
416
|
-
await executeUnitOfWork(
|
|
417
|
-
{
|
|
418
|
-
retrieve: (uow) => uow.find("users", (b) => b.whereIndex("primary")),
|
|
419
|
-
mutate: async (uow, [users]) => {
|
|
420
|
-
uow.update("users", users[0].id, (b) => b.set({ balance: 200 }));
|
|
421
|
-
},
|
|
422
|
-
onSuccess,
|
|
423
|
-
},
|
|
424
|
-
{ createUnitOfWork: factory },
|
|
425
|
-
);
|
|
426
|
-
|
|
427
|
-
expect(onSuccess).toHaveBeenCalledTimes(1);
|
|
428
|
-
});
|
|
429
|
-
});
|
|
171
|
+
],
|
|
172
|
+
executeMutationPhase: async () => ({ success: true, createdInternalIds: [] }),
|
|
173
|
+
};
|
|
174
|
+
const decoder = createMockDecoder();
|
|
175
|
+
const baseUow = createUnitOfWork(compiler, executor, decoder);
|
|
430
176
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
const testError = new Error("Retrieve failed");
|
|
177
|
+
const txResult = createServiceTxBuilder(testSchema, baseUow)
|
|
178
|
+
.retrieve((uow) => uow.find("users", (b) => b.whereIndex("idx_email")))
|
|
179
|
+
.build();
|
|
435
180
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
throw testError;
|
|
440
|
-
},
|
|
441
|
-
mutate: async () => {},
|
|
442
|
-
},
|
|
443
|
-
{ createUnitOfWork: factory },
|
|
444
|
-
);
|
|
445
|
-
|
|
446
|
-
assert(!result.success);
|
|
447
|
-
assert(result.reason === "error");
|
|
448
|
-
expect(result.error).toBe(testError);
|
|
181
|
+
expect(isTxResult(txResult)).toBe(true);
|
|
182
|
+
expect(txResult._internal.schema).toBe(testSchema);
|
|
183
|
+
expect(txResult._internal.callbacks.retrieve).toBeDefined();
|
|
449
184
|
});
|
|
450
185
|
|
|
451
|
-
it("should
|
|
452
|
-
const
|
|
453
|
-
const
|
|
186
|
+
it("should create a TxResult with transformRetrieve callback", () => {
|
|
187
|
+
const compiler = createMockCompiler();
|
|
188
|
+
const executor: UOWExecutor<unknown, unknown> = {
|
|
189
|
+
executeRetrievalPhase: async () => [
|
|
190
|
+
[
|
|
191
|
+
{
|
|
192
|
+
id: FragnoId.fromExternal("1", 1),
|
|
193
|
+
email: "test@example.com",
|
|
194
|
+
name: "Test",
|
|
195
|
+
balance: 100,
|
|
196
|
+
},
|
|
197
|
+
],
|
|
198
|
+
],
|
|
199
|
+
executeMutationPhase: async () => ({ success: true, createdInternalIds: [] }),
|
|
200
|
+
};
|
|
201
|
+
const decoder = createMockDecoder();
|
|
202
|
+
const baseUow = createUnitOfWork(compiler, executor, decoder);
|
|
454
203
|
|
|
455
|
-
const
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
throw testError;
|
|
460
|
-
},
|
|
461
|
-
},
|
|
462
|
-
{ createUnitOfWork: factory },
|
|
463
|
-
);
|
|
204
|
+
const txResult = createServiceTxBuilder(testSchema, baseUow)
|
|
205
|
+
.retrieve((uow) => uow.find("users", (b) => b.whereIndex("idx_email")))
|
|
206
|
+
.transformRetrieve(([users]) => users[0] ?? null)
|
|
207
|
+
.build();
|
|
464
208
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
expect(result.error).toBe(testError);
|
|
209
|
+
expect(isTxResult(txResult)).toBe(true);
|
|
210
|
+
expect(txResult._internal.callbacks.retrieveSuccess).toBeDefined();
|
|
468
211
|
});
|
|
469
212
|
|
|
470
|
-
it("should
|
|
471
|
-
const
|
|
472
|
-
const
|
|
213
|
+
it("should create a TxResult with serviceCalls", () => {
|
|
214
|
+
const compiler = createMockCompiler();
|
|
215
|
+
const executor: UOWExecutor<unknown, unknown> = {
|
|
216
|
+
executeRetrievalPhase: async () => [
|
|
217
|
+
[
|
|
218
|
+
{
|
|
219
|
+
id: FragnoId.fromExternal("1", 1),
|
|
220
|
+
email: "test@example.com",
|
|
221
|
+
name: "Test",
|
|
222
|
+
balance: 100,
|
|
223
|
+
},
|
|
224
|
+
],
|
|
225
|
+
],
|
|
226
|
+
executeMutationPhase: async () => ({ success: true, createdInternalIds: [] }),
|
|
227
|
+
};
|
|
228
|
+
const decoder = createMockDecoder();
|
|
229
|
+
const baseUow = createUnitOfWork(compiler, executor, decoder);
|
|
473
230
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
231
|
+
// Create a dependency TxResult
|
|
232
|
+
const depTxResult = createServiceTxBuilder(testSchema, baseUow)
|
|
233
|
+
.retrieve((uow) => uow.find("users", (b) => b.whereIndex("idx_email")))
|
|
234
|
+
.transformRetrieve(([users]) => users[0] ?? null)
|
|
235
|
+
.build();
|
|
236
|
+
|
|
237
|
+
// Create a TxResult that depends on it
|
|
238
|
+
const txResult = createServiceTxBuilder(testSchema, baseUow)
|
|
239
|
+
.withServiceCalls(() => [depTxResult])
|
|
240
|
+
.mutate(({ uow, serviceIntermediateResult: [user] }) => {
|
|
241
|
+
if (!user) {
|
|
242
|
+
throw new Error("User not found");
|
|
243
|
+
}
|
|
244
|
+
return uow.create("users", { email: "new@example.com", name: "New", balance: 0 });
|
|
245
|
+
})
|
|
246
|
+
.build();
|
|
484
247
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
expect(result.error).toBe(testError);
|
|
248
|
+
expect(isTxResult(txResult)).toBe(true);
|
|
249
|
+
expect(txResult._internal.serviceCalls).toHaveLength(1);
|
|
488
250
|
});
|
|
489
251
|
|
|
490
|
-
it("should
|
|
491
|
-
const
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
{
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
},
|
|
499
|
-
},
|
|
500
|
-
{ createUnitOfWork: factory },
|
|
501
|
-
);
|
|
252
|
+
it("should type mutateResult as non-undefined when success AND mutate are provided", () => {
|
|
253
|
+
const compiler = createMockCompiler();
|
|
254
|
+
const executor: UOWExecutor<unknown, unknown> = {
|
|
255
|
+
executeRetrievalPhase: async () => [],
|
|
256
|
+
executeMutationPhase: async () => ({ success: true, createdInternalIds: [] }),
|
|
257
|
+
};
|
|
258
|
+
const decoder = createMockDecoder();
|
|
259
|
+
const baseUow = createUnitOfWork(compiler, executor, decoder);
|
|
502
260
|
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
261
|
+
// When BOTH mutate AND success are provided, mutateResult should NOT be undefined
|
|
262
|
+
createServiceTxBuilder(testSchema, baseUow)
|
|
263
|
+
.mutate(({ uow }) => {
|
|
264
|
+
uow.create("users", { email: "test@example.com", name: "Test", balance: 0 });
|
|
265
|
+
return { created: true as const, code: "ABC123" };
|
|
266
|
+
})
|
|
267
|
+
.transform(({ mutateResult }) => {
|
|
268
|
+
// Key type assertion: mutateResult is NOT undefined when mutate callback IS provided
|
|
269
|
+
expectTypeOf(mutateResult).toEqualTypeOf<{ created: true; code: string }>();
|
|
270
|
+
// Should be able to access properties without null check
|
|
271
|
+
return { success: true, code: mutateResult.code };
|
|
272
|
+
})
|
|
273
|
+
.build();
|
|
506
274
|
});
|
|
507
|
-
});
|
|
508
|
-
|
|
509
|
-
describe("retrieval results", () => {
|
|
510
|
-
it("should pass retrieval results to mutation phase", async () => {
|
|
511
|
-
const { factory } = createMockUOWFactory([{ success: true }]);
|
|
512
|
-
const mutationPhase = vi.fn(async (_uow: unknown, _results: unknown) => {});
|
|
513
275
|
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
276
|
+
it("should type mutateResult as undefined when success is provided but mutate is NOT", () => {
|
|
277
|
+
const compiler = createMockCompiler();
|
|
278
|
+
const executor: UOWExecutor<unknown, unknown> = {
|
|
279
|
+
executeRetrievalPhase: async () => [
|
|
280
|
+
[
|
|
281
|
+
{
|
|
282
|
+
id: FragnoId.fromExternal("1", 1),
|
|
283
|
+
email: "test@example.com",
|
|
284
|
+
name: "Test",
|
|
285
|
+
balance: 100,
|
|
286
|
+
},
|
|
287
|
+
],
|
|
288
|
+
],
|
|
289
|
+
executeMutationPhase: async () => ({ success: true, createdInternalIds: [] }),
|
|
290
|
+
};
|
|
291
|
+
const decoder = createMockDecoder();
|
|
292
|
+
const baseUow = createUnitOfWork(compiler, executor, decoder);
|
|
521
293
|
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
294
|
+
// When success is provided but mutate is NOT, mutateResult should be undefined
|
|
295
|
+
createServiceTxBuilder(testSchema, baseUow)
|
|
296
|
+
.retrieve((uow) => uow.find("users", (b) => b.whereIndex("idx_email")))
|
|
297
|
+
.transformRetrieve(([users]) => users[0] ?? null)
|
|
298
|
+
// NO mutate callback
|
|
299
|
+
.transform(({ mutateResult, retrieveResult }) => {
|
|
300
|
+
// Key type assertion: mutateResult IS undefined when no mutate callback
|
|
301
|
+
expectTypeOf(mutateResult).toEqualTypeOf<undefined>();
|
|
302
|
+
// retrieveResult should still be properly typed (can be null from ?? null)
|
|
303
|
+
if (retrieveResult !== null) {
|
|
304
|
+
expectTypeOf(retrieveResult.email).toEqualTypeOf<string>();
|
|
305
|
+
}
|
|
306
|
+
return { user: retrieveResult };
|
|
307
|
+
})
|
|
308
|
+
.build();
|
|
529
309
|
});
|
|
530
310
|
|
|
531
|
-
it("should
|
|
532
|
-
const
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
311
|
+
it("should type retrieveResult as TRetrieveResults when retrieve is provided but retrieveSuccess is NOT", () => {
|
|
312
|
+
const compiler = createMockCompiler();
|
|
313
|
+
const executor: UOWExecutor<unknown, unknown> = {
|
|
314
|
+
executeRetrievalPhase: async () => [
|
|
315
|
+
[
|
|
316
|
+
{
|
|
317
|
+
id: FragnoId.fromExternal("1", 1),
|
|
318
|
+
email: "test@example.com",
|
|
319
|
+
name: "Test",
|
|
320
|
+
balance: 100,
|
|
321
|
+
},
|
|
322
|
+
],
|
|
323
|
+
],
|
|
324
|
+
executeMutationPhase: async () => ({ success: true, createdInternalIds: [] }),
|
|
325
|
+
};
|
|
326
|
+
const decoder = createMockDecoder();
|
|
327
|
+
const baseUow = createUnitOfWork(compiler, executor, decoder);
|
|
541
328
|
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
329
|
+
// When retrieve IS provided but retrieveSuccess is NOT, retrieveResult should be TRetrieveResults
|
|
330
|
+
// (the raw array from the retrieve callback), NOT unknown
|
|
331
|
+
createServiceTxBuilder(testSchema, baseUow)
|
|
332
|
+
.retrieve((uow) => uow.find("users", (b) => b.whereIndex("idx_email")))
|
|
333
|
+
// NO transformRetrieve callback - this is the key scenario
|
|
334
|
+
.mutate(({ uow, retrieveResult }) => {
|
|
335
|
+
// Key type assertion: retrieveResult should be the raw array type, NOT unknown
|
|
336
|
+
// The retrieve callback returns TypedUnitOfWork with [users[]] as the results type
|
|
337
|
+
expectTypeOf(retrieveResult).toEqualTypeOf<
|
|
338
|
+
[{ id: FragnoId; email: string; name: string; balance: number }[]]
|
|
339
|
+
>();
|
|
340
|
+
|
|
341
|
+
// Should be able to access the array without type errors
|
|
342
|
+
const users = retrieveResult[0];
|
|
343
|
+
expectTypeOf(users).toEqualTypeOf<
|
|
344
|
+
{ id: FragnoId; email: string; name: string; balance: number }[]
|
|
345
|
+
>();
|
|
346
|
+
|
|
347
|
+
if (users.length > 0) {
|
|
348
|
+
const user = users[0];
|
|
349
|
+
uow.update("users", user.id, (b) => b.set({ balance: user.balance + 100 }));
|
|
350
|
+
}
|
|
351
|
+
return { processed: true };
|
|
352
|
+
})
|
|
353
|
+
.build();
|
|
545
354
|
});
|
|
546
355
|
});
|
|
547
356
|
|
|
548
|
-
describe("
|
|
549
|
-
it("should
|
|
550
|
-
const
|
|
551
|
-
|
|
552
|
-
const result = await executeUnitOfWork(
|
|
553
|
-
{
|
|
554
|
-
retrieve: (uow) => uow.find("users", (b) => b.whereIndex("primary")),
|
|
555
|
-
mutate: async () => {
|
|
556
|
-
return {
|
|
557
|
-
userId: Promise.resolve("user-123"),
|
|
558
|
-
count: Promise.resolve(42),
|
|
559
|
-
data: "plain-value",
|
|
560
|
-
};
|
|
561
|
-
},
|
|
562
|
-
},
|
|
563
|
-
{ createUnitOfWork: factory },
|
|
564
|
-
);
|
|
565
|
-
|
|
566
|
-
assert(result.success);
|
|
567
|
-
expect(result.mutationResult).toEqual({
|
|
568
|
-
userId: "user-123",
|
|
569
|
-
count: 42,
|
|
570
|
-
data: "plain-value",
|
|
571
|
-
});
|
|
572
|
-
});
|
|
573
|
-
|
|
574
|
-
it("should await promises in mutation result array", async () => {
|
|
575
|
-
const { factory } = createMockUOWFactory([{ success: true }]);
|
|
576
|
-
|
|
577
|
-
const result = await executeUnitOfWork(
|
|
578
|
-
{
|
|
579
|
-
retrieve: (uow) => uow.find("users", (b) => b.whereIndex("primary")),
|
|
580
|
-
mutate: async () => {
|
|
581
|
-
return [Promise.resolve("a"), Promise.resolve("b"), "c"];
|
|
582
|
-
},
|
|
583
|
-
},
|
|
584
|
-
{ createUnitOfWork: factory },
|
|
585
|
-
);
|
|
586
|
-
|
|
587
|
-
assert(result.success);
|
|
588
|
-
expect(result.mutationResult).toEqual(["a", "b", "c"]);
|
|
589
|
-
});
|
|
590
|
-
|
|
591
|
-
it("should await direct promise mutation result", async () => {
|
|
592
|
-
const { factory } = createMockUOWFactory([{ success: true }]);
|
|
593
|
-
|
|
594
|
-
const result = await executeUnitOfWork(
|
|
595
|
-
{
|
|
596
|
-
retrieve: (uow) => uow.find("users", (b) => b.whereIndex("primary")),
|
|
597
|
-
mutate: async () => {
|
|
598
|
-
return Promise.resolve({ value: "resolved" });
|
|
599
|
-
},
|
|
600
|
-
},
|
|
601
|
-
{ createUnitOfWork: factory },
|
|
602
|
-
);
|
|
603
|
-
|
|
604
|
-
assert(result.success);
|
|
605
|
-
expect(result.mutationResult).toEqual({ value: "resolved" });
|
|
606
|
-
});
|
|
607
|
-
|
|
608
|
-
it("should NOT await nested promises (only 1 level deep)", async () => {
|
|
609
|
-
const { factory } = createMockUOWFactory([{ success: true }]);
|
|
610
|
-
|
|
611
|
-
const result = await executeUnitOfWork(
|
|
357
|
+
describe("executeTx", () => {
|
|
358
|
+
it("should execute a simple retrieve-only transaction", async () => {
|
|
359
|
+
const compiler = createMockCompiler();
|
|
360
|
+
const mockUsers = [
|
|
612
361
|
{
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
};
|
|
618
|
-
},
|
|
362
|
+
id: FragnoId.fromExternal("1", 1),
|
|
363
|
+
email: "alice@example.com",
|
|
364
|
+
name: "Alice",
|
|
365
|
+
balance: 100,
|
|
619
366
|
},
|
|
620
|
-
{ createUnitOfWork: factory },
|
|
621
|
-
);
|
|
622
|
-
|
|
623
|
-
assert(result.success);
|
|
624
|
-
// The nested promise should still be a promise
|
|
625
|
-
expect(result.mutationResult.nested.promise).toBeInstanceOf(Promise);
|
|
626
|
-
});
|
|
627
|
-
|
|
628
|
-
it("should handle mixed types in mutation result", async () => {
|
|
629
|
-
const { factory } = createMockUOWFactory([{ success: true }]);
|
|
630
|
-
|
|
631
|
-
const result = await executeUnitOfWork(
|
|
632
367
|
{
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
number: 42,
|
|
638
|
-
string: "test",
|
|
639
|
-
null: null,
|
|
640
|
-
undefined: undefined,
|
|
641
|
-
nested: { value: "nested" },
|
|
642
|
-
};
|
|
643
|
-
},
|
|
644
|
-
},
|
|
645
|
-
{ createUnitOfWork: factory },
|
|
646
|
-
);
|
|
647
|
-
|
|
648
|
-
assert(result.success);
|
|
649
|
-
expect(result.mutationResult).toEqual({
|
|
650
|
-
promise: 100,
|
|
651
|
-
number: 42,
|
|
652
|
-
string: "test",
|
|
653
|
-
null: null,
|
|
654
|
-
undefined: undefined,
|
|
655
|
-
nested: { value: "nested" },
|
|
656
|
-
});
|
|
657
|
-
});
|
|
658
|
-
|
|
659
|
-
it("should pass awaited mutation result to onSuccess callback", async () => {
|
|
660
|
-
const { factory } = createMockUOWFactory([{ success: true }]);
|
|
661
|
-
const onSuccess = vi.fn();
|
|
662
|
-
|
|
663
|
-
await executeUnitOfWork(
|
|
664
|
-
{
|
|
665
|
-
retrieve: (uow) => uow.find("users", (b) => b.whereIndex("primary")),
|
|
666
|
-
mutate: async () => {
|
|
667
|
-
return {
|
|
668
|
-
userId: Promise.resolve("user-456"),
|
|
669
|
-
status: Promise.resolve("active"),
|
|
670
|
-
};
|
|
671
|
-
},
|
|
672
|
-
onSuccess,
|
|
673
|
-
},
|
|
674
|
-
{ createUnitOfWork: factory },
|
|
675
|
-
);
|
|
676
|
-
|
|
677
|
-
expect(onSuccess).toHaveBeenCalledExactlyOnceWith({
|
|
678
|
-
results: expect.any(Array),
|
|
679
|
-
mutationResult: {
|
|
680
|
-
userId: "user-456",
|
|
681
|
-
status: "active",
|
|
682
|
-
},
|
|
683
|
-
createdIds: [],
|
|
684
|
-
nonce: expect.any(String),
|
|
685
|
-
});
|
|
686
|
-
});
|
|
687
|
-
});
|
|
688
|
-
});
|
|
689
|
-
|
|
690
|
-
describe("executeRestrictedUnitOfWork", () => {
|
|
691
|
-
describe("basic success cases", () => {
|
|
692
|
-
it("should execute a simple mutation-only workflow", async () => {
|
|
693
|
-
const { factory, callCount } = createMockUOWFactory([{ success: true }]);
|
|
694
|
-
|
|
695
|
-
const result = await executeRestrictedUnitOfWork(
|
|
696
|
-
async ({ forSchema, executeMutate }) => {
|
|
697
|
-
const uow = forSchema(testSchema);
|
|
698
|
-
const userId = uow.create("users", {
|
|
699
|
-
id: "user-1",
|
|
700
|
-
email: "test@example.com",
|
|
701
|
-
name: "Test User",
|
|
702
|
-
balance: 100,
|
|
703
|
-
});
|
|
704
|
-
|
|
705
|
-
await executeMutate();
|
|
706
|
-
|
|
707
|
-
return { userId: userId.externalId };
|
|
708
|
-
},
|
|
709
|
-
{ createUnitOfWork: factory },
|
|
710
|
-
);
|
|
711
|
-
|
|
712
|
-
expect(result).toEqual({ userId: "user-1" });
|
|
713
|
-
expect(callCount.value).toBe(1);
|
|
714
|
-
});
|
|
715
|
-
|
|
716
|
-
it("should execute retrieval and mutation phases", async () => {
|
|
717
|
-
const { factory, callCount } = createMockUOWFactory([{ success: true }]);
|
|
718
|
-
|
|
719
|
-
const result = await executeRestrictedUnitOfWork(
|
|
720
|
-
async ({ forSchema, executeRetrieve, executeMutate }) => {
|
|
721
|
-
const uow = forSchema(testSchema).find("users", (b) => b.whereIndex("primary"));
|
|
722
|
-
await executeRetrieve();
|
|
723
|
-
const [[user]] = await uow.retrievalPhase;
|
|
724
|
-
|
|
725
|
-
uow.update("users", user.id, (b) => b.set({ balance: user.balance + 50 }).check());
|
|
726
|
-
await executeMutate();
|
|
727
|
-
|
|
728
|
-
return { newBalance: user.balance + 50 };
|
|
729
|
-
},
|
|
730
|
-
{ createUnitOfWork: factory },
|
|
731
|
-
);
|
|
732
|
-
|
|
733
|
-
expect(result).toEqual({ newBalance: 150 });
|
|
734
|
-
expect(callCount.value).toBe(1);
|
|
735
|
-
});
|
|
736
|
-
|
|
737
|
-
it("should return callback result directly", async () => {
|
|
738
|
-
const { factory } = createMockUOWFactory([{ success: true }]);
|
|
739
|
-
|
|
740
|
-
const result = await executeRestrictedUnitOfWork(
|
|
741
|
-
async () => {
|
|
742
|
-
return { data: "test", count: 42, nested: { value: true } };
|
|
368
|
+
id: FragnoId.fromExternal("2", 1),
|
|
369
|
+
email: "bob@example.com",
|
|
370
|
+
name: "Bob",
|
|
371
|
+
balance: 200,
|
|
743
372
|
},
|
|
744
|
-
|
|
745
|
-
|
|
373
|
+
];
|
|
374
|
+
const executor: UOWExecutor<unknown, unknown> = {
|
|
375
|
+
executeRetrievalPhase: async () => [mockUsers],
|
|
376
|
+
executeMutationPhase: async () => ({ success: true, createdInternalIds: [] }),
|
|
377
|
+
};
|
|
378
|
+
const decoder = createMockDecoder();
|
|
746
379
|
|
|
747
|
-
|
|
380
|
+
const [users] = await createHandlerTxBuilder({
|
|
381
|
+
createUnitOfWork: () => createUnitOfWork(compiler, executor, decoder),
|
|
382
|
+
})
|
|
383
|
+
.retrieve(({ forSchema }) =>
|
|
384
|
+
forSchema(testSchema).find("users", (b) => b.whereIndex("idx_email")),
|
|
385
|
+
)
|
|
386
|
+
.execute();
|
|
387
|
+
|
|
388
|
+
expect(users).toHaveLength(2);
|
|
389
|
+
expect(users[0].email).toBe("alice@example.com");
|
|
390
|
+
expect(users[1].name).toBe("Bob");
|
|
748
391
|
});
|
|
749
|
-
});
|
|
750
|
-
|
|
751
|
-
describe("retry behavior", () => {
|
|
752
|
-
it("should retry on conflict and eventually succeed", async () => {
|
|
753
|
-
const { factory, callCount } = createMockUOWFactory([
|
|
754
|
-
{ success: false }, // First attempt fails
|
|
755
|
-
{ success: false }, // Second attempt fails
|
|
756
|
-
{ success: true }, // Third attempt succeeds
|
|
757
|
-
]);
|
|
758
392
|
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
const
|
|
762
|
-
async (
|
|
763
|
-
|
|
764
|
-
|
|
393
|
+
it("should execute a simple mutate-only transaction", async () => {
|
|
394
|
+
const compiler = createMockCompiler();
|
|
395
|
+
const executor: UOWExecutor<unknown, unknown> = {
|
|
396
|
+
executeRetrievalPhase: async () => [],
|
|
397
|
+
executeMutationPhase: async () => ({ success: true, createdInternalIds: [BigInt(1)] }),
|
|
398
|
+
};
|
|
399
|
+
const decoder = createMockDecoder();
|
|
765
400
|
|
|
766
|
-
|
|
767
|
-
|
|
401
|
+
const result = await createHandlerTxBuilder({
|
|
402
|
+
createUnitOfWork: () => createUnitOfWork(compiler, executor, decoder),
|
|
403
|
+
})
|
|
404
|
+
.mutate(({ forSchema }) => {
|
|
405
|
+
const userId = forSchema(testSchema).create("users", {
|
|
768
406
|
email: "test@example.com",
|
|
769
|
-
name: "Test
|
|
407
|
+
name: "Test",
|
|
770
408
|
balance: 100,
|
|
771
409
|
});
|
|
410
|
+
return { userId };
|
|
411
|
+
})
|
|
412
|
+
.execute();
|
|
772
413
|
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
return { attempt: callbackExecutions.count };
|
|
776
|
-
},
|
|
777
|
-
{ createUnitOfWork: factory },
|
|
778
|
-
);
|
|
779
|
-
|
|
780
|
-
expect(result.attempt).toBe(3);
|
|
781
|
-
expect(callCount.value).toBe(3);
|
|
782
|
-
expect(callbackExecutions.count).toBe(3);
|
|
414
|
+
expect(result.userId).toBeInstanceOf(FragnoId);
|
|
783
415
|
});
|
|
784
416
|
|
|
785
|
-
it("should throw
|
|
786
|
-
const
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
{ success: false },
|
|
790
|
-
|
|
791
|
-
|
|
417
|
+
it("should throw when retry policy is provided without retrieve operations", async () => {
|
|
418
|
+
const compiler = createMockCompiler();
|
|
419
|
+
const executor: UOWExecutor<unknown, unknown> = {
|
|
420
|
+
executeRetrievalPhase: async () => [],
|
|
421
|
+
executeMutationPhase: async () => ({ success: false }),
|
|
422
|
+
};
|
|
423
|
+
const decoder = createMockDecoder();
|
|
792
424
|
|
|
793
425
|
await expect(
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
// Default policy has maxRetries: 5, so we make 6 attempts (initial + 5 retries)
|
|
804
|
-
expect(callCount.value).toBe(6);
|
|
805
|
-
});
|
|
806
|
-
|
|
807
|
-
it("should respect custom retry policy", async () => {
|
|
808
|
-
const { factory, callCount } = createMockUOWFactory([
|
|
809
|
-
{ success: false },
|
|
810
|
-
{ success: false },
|
|
811
|
-
{ success: false },
|
|
812
|
-
{ success: false },
|
|
813
|
-
{ success: false },
|
|
814
|
-
{ success: true },
|
|
815
|
-
]);
|
|
816
|
-
|
|
817
|
-
const result = await executeRestrictedUnitOfWork(
|
|
818
|
-
async ({ executeMutate }) => {
|
|
819
|
-
await executeMutate();
|
|
820
|
-
return { done: true };
|
|
821
|
-
},
|
|
822
|
-
{
|
|
823
|
-
createUnitOfWork: factory,
|
|
824
|
-
retryPolicy: new ExponentialBackoffRetryPolicy({ maxRetries: 5, initialDelayMs: 1 }),
|
|
825
|
-
},
|
|
826
|
-
);
|
|
827
|
-
|
|
828
|
-
expect(result).toEqual({ done: true });
|
|
829
|
-
expect(callCount.value).toBe(6); // Initial + 5 retries
|
|
830
|
-
});
|
|
831
|
-
|
|
832
|
-
it("should use default ExponentialBackoffRetryPolicy with small delays", async () => {
|
|
833
|
-
const { factory } = createMockUOWFactory([{ success: false }, { success: true }]);
|
|
834
|
-
|
|
835
|
-
const startTime = Date.now();
|
|
836
|
-
await executeRestrictedUnitOfWork(
|
|
837
|
-
async ({ executeMutate }) => {
|
|
838
|
-
await executeMutate();
|
|
839
|
-
return {};
|
|
840
|
-
},
|
|
841
|
-
{ createUnitOfWork: factory },
|
|
426
|
+
createHandlerTxBuilder({
|
|
427
|
+
createUnitOfWork: () => createUnitOfWork(compiler, executor, decoder),
|
|
428
|
+
retryPolicy: new NoRetryPolicy(),
|
|
429
|
+
})
|
|
430
|
+
.mutate(() => ({ done: true }))
|
|
431
|
+
.execute(),
|
|
432
|
+
).rejects.toThrow(
|
|
433
|
+
"Retry policy is only supported when the transaction includes retrieve operations.",
|
|
842
434
|
);
|
|
843
|
-
const elapsed = Date.now() - startTime;
|
|
844
|
-
|
|
845
|
-
// Default policy has initialDelayMs: 10, maxDelayMs: 100
|
|
846
|
-
// First retry delay should be around 10ms
|
|
847
|
-
expect(elapsed).toBeLessThan(200); // Allow some margin
|
|
848
|
-
});
|
|
849
|
-
});
|
|
850
|
-
|
|
851
|
-
describe("error handling", () => {
|
|
852
|
-
it("should throw error from callback immediately without retry", async () => {
|
|
853
|
-
const { factory, callCount } = createMockUOWFactory([{ success: true }]);
|
|
854
|
-
|
|
855
|
-
await expect(
|
|
856
|
-
executeRestrictedUnitOfWork(
|
|
857
|
-
async () => {
|
|
858
|
-
throw new Error("Callback error");
|
|
859
|
-
},
|
|
860
|
-
{ createUnitOfWork: factory },
|
|
861
|
-
),
|
|
862
|
-
).rejects.toThrow("Callback error");
|
|
863
|
-
|
|
864
|
-
// Should NOT retry non-conflict errors
|
|
865
|
-
expect(callCount.value).toBe(1); // Only initial attempt
|
|
866
435
|
});
|
|
867
436
|
|
|
868
|
-
it("should
|
|
869
|
-
const
|
|
870
|
-
const
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
);
|
|
882
|
-
expect.fail("Should have thrown");
|
|
883
|
-
} catch (error) {
|
|
884
|
-
// Error should be thrown directly, not wrapped
|
|
885
|
-
expect(error).toBe(originalError);
|
|
886
|
-
}
|
|
887
|
-
});
|
|
888
|
-
});
|
|
889
|
-
|
|
890
|
-
describe("abort signal", () => {
|
|
891
|
-
it("should throw when aborted before execution", async () => {
|
|
892
|
-
const { factory } = createMockUOWFactory([{ success: true }]);
|
|
893
|
-
const controller = new AbortController();
|
|
894
|
-
controller.abort();
|
|
437
|
+
it("should execute a transaction with serviceCalls as retrieve source", async () => {
|
|
438
|
+
const compiler = createMockCompiler();
|
|
439
|
+
const mockUser = {
|
|
440
|
+
id: FragnoId.fromExternal("1", 1),
|
|
441
|
+
email: "test@example.com",
|
|
442
|
+
name: "Test",
|
|
443
|
+
balance: 100,
|
|
444
|
+
};
|
|
445
|
+
const executor: UOWExecutor<unknown, unknown> = {
|
|
446
|
+
executeRetrievalPhase: async () => [[mockUser]],
|
|
447
|
+
executeMutationPhase: async () => ({ success: true, createdInternalIds: [] }),
|
|
448
|
+
};
|
|
449
|
+
const decoder = createMockDecoder();
|
|
895
450
|
|
|
896
|
-
|
|
897
|
-
executeRestrictedUnitOfWork(
|
|
898
|
-
async () => {
|
|
899
|
-
return {};
|
|
900
|
-
},
|
|
901
|
-
{ createUnitOfWork: factory, signal: controller.signal },
|
|
902
|
-
),
|
|
903
|
-
).rejects.toThrow("Unit of Work execution aborted");
|
|
904
|
-
});
|
|
451
|
+
let currentUow: IUnitOfWork | null = null;
|
|
905
452
|
|
|
906
|
-
|
|
907
|
-
const
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
453
|
+
// Service that retrieves
|
|
454
|
+
const getUserById = () => {
|
|
455
|
+
return createServiceTxBuilder(testSchema, currentUow!)
|
|
456
|
+
.retrieve((uow) => uow.find("users", (b) => b.whereIndex("idx_email")))
|
|
457
|
+
.transformRetrieve(([users]) => users[0] ?? null)
|
|
458
|
+
.build();
|
|
459
|
+
};
|
|
913
460
|
|
|
914
|
-
const
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
}
|
|
919
|
-
await executeMutate();
|
|
920
|
-
return {};
|
|
461
|
+
const result = await createHandlerTxBuilder({
|
|
462
|
+
createUnitOfWork: () => {
|
|
463
|
+
currentUow = createUnitOfWork(compiler, executor, decoder);
|
|
464
|
+
return currentUow;
|
|
921
465
|
},
|
|
922
|
-
|
|
923
|
-
|
|
466
|
+
})
|
|
467
|
+
.withServiceCalls(() => [getUserById()])
|
|
468
|
+
.transform(({ serviceResult: [user] }) => user)
|
|
469
|
+
.execute();
|
|
924
470
|
|
|
925
|
-
|
|
926
|
-
expect(callCount.value).toBeLessThanOrEqual(2);
|
|
471
|
+
expect(result).toEqual(mockUser);
|
|
927
472
|
});
|
|
928
|
-
});
|
|
929
|
-
|
|
930
|
-
describe("restricted UOW interface", () => {
|
|
931
|
-
it("should provide access to forSchema", async () => {
|
|
932
|
-
const { factory } = createMockUOWFactory([{ success: true }]);
|
|
933
473
|
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
474
|
+
it("should execute a transaction with mutate callback using serviceCalls", async () => {
|
|
475
|
+
const compiler = createMockCompiler();
|
|
476
|
+
const mockUser = {
|
|
477
|
+
id: FragnoId.fromExternal("1", 1),
|
|
478
|
+
email: "test@example.com",
|
|
479
|
+
name: "Test",
|
|
480
|
+
balance: 100,
|
|
481
|
+
};
|
|
482
|
+
const executor: UOWExecutor<unknown, unknown> = {
|
|
483
|
+
executeRetrievalPhase: async () => [[mockUser]],
|
|
484
|
+
executeMutationPhase: async () => ({ success: true, createdInternalIds: [BigInt(2)] }),
|
|
485
|
+
};
|
|
486
|
+
const decoder = createMockDecoder();
|
|
944
487
|
|
|
945
|
-
|
|
946
|
-
const { factory } = createMockUOWFactory([{ success: true }]);
|
|
488
|
+
let currentUow: IUnitOfWork | null = null;
|
|
947
489
|
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
490
|
+
// Service that retrieves
|
|
491
|
+
const getUserById = () => {
|
|
492
|
+
return createServiceTxBuilder(testSchema, currentUow!)
|
|
493
|
+
.retrieve((uow) => uow.find("users", (b) => b.whereIndex("idx_email")))
|
|
494
|
+
.transformRetrieve(([users]) => users[0] ?? null)
|
|
495
|
+
.build();
|
|
496
|
+
};
|
|
952
497
|
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
498
|
+
const result = await createHandlerTxBuilder({
|
|
499
|
+
createUnitOfWork: () => {
|
|
500
|
+
currentUow = createUnitOfWork(compiler, executor, decoder);
|
|
501
|
+
return currentUow;
|
|
502
|
+
},
|
|
503
|
+
})
|
|
504
|
+
.withServiceCalls(() => [getUserById()])
|
|
505
|
+
.mutate(({ forSchema, serviceIntermediateResult: [user] }) => {
|
|
506
|
+
if (!user) {
|
|
507
|
+
return { ok: false as const };
|
|
508
|
+
}
|
|
509
|
+
const newUserId = forSchema(testSchema).create("users", {
|
|
510
|
+
email: "new@example.com",
|
|
511
|
+
name: "New User",
|
|
957
512
|
balance: 0,
|
|
958
513
|
});
|
|
514
|
+
return { ok: true as const, newUserId };
|
|
515
|
+
})
|
|
516
|
+
.execute();
|
|
959
517
|
|
|
960
|
-
|
|
518
|
+
expect(result.ok).toBe(true);
|
|
519
|
+
if (result.ok) {
|
|
520
|
+
expect(result.newUserId).toBeInstanceOf(FragnoId);
|
|
521
|
+
}
|
|
522
|
+
});
|
|
961
523
|
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
524
|
+
it("should execute a transaction with transform callback", async () => {
|
|
525
|
+
const compiler = createMockCompiler();
|
|
526
|
+
const mockUser = {
|
|
527
|
+
id: FragnoId.fromExternal("1", 1),
|
|
528
|
+
email: "test@example.com",
|
|
529
|
+
name: "Test",
|
|
530
|
+
balance: 100,
|
|
531
|
+
};
|
|
532
|
+
const executor: UOWExecutor<unknown, unknown> = {
|
|
533
|
+
executeRetrievalPhase: async () => [[mockUser]],
|
|
534
|
+
executeMutationPhase: async () => ({ success: true, createdInternalIds: [BigInt(2)] }),
|
|
535
|
+
};
|
|
536
|
+
const decoder = createMockDecoder();
|
|
966
537
|
|
|
967
|
-
|
|
968
|
-
expect(result.userId.externalId).toBe("user-123");
|
|
969
|
-
});
|
|
970
|
-
});
|
|
538
|
+
let currentUow: IUnitOfWork | null = null;
|
|
971
539
|
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
540
|
+
// Service that retrieves
|
|
541
|
+
const getUserById = () => {
|
|
542
|
+
return createServiceTxBuilder(testSchema, currentUow!)
|
|
543
|
+
.retrieve((uow) => uow.find("users", (b) => b.whereIndex("idx_email")))
|
|
544
|
+
.transformRetrieve(([users]) => users[0] ?? null)
|
|
545
|
+
.build();
|
|
546
|
+
};
|
|
975
547
|
|
|
976
|
-
const result = await
|
|
977
|
-
|
|
978
|
-
|
|
548
|
+
const result = await createHandlerTxBuilder({
|
|
549
|
+
createUnitOfWork: () => {
|
|
550
|
+
currentUow = createUnitOfWork(compiler, executor, decoder);
|
|
551
|
+
return currentUow;
|
|
552
|
+
},
|
|
553
|
+
})
|
|
554
|
+
.withServiceCalls(() => [getUserById()])
|
|
555
|
+
.mutate(({ forSchema, serviceIntermediateResult: [user] }) => {
|
|
556
|
+
if (!user) {
|
|
557
|
+
return { created: false as const };
|
|
558
|
+
}
|
|
559
|
+
const newUserId = forSchema(testSchema).create("users", {
|
|
560
|
+
email: "new@example.com",
|
|
561
|
+
name: "New User",
|
|
562
|
+
balance: 0,
|
|
563
|
+
});
|
|
564
|
+
return { created: true as const, newUserId };
|
|
565
|
+
})
|
|
566
|
+
.transform(({ serviceResult: [user], mutateResult }) => {
|
|
979
567
|
return {
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
568
|
+
originalUser: user,
|
|
569
|
+
mutationResult: mutateResult,
|
|
570
|
+
summary: "Transaction completed",
|
|
983
571
|
};
|
|
984
|
-
}
|
|
985
|
-
|
|
986
|
-
);
|
|
572
|
+
})
|
|
573
|
+
.execute();
|
|
987
574
|
|
|
988
|
-
expect(result).
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
status: "completed",
|
|
992
|
-
});
|
|
575
|
+
expect(result.summary).toBe("Transaction completed");
|
|
576
|
+
expect(result.originalUser).toEqual(mockUser);
|
|
577
|
+
expect(result.mutationResult?.created).toBe(true);
|
|
993
578
|
});
|
|
994
579
|
|
|
995
|
-
it("should
|
|
996
|
-
const
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
580
|
+
it("should execute a transaction with serviceCalls (service composition)", async () => {
|
|
581
|
+
const compiler = createMockCompiler();
|
|
582
|
+
const mockUser = {
|
|
583
|
+
id: FragnoId.fromExternal("1", 1),
|
|
584
|
+
email: "test@example.com",
|
|
585
|
+
name: "Test",
|
|
586
|
+
balance: 100,
|
|
587
|
+
};
|
|
588
|
+
const executor: UOWExecutor<unknown, unknown> = {
|
|
589
|
+
executeRetrievalPhase: async () => [[mockUser]],
|
|
590
|
+
executeMutationPhase: async () => ({ success: true, createdInternalIds: [BigInt(2)] }),
|
|
591
|
+
};
|
|
592
|
+
const decoder = createMockDecoder();
|
|
1005
593
|
|
|
1006
|
-
|
|
1007
|
-
});
|
|
594
|
+
let currentUow: IUnitOfWork | null = null;
|
|
1008
595
|
|
|
1009
|
-
|
|
1010
|
-
const
|
|
596
|
+
// Simulate a service method that returns a TxResult
|
|
597
|
+
const getUserById = (userId: string) => {
|
|
598
|
+
return createServiceTxBuilder(testSchema, currentUow!)
|
|
599
|
+
.retrieve((uow) =>
|
|
600
|
+
uow.find("users", (b) => b.whereIndex("primary", (eb) => eb("id", "=", userId))),
|
|
601
|
+
)
|
|
602
|
+
.transformRetrieve(([users]) => users[0] ?? null)
|
|
603
|
+
.build();
|
|
604
|
+
};
|
|
1011
605
|
|
|
1012
|
-
const result = await
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
return
|
|
606
|
+
const result = await createHandlerTxBuilder({
|
|
607
|
+
createUnitOfWork: () => {
|
|
608
|
+
currentUow = createUnitOfWork(compiler, executor, decoder);
|
|
609
|
+
return currentUow;
|
|
1016
610
|
},
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
611
|
+
})
|
|
612
|
+
.withServiceCalls(() => [getUserById("1")])
|
|
613
|
+
.mutate(({ forSchema, serviceIntermediateResult: [user] }) => {
|
|
614
|
+
if (!user) {
|
|
615
|
+
return { ok: false as const };
|
|
616
|
+
}
|
|
617
|
+
const orderId = forSchema(testSchema).create("users", {
|
|
618
|
+
email: "order@example.com",
|
|
619
|
+
name: "Order",
|
|
620
|
+
balance: 0,
|
|
621
|
+
});
|
|
622
|
+
return { ok: true as const, orderId, forUser: user.email };
|
|
623
|
+
})
|
|
624
|
+
.execute();
|
|
625
|
+
|
|
626
|
+
expect(result.ok).toBe(true);
|
|
627
|
+
if (result.ok) {
|
|
628
|
+
expect(result.forUser).toBe("test@example.com");
|
|
629
|
+
expect(result.orderId).toBeInstanceOf(FragnoId);
|
|
630
|
+
}
|
|
1021
631
|
});
|
|
1022
632
|
|
|
1023
|
-
it("should
|
|
1024
|
-
const
|
|
633
|
+
it("should type check serviceCalls with undefined (optional service pattern)", async () => {
|
|
634
|
+
const compiler = createMockCompiler();
|
|
635
|
+
const executor: UOWExecutor<unknown, unknown> = {
|
|
636
|
+
executeRetrievalPhase: async () => [],
|
|
637
|
+
executeMutationPhase: async () => ({ success: true, createdInternalIds: [BigInt(1)] }),
|
|
638
|
+
};
|
|
639
|
+
const decoder = createMockDecoder();
|
|
1025
640
|
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
},
|
|
1033
|
-
};
|
|
1034
|
-
},
|
|
1035
|
-
{ createUnitOfWork: factory },
|
|
1036
|
-
);
|
|
641
|
+
// Simulate optional service pattern: optionalService?.method()
|
|
642
|
+
// When optionalService is undefined, this evaluates to undefined
|
|
643
|
+
// Use type assertion to prevent TypeScript from narrowing to literal undefined
|
|
644
|
+
const optionalService = undefined as
|
|
645
|
+
| { getUser: () => TxResult<{ name: string }, { name: string }> }
|
|
646
|
+
| undefined;
|
|
1037
647
|
|
|
1038
|
-
//
|
|
1039
|
-
|
|
648
|
+
// This test demonstrates that serviceCalls can contain TxResult | undefined
|
|
649
|
+
// This is useful for optional service patterns like: optionalService?.method()
|
|
650
|
+
const result = await createHandlerTxBuilder({
|
|
651
|
+
createUnitOfWork: () => createUnitOfWork(compiler, executor, decoder),
|
|
652
|
+
})
|
|
653
|
+
.withServiceCalls(() => [optionalService?.getUser()])
|
|
654
|
+
.mutate(({ forSchema, serviceIntermediateResult: [maybeUser] }) => {
|
|
655
|
+
// maybeUser is typed as { name: string } | undefined
|
|
656
|
+
// This demonstrates the optional chaining pattern works correctly
|
|
657
|
+
expectTypeOf(maybeUser).toEqualTypeOf<{ name: string } | undefined>();
|
|
658
|
+
if (!maybeUser) {
|
|
659
|
+
const userId = forSchema(testSchema).create("users", {
|
|
660
|
+
email: "fallback@example.com",
|
|
661
|
+
name: "Fallback User",
|
|
662
|
+
balance: 0,
|
|
663
|
+
});
|
|
664
|
+
return { hadUser: false as const, userId };
|
|
665
|
+
}
|
|
666
|
+
return { hadUser: true as const, userName: maybeUser.name };
|
|
667
|
+
})
|
|
668
|
+
.execute();
|
|
669
|
+
|
|
670
|
+
// Since optionalService was undefined, maybeUser was undefined, so we hit the fallback path
|
|
671
|
+
expect(result.hadUser).toBe(false);
|
|
672
|
+
if (!result.hadUser) {
|
|
673
|
+
expect(result.userId).toBeInstanceOf(FragnoId);
|
|
674
|
+
}
|
|
1040
675
|
});
|
|
1041
676
|
|
|
1042
|
-
it("should handle
|
|
1043
|
-
const
|
|
677
|
+
it("should handle serviceCalls with mix of TxResult and undefined", async () => {
|
|
678
|
+
const compiler = createMockCompiler();
|
|
679
|
+
const executor: UOWExecutor<unknown, unknown> = {
|
|
680
|
+
executeRetrievalPhase: async () => [],
|
|
681
|
+
executeMutationPhase: async () => ({ success: true, createdInternalIds: [BigInt(1)] }),
|
|
682
|
+
};
|
|
683
|
+
const decoder = createMockDecoder();
|
|
1044
684
|
|
|
1045
|
-
|
|
1046
|
-
async ({ executeMutate }) => {
|
|
1047
|
-
await executeMutate();
|
|
1048
|
-
return {
|
|
1049
|
-
promise: Promise.resolve("resolved"),
|
|
1050
|
-
number: 42,
|
|
1051
|
-
string: "test",
|
|
1052
|
-
boolean: true,
|
|
1053
|
-
null: null,
|
|
1054
|
-
undefined: undefined,
|
|
1055
|
-
object: { nested: "value" },
|
|
1056
|
-
};
|
|
1057
|
-
},
|
|
1058
|
-
{ createUnitOfWork: factory },
|
|
1059
|
-
);
|
|
685
|
+
let currentUow: IUnitOfWork | null = null;
|
|
1060
686
|
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
687
|
+
// Type for the created data returned by the defined service
|
|
688
|
+
type CreatedData = { userId: FragnoId; generatedCode: string };
|
|
689
|
+
// Type for the extra data that would be returned by the optional service
|
|
690
|
+
type ExtraData = { extraCode: string; timestamp: number };
|
|
691
|
+
|
|
692
|
+
// One defined service with mutation that returns data
|
|
693
|
+
const definedService = {
|
|
694
|
+
createUserAndReturnCode: (): TxResult<CreatedData, CreatedData> =>
|
|
695
|
+
createServiceTxBuilder(testSchema, currentUow!)
|
|
696
|
+
.mutate(({ uow }): CreatedData => {
|
|
697
|
+
const userId = uow.create("users", {
|
|
698
|
+
email: "created@example.com",
|
|
699
|
+
name: "Created User",
|
|
700
|
+
balance: 0,
|
|
701
|
+
});
|
|
702
|
+
// Return arbitrary data from the mutation
|
|
703
|
+
return { userId, generatedCode: "ABC123" };
|
|
704
|
+
})
|
|
705
|
+
.build(),
|
|
706
|
+
};
|
|
1071
707
|
|
|
1072
|
-
|
|
1073
|
-
const
|
|
708
|
+
// Optional service that would also return mutation data
|
|
709
|
+
const optionalService = undefined as
|
|
710
|
+
| {
|
|
711
|
+
generateExtra: () => TxResult<ExtraData, ExtraData>;
|
|
712
|
+
}
|
|
713
|
+
| undefined;
|
|
714
|
+
|
|
715
|
+
const result = await createHandlerTxBuilder({
|
|
716
|
+
createUnitOfWork: () => {
|
|
717
|
+
currentUow = createUnitOfWork(compiler, executor, decoder);
|
|
718
|
+
return currentUow;
|
|
719
|
+
},
|
|
720
|
+
})
|
|
721
|
+
.withServiceCalls(
|
|
722
|
+
() =>
|
|
723
|
+
[definedService.createUserAndReturnCode(), optionalService?.generateExtra()] as const,
|
|
724
|
+
)
|
|
725
|
+
.mutate(({ forSchema, serviceIntermediateResult }) => {
|
|
726
|
+
// serviceIntermediateResult contains the mutation results from service calls
|
|
727
|
+
// (since service calls have no retrieveSuccess, the mutate result becomes the retrieve result for dependents)
|
|
728
|
+
const [createdData, maybeExtra] = serviceIntermediateResult;
|
|
729
|
+
|
|
730
|
+
// Type checks: createdData should have userId and generatedCode
|
|
731
|
+
// maybeExtra should be ExtraData | undefined
|
|
732
|
+
expectTypeOf(createdData).toEqualTypeOf<CreatedData>();
|
|
733
|
+
expectTypeOf(maybeExtra).toEqualTypeOf<ExtraData | undefined>();
|
|
734
|
+
|
|
735
|
+
forSchema(testSchema).create("users", {
|
|
736
|
+
email: "handler@example.com",
|
|
737
|
+
name: "Handler User",
|
|
738
|
+
balance: 0,
|
|
739
|
+
});
|
|
1074
740
|
|
|
1075
|
-
const result = await executeRestrictedUnitOfWork(
|
|
1076
|
-
async ({ executeMutate }) => {
|
|
1077
|
-
await executeMutate();
|
|
1078
741
|
return {
|
|
1079
|
-
|
|
1080
|
-
|
|
742
|
+
depCode: createdData.generatedCode,
|
|
743
|
+
hadExtra: maybeExtra !== undefined,
|
|
1081
744
|
};
|
|
1082
|
-
}
|
|
1083
|
-
{
|
|
1084
|
-
|
|
745
|
+
})
|
|
746
|
+
.transform(({ serviceResult, serviceIntermediateResult, mutateResult }) => {
|
|
747
|
+
// Verify serviceResult types - these are the FINAL results from serviceCalls
|
|
748
|
+
const [finalCreatedData, maybeFinalExtra] = serviceResult;
|
|
1085
749
|
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
});
|
|
750
|
+
// Type check: serviceResult should have same structure
|
|
751
|
+
// The final result is CreatedData (since there's no transform callback on the dep)
|
|
752
|
+
expectTypeOf(finalCreatedData).toEqualTypeOf<CreatedData>();
|
|
753
|
+
expectTypeOf(maybeFinalExtra).toEqualTypeOf<ExtraData | undefined>();
|
|
1091
754
|
|
|
1092
|
-
|
|
1093
|
-
|
|
755
|
+
// serviceIntermediateResult should still be accessible in transform
|
|
756
|
+
const [_retrieveData, maybeRetrieveExtra] = serviceIntermediateResult;
|
|
757
|
+
expectTypeOf(maybeRetrieveExtra).toEqualTypeOf<ExtraData | undefined>();
|
|
1094
758
|
|
|
1095
|
-
const result = await executeRestrictedUnitOfWork(
|
|
1096
|
-
async ({ executeMutate }) => {
|
|
1097
|
-
await executeMutate();
|
|
1098
759
|
return {
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
metadata: {
|
|
1104
|
-
timestamp: Date.now(),
|
|
1105
|
-
version: 1,
|
|
1106
|
-
},
|
|
760
|
+
...mutateResult,
|
|
761
|
+
finalDepUserId: finalCreatedData.userId,
|
|
762
|
+
finalDepCode: finalCreatedData.generatedCode,
|
|
763
|
+
extraWasUndefined: maybeFinalExtra === undefined,
|
|
1107
764
|
};
|
|
1108
|
-
}
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
expect(
|
|
1113
|
-
expect(result.
|
|
1114
|
-
expect(
|
|
1115
|
-
expect(result.
|
|
1116
|
-
expect(
|
|
1117
|
-
expect(result.count).toBe(100);
|
|
1118
|
-
expect(typeof result.active).toBe("boolean");
|
|
1119
|
-
expect(result.active).toBe(true);
|
|
1120
|
-
expect(typeof result.metadata.timestamp).toBe("number");
|
|
1121
|
-
expect(result.metadata.version).toBe(1);
|
|
765
|
+
})
|
|
766
|
+
.execute();
|
|
767
|
+
|
|
768
|
+
// Verify runtime behavior
|
|
769
|
+
expect(result.depCode).toBe("ABC123");
|
|
770
|
+
expect(result.hadExtra).toBe(false);
|
|
771
|
+
expect(result.finalDepCode).toBe("ABC123");
|
|
772
|
+
expect(result.extraWasUndefined).toBe(true);
|
|
773
|
+
expect(result.finalDepUserId).toBeInstanceOf(FragnoId);
|
|
1122
774
|
});
|
|
1123
775
|
|
|
1124
|
-
it("should
|
|
1125
|
-
const
|
|
1126
|
-
|
|
1127
|
-
const
|
|
1128
|
-
async (
|
|
1129
|
-
|
|
1130
|
-
|
|
776
|
+
it("should retry on concurrency conflict", async () => {
|
|
777
|
+
const compiler = createMockCompiler();
|
|
778
|
+
let mutationAttempts = 0;
|
|
779
|
+
const executor: UOWExecutor<unknown, unknown> = {
|
|
780
|
+
executeRetrievalPhase: async () => [],
|
|
781
|
+
executeMutationPhase: async () => {
|
|
782
|
+
mutationAttempts++;
|
|
783
|
+
if (mutationAttempts < 3) {
|
|
784
|
+
return { success: false };
|
|
785
|
+
}
|
|
786
|
+
return { success: true, createdInternalIds: [] };
|
|
1131
787
|
},
|
|
1132
|
-
|
|
1133
|
-
);
|
|
788
|
+
};
|
|
789
|
+
const decoder = createMockDecoder();
|
|
1134
790
|
|
|
1135
|
-
|
|
791
|
+
const result = await createHandlerTxBuilder({
|
|
792
|
+
createUnitOfWork: () => createUnitOfWork(compiler, executor, decoder),
|
|
793
|
+
retryPolicy: new ExponentialBackoffRetryPolicy({ maxRetries: 5, initialDelayMs: 1 }),
|
|
794
|
+
})
|
|
795
|
+
.retrieve(({ forSchema }) => forSchema(testSchema).find("users"))
|
|
796
|
+
.mutate(({ forSchema }) => {
|
|
797
|
+
forSchema(testSchema).create("users", {
|
|
798
|
+
email: "test@example.com",
|
|
799
|
+
name: "Test",
|
|
800
|
+
balance: 0,
|
|
801
|
+
});
|
|
802
|
+
return { createdAt: Date.now() };
|
|
803
|
+
})
|
|
804
|
+
.execute();
|
|
805
|
+
|
|
806
|
+
// Verify we retried the correct number of times
|
|
807
|
+
expect(mutationAttempts).toBe(3);
|
|
808
|
+
// Verify we got a result
|
|
809
|
+
expect(result.createdAt).toBeGreaterThan(0);
|
|
1136
810
|
});
|
|
1137
811
|
|
|
1138
|
-
it("should
|
|
1139
|
-
const
|
|
812
|
+
it("should throw ConcurrencyConflictError when retries are exhausted", async () => {
|
|
813
|
+
const compiler = createMockCompiler();
|
|
814
|
+
const executor: UOWExecutor<unknown, unknown> = {
|
|
815
|
+
executeRetrievalPhase: async () => [],
|
|
816
|
+
executeMutationPhase: async () => ({ success: false }),
|
|
817
|
+
};
|
|
818
|
+
const decoder = createMockDecoder();
|
|
1140
819
|
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
}
|
|
1146
|
-
|
|
1147
|
-
|
|
820
|
+
await expect(
|
|
821
|
+
createHandlerTxBuilder({
|
|
822
|
+
createUnitOfWork: () => createUnitOfWork(compiler, executor, decoder),
|
|
823
|
+
retryPolicy: new NoRetryPolicy(),
|
|
824
|
+
})
|
|
825
|
+
.retrieve(({ forSchema }) => forSchema(testSchema).find("users"))
|
|
826
|
+
.mutate(() => ({ done: true }))
|
|
827
|
+
.execute(),
|
|
828
|
+
).rejects.toThrow(ConcurrencyConflictError);
|
|
829
|
+
});
|
|
1148
830
|
|
|
1149
|
-
|
|
831
|
+
it("should abort when signal is aborted", async () => {
|
|
832
|
+
const compiler = createMockCompiler();
|
|
833
|
+
const executor: UOWExecutor<unknown, unknown> = {
|
|
834
|
+
executeRetrievalPhase: async () => [],
|
|
835
|
+
executeMutationPhase: async () => ({ success: true, createdInternalIds: [] }),
|
|
836
|
+
};
|
|
837
|
+
const decoder = createMockDecoder();
|
|
1150
838
|
|
|
1151
|
-
const
|
|
1152
|
-
|
|
1153
|
-
async ({ executeMutate }) => {
|
|
1154
|
-
await executeMutate();
|
|
1155
|
-
return 42;
|
|
1156
|
-
},
|
|
1157
|
-
{ createUnitOfWork: factory2 },
|
|
1158
|
-
);
|
|
839
|
+
const controller = new AbortController();
|
|
840
|
+
controller.abort();
|
|
1159
841
|
|
|
1160
|
-
expect(
|
|
842
|
+
await expect(
|
|
843
|
+
createHandlerTxBuilder({
|
|
844
|
+
createUnitOfWork: () => createUnitOfWork(compiler, executor, decoder),
|
|
845
|
+
signal: controller.signal,
|
|
846
|
+
})
|
|
847
|
+
.mutate(() => ({ done: true }))
|
|
848
|
+
.execute(),
|
|
849
|
+
).rejects.toThrow("Transaction execution aborted");
|
|
1161
850
|
});
|
|
1162
|
-
});
|
|
1163
851
|
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
852
|
+
it("should create fresh UOW on each retry attempt", async () => {
|
|
853
|
+
const compiler = createMockCompiler();
|
|
854
|
+
let callCount = 0;
|
|
855
|
+
let mutationAttempts = 0;
|
|
856
|
+
const executor: UOWExecutor<unknown, unknown> = {
|
|
857
|
+
executeRetrievalPhase: async () => [],
|
|
858
|
+
executeMutationPhase: async () => {
|
|
859
|
+
mutationAttempts++;
|
|
860
|
+
if (mutationAttempts < 3) {
|
|
861
|
+
return { success: false };
|
|
862
|
+
}
|
|
863
|
+
return { success: true, createdInternalIds: [] };
|
|
864
|
+
},
|
|
865
|
+
};
|
|
866
|
+
const decoder = createMockDecoder();
|
|
1167
867
|
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
return [Promise.resolve("user-123"), Promise.resolve(42)] as const;
|
|
868
|
+
await createHandlerTxBuilder({
|
|
869
|
+
createUnitOfWork: () => {
|
|
870
|
+
callCount++;
|
|
871
|
+
return createUnitOfWork(compiler, executor, decoder);
|
|
1173
872
|
},
|
|
1174
|
-
{
|
|
1175
|
-
)
|
|
873
|
+
retryPolicy: new LinearBackoffRetryPolicy({ maxRetries: 3, delayMs: 1 }),
|
|
874
|
+
})
|
|
875
|
+
.retrieve(({ forSchema }) => forSchema(testSchema).find("users"))
|
|
876
|
+
.mutate(({ forSchema }) => {
|
|
877
|
+
forSchema(testSchema).create("users", {
|
|
878
|
+
email: "test@example.com",
|
|
879
|
+
name: "Test",
|
|
880
|
+
balance: 0,
|
|
881
|
+
});
|
|
882
|
+
})
|
|
883
|
+
.execute();
|
|
1176
884
|
|
|
1177
|
-
//
|
|
1178
|
-
expect(
|
|
1179
|
-
expect(result[0]).toBe("user-123");
|
|
1180
|
-
expect(result[1]).toBe(42);
|
|
885
|
+
// Verify factory was called for each attempt (initial + 2 retries)
|
|
886
|
+
expect(callCount).toBe(3);
|
|
1181
887
|
});
|
|
1182
888
|
|
|
1183
|
-
it("should
|
|
1184
|
-
const
|
|
889
|
+
it("should abort when signal is aborted during retry delay", async () => {
|
|
890
|
+
const compiler = createMockCompiler();
|
|
891
|
+
const executor: UOWExecutor<unknown, unknown> = {
|
|
892
|
+
executeRetrievalPhase: async () => [],
|
|
893
|
+
executeMutationPhase: async () => ({ success: false }),
|
|
894
|
+
};
|
|
895
|
+
const decoder = createMockDecoder();
|
|
896
|
+
|
|
897
|
+
const controller = new AbortController();
|
|
1185
898
|
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
await executeMutate();
|
|
1189
|
-
// Tuple with mixed types
|
|
1190
|
-
return [Promise.resolve("first"), "second", Promise.resolve(3)] as const;
|
|
1191
|
-
},
|
|
1192
|
-
{ createUnitOfWork: factory },
|
|
1193
|
-
);
|
|
899
|
+
// Abort after first attempt during retry delay
|
|
900
|
+
setTimeout(() => controller.abort(), 50);
|
|
1194
901
|
|
|
1195
|
-
expect(
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
902
|
+
await expect(
|
|
903
|
+
createHandlerTxBuilder({
|
|
904
|
+
createUnitOfWork: () => createUnitOfWork(compiler, executor, decoder),
|
|
905
|
+
retryPolicy: new LinearBackoffRetryPolicy({ maxRetries: 5, delayMs: 100 }),
|
|
906
|
+
signal: controller.signal,
|
|
907
|
+
})
|
|
908
|
+
.retrieve(({ forSchema }) => forSchema(testSchema).find("users"))
|
|
909
|
+
.mutate(() => ({ done: true }))
|
|
910
|
+
.execute(),
|
|
911
|
+
).rejects.toThrow("Transaction execution aborted");
|
|
1199
912
|
});
|
|
1200
913
|
|
|
1201
|
-
it("should
|
|
1202
|
-
const
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
},
|
|
1215
|
-
{ createUnitOfWork: factory },
|
|
1216
|
-
);
|
|
914
|
+
it("should pass serviceResult to transform callback with final results", async () => {
|
|
915
|
+
const compiler = createMockCompiler();
|
|
916
|
+
const mockUser = {
|
|
917
|
+
id: FragnoId.fromExternal("1", 1),
|
|
918
|
+
email: "test@example.com",
|
|
919
|
+
name: "Test",
|
|
920
|
+
balance: 100,
|
|
921
|
+
};
|
|
922
|
+
const executor: UOWExecutor<unknown, unknown> = {
|
|
923
|
+
executeRetrievalPhase: async () => [[mockUser]],
|
|
924
|
+
executeMutationPhase: async () => ({ success: true, createdInternalIds: [BigInt(2)] }),
|
|
925
|
+
};
|
|
926
|
+
const decoder = createMockDecoder();
|
|
1217
927
|
|
|
1218
|
-
|
|
1219
|
-
expect(result).toHaveLength(2);
|
|
1220
|
-
expect(result[0]).toEqual({ id: "user-1", name: "John" });
|
|
1221
|
-
expect(result[1]).toEqual([
|
|
1222
|
-
{ id: "order-1", total: 100 },
|
|
1223
|
-
{ id: "order-2", total: 200 },
|
|
1224
|
-
]);
|
|
1225
|
-
|
|
1226
|
-
// Type check: result should be [{ id: string; name: string }, { id: string; total: number }[]]
|
|
1227
|
-
// But with current implementation, it's incorrectly typed as an array union
|
|
1228
|
-
const [user, orders] = result;
|
|
1229
|
-
expect(user).toBeDefined();
|
|
1230
|
-
expect(orders).toBeDefined();
|
|
1231
|
-
});
|
|
928
|
+
let currentUow: IUnitOfWork | null = null;
|
|
1232
929
|
|
|
1233
|
-
|
|
1234
|
-
const
|
|
930
|
+
// Service that retrieves and mutates
|
|
931
|
+
const getUserAndUpdateBalance = (userId: string) => {
|
|
932
|
+
return createServiceTxBuilder(testSchema, currentUow!)
|
|
933
|
+
.retrieve((uow) =>
|
|
934
|
+
uow.find("users", (b) => b.whereIndex("primary", (eb) => eb("id", "=", userId))),
|
|
935
|
+
)
|
|
936
|
+
.transformRetrieve(([users]) => users[0] ?? null)
|
|
937
|
+
.mutate(({ uow, retrieveResult: user }) => {
|
|
938
|
+
expect(user).toEqual(mockUser);
|
|
939
|
+
expectTypeOf(user).toEqualTypeOf<typeof mockUser>();
|
|
940
|
+
if (!user) {
|
|
941
|
+
return { updated: false as const };
|
|
942
|
+
}
|
|
943
|
+
uow.update("users", user.id, (b) => b.set({ balance: user.balance + 100 }).check());
|
|
944
|
+
return { updated: true as const, newBalance: user.balance + 100 };
|
|
945
|
+
})
|
|
946
|
+
.build();
|
|
947
|
+
};
|
|
1235
948
|
|
|
1236
|
-
const result = await
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
const items = [Promise.resolve(1), Promise.resolve(2), Promise.resolve(3)];
|
|
1241
|
-
return items;
|
|
949
|
+
const result = await createHandlerTxBuilder({
|
|
950
|
+
createUnitOfWork: () => {
|
|
951
|
+
currentUow = createUnitOfWork(compiler, executor, decoder);
|
|
952
|
+
return currentUow;
|
|
1242
953
|
},
|
|
1243
|
-
|
|
1244
|
-
|
|
954
|
+
})
|
|
955
|
+
.withServiceCalls(() => [getUserAndUpdateBalance("1")])
|
|
956
|
+
.transform(
|
|
957
|
+
({ serviceResult: [depResult], serviceIntermediateResult: [depRetrieveResult] }) => {
|
|
958
|
+
expect(depResult).toEqual({
|
|
959
|
+
updated: true,
|
|
960
|
+
newBalance: 200,
|
|
961
|
+
});
|
|
962
|
+
|
|
963
|
+
expect(depRetrieveResult).toEqual(mockUser);
|
|
964
|
+
|
|
965
|
+
const dep = depResult;
|
|
966
|
+
return {
|
|
967
|
+
depUpdated: dep.updated,
|
|
968
|
+
depNewBalance: dep.updated ? dep.newBalance : null,
|
|
969
|
+
};
|
|
970
|
+
},
|
|
971
|
+
)
|
|
972
|
+
.execute();
|
|
1245
973
|
|
|
1246
|
-
expect(result).
|
|
1247
|
-
expect(result).
|
|
974
|
+
expect(result.depUpdated).toBe(true);
|
|
975
|
+
expect(result.depNewBalance).toBe(200);
|
|
1248
976
|
});
|
|
1249
977
|
});
|
|
1250
978
|
|
|
1251
|
-
describe("
|
|
1252
|
-
it("should
|
|
1253
|
-
const
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
.addColumn("id", idColumn())
|
|
1257
|
-
.addColumn("key", "string")
|
|
1258
|
-
.addColumn("value", "string")
|
|
1259
|
-
.createIndex("unique_key", ["key"], { unique: true }),
|
|
1260
|
-
),
|
|
1261
|
-
);
|
|
1262
|
-
|
|
1263
|
-
// Create executor that throws "table does not exist" error
|
|
1264
|
-
const failingExecutor: UOWExecutor<unknown, unknown> = {
|
|
1265
|
-
executeRetrievalPhase: async () => {
|
|
1266
|
-
throw new Error('relation "settings" does not exist');
|
|
1267
|
-
},
|
|
979
|
+
describe("return type priority", () => {
|
|
980
|
+
it("should return transform result when transform callback is provided", async () => {
|
|
981
|
+
const compiler = createMockCompiler();
|
|
982
|
+
const executor: UOWExecutor<unknown, unknown> = {
|
|
983
|
+
executeRetrievalPhase: async () => [],
|
|
1268
984
|
executeMutationPhase: async () => ({ success: true, createdInternalIds: [] }),
|
|
1269
985
|
};
|
|
986
|
+
const decoder = createMockDecoder();
|
|
1270
987
|
|
|
1271
|
-
const
|
|
1272
|
-
createUnitOfWork(
|
|
1273
|
-
|
|
1274
|
-
|
|
988
|
+
const result = await createHandlerTxBuilder({
|
|
989
|
+
createUnitOfWork: () => createUnitOfWork(compiler, executor, decoder),
|
|
990
|
+
})
|
|
991
|
+
.retrieve(() => {
|
|
992
|
+
// Empty retrieve - defaults to empty array
|
|
993
|
+
})
|
|
994
|
+
.transformRetrieve((ctx) => {
|
|
995
|
+
expectTypeOf(ctx).toEqualTypeOf<unknown[]>();
|
|
996
|
+
|
|
997
|
+
return "retrieveSuccess result" as const;
|
|
998
|
+
})
|
|
999
|
+
.mutate((ctx) => {
|
|
1000
|
+
expectTypeOf(ctx.retrieveResult).toEqualTypeOf<"retrieveSuccess result">();
|
|
1001
|
+
|
|
1002
|
+
return "mutate result" as const;
|
|
1003
|
+
})
|
|
1004
|
+
.transform((ctx) => {
|
|
1005
|
+
expectTypeOf(ctx.retrieveResult).toEqualTypeOf<"retrieveSuccess result">();
|
|
1006
|
+
// mutateResult is NOT | undefined because mutate callback IS provided
|
|
1007
|
+
expectTypeOf(ctx.mutateResult).toEqualTypeOf<"mutate result">();
|
|
1008
|
+
// serviceResult and serviceIntermediateResult are empty tuples since no service calls callback
|
|
1009
|
+
expectTypeOf(ctx.serviceResult).toEqualTypeOf<readonly []>();
|
|
1010
|
+
expectTypeOf(ctx.serviceIntermediateResult).toEqualTypeOf<readonly []>();
|
|
1011
|
+
|
|
1012
|
+
return "success result" as const;
|
|
1013
|
+
})
|
|
1014
|
+
.execute();
|
|
1015
|
+
|
|
1016
|
+
expect(result).toBe("success result");
|
|
1017
|
+
});
|
|
1275
1018
|
|
|
1276
|
-
|
|
1277
|
-
const
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
)
|
|
1281
|
-
const [results] = await uow.retrievalPhase;
|
|
1282
|
-
return results?.[0];
|
|
1019
|
+
it("should return mutate result when no transform callback", async () => {
|
|
1020
|
+
const compiler = createMockCompiler();
|
|
1021
|
+
const executor: UOWExecutor<unknown, unknown> = {
|
|
1022
|
+
executeRetrievalPhase: async () => [],
|
|
1023
|
+
executeMutationPhase: async () => ({ success: true, createdInternalIds: [] }),
|
|
1283
1024
|
};
|
|
1025
|
+
const decoder = createMockDecoder();
|
|
1284
1026
|
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1027
|
+
const result = await createHandlerTxBuilder({
|
|
1028
|
+
createUnitOfWork: () => createUnitOfWork(compiler, executor, decoder),
|
|
1029
|
+
})
|
|
1030
|
+
.transformRetrieve(() => "retrieveSuccess result")
|
|
1031
|
+
.mutate(() => "mutate result")
|
|
1032
|
+
.execute();
|
|
1290
1033
|
|
|
1291
|
-
|
|
1034
|
+
expect(result).toBe("mutate result");
|
|
1035
|
+
});
|
|
1292
1036
|
|
|
1293
|
-
|
|
1294
|
-
|
|
1037
|
+
it("should return transformRetrieve result when no mutate or transform", async () => {
|
|
1038
|
+
const compiler = createMockCompiler();
|
|
1039
|
+
const executor: UOWExecutor<unknown, unknown> = {
|
|
1040
|
+
executeRetrievalPhase: async () => [],
|
|
1041
|
+
executeMutationPhase: async () => ({ success: true, createdInternalIds: [] }),
|
|
1042
|
+
};
|
|
1043
|
+
const decoder = createMockDecoder();
|
|
1295
1044
|
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
retryPolicy: new NoRetryPolicy(),
|
|
1302
|
-
},
|
|
1303
|
-
);
|
|
1304
|
-
expect.fail("Should have thrown an error");
|
|
1305
|
-
} catch (error) {
|
|
1306
|
-
// The error should be thrown directly (not wrapped) since it's not a concurrency conflict
|
|
1307
|
-
expect(error).toBeInstanceOf(Error);
|
|
1308
|
-
expect((error as Error).message).toContain('relation "settings" does not exist');
|
|
1309
|
-
deferred.resolve((error as Error).message);
|
|
1310
|
-
}
|
|
1045
|
+
const result = await createHandlerTxBuilder({
|
|
1046
|
+
createUnitOfWork: () => createUnitOfWork(compiler, executor, decoder),
|
|
1047
|
+
})
|
|
1048
|
+
.transformRetrieve(() => "retrieveSuccess result")
|
|
1049
|
+
.execute();
|
|
1311
1050
|
|
|
1312
|
-
|
|
1313
|
-
// If the test completes without throwing, the promise rejection was properly handled
|
|
1314
|
-
expect(await deferred.promise).toContain('relation "settings" does not exist');
|
|
1051
|
+
expect(result).toBe("retrieveSuccess result");
|
|
1315
1052
|
});
|
|
1316
|
-
});
|
|
1317
1053
|
|
|
1318
|
-
|
|
1319
|
-
it("should execute multiple service promises and await them before mutations", async () => {
|
|
1054
|
+
it("should return serviceCalls final results when no local callbacks", async () => {
|
|
1320
1055
|
const compiler = createMockCompiler();
|
|
1056
|
+
const mockUser = {
|
|
1057
|
+
id: FragnoId.fromExternal("1", 1),
|
|
1058
|
+
email: "test@example.com",
|
|
1059
|
+
name: "Test",
|
|
1060
|
+
balance: 100,
|
|
1061
|
+
};
|
|
1321
1062
|
const executor: UOWExecutor<unknown, unknown> = {
|
|
1322
|
-
executeRetrievalPhase: async () => [],
|
|
1323
|
-
executeMutationPhase: async () => ({
|
|
1324
|
-
success: true,
|
|
1325
|
-
createdInternalIds: [],
|
|
1326
|
-
}),
|
|
1063
|
+
executeRetrievalPhase: async () => [[mockUser]],
|
|
1064
|
+
executeMutationPhase: async () => ({ success: true, createdInternalIds: [] }),
|
|
1327
1065
|
};
|
|
1328
1066
|
const decoder = createMockDecoder();
|
|
1329
1067
|
|
|
1330
|
-
let
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
const
|
|
1334
|
-
()
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
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
|
-
},
|
|
1068
|
+
let currentUow: IUnitOfWork | null = null;
|
|
1069
|
+
|
|
1070
|
+
// Service that just retrieves
|
|
1071
|
+
const getUser = () => {
|
|
1072
|
+
return createServiceTxBuilder(testSchema, currentUow!)
|
|
1073
|
+
.retrieve((uow) => uow.find("users", (b) => b.whereIndex("idx_email")))
|
|
1074
|
+
.transformRetrieve(([users]) => users[0] ?? null)
|
|
1075
|
+
.build();
|
|
1076
|
+
};
|
|
1077
|
+
|
|
1078
|
+
// executeTx with only serviceCalls - should return serviceCalls' final results
|
|
1079
|
+
const result = await createHandlerTxBuilder({
|
|
1080
|
+
createUnitOfWork: () => {
|
|
1081
|
+
currentUow = createUnitOfWork(compiler, executor, decoder);
|
|
1082
|
+
return currentUow;
|
|
1355
1083
|
},
|
|
1356
|
-
)
|
|
1084
|
+
})
|
|
1085
|
+
.withServiceCalls(() => [getUser()])
|
|
1086
|
+
.execute();
|
|
1357
1087
|
|
|
1358
|
-
expect(
|
|
1359
|
-
expect(servicesResolved).toBe(true);
|
|
1360
|
-
expect(result).toEqual([{ result1: "value1" }, { result2: "value2" }]);
|
|
1088
|
+
expect(result).toEqual([mockUser]);
|
|
1361
1089
|
});
|
|
1090
|
+
});
|
|
1362
1091
|
|
|
1363
|
-
|
|
1092
|
+
describe("serviceResult vs serviceIntermediateResult", () => {
|
|
1093
|
+
it("serviceIntermediateResult in mutate should contain transformRetrieve results", async () => {
|
|
1364
1094
|
const compiler = createMockCompiler();
|
|
1365
|
-
|
|
1095
|
+
const mockUser = {
|
|
1096
|
+
id: FragnoId.fromExternal("1", 1),
|
|
1097
|
+
email: "test@example.com",
|
|
1098
|
+
name: "Test",
|
|
1099
|
+
balance: 100,
|
|
1100
|
+
};
|
|
1366
1101
|
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
|
-
},
|
|
1102
|
+
executeRetrievalPhase: async () => [[mockUser]],
|
|
1103
|
+
executeMutationPhase: async () => ({ success: true, createdInternalIds: [] }),
|
|
1378
1104
|
};
|
|
1379
1105
|
const decoder = createMockDecoder();
|
|
1380
1106
|
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1107
|
+
let currentUow: IUnitOfWork | null = null;
|
|
1108
|
+
let capturedServiceIntermediateResult: unknown[] = [];
|
|
1109
|
+
|
|
1110
|
+
const getUserById = () => {
|
|
1111
|
+
return (
|
|
1112
|
+
createServiceTxBuilder(testSchema, currentUow!)
|
|
1113
|
+
.retrieve((uow) => uow.find("users", (b) => b.whereIndex("idx_email")))
|
|
1114
|
+
// This transformRetrieve transforms the result
|
|
1115
|
+
.transformRetrieve(([users]) => ({ transformed: true, user: users[0] }))
|
|
1116
|
+
.build()
|
|
1117
|
+
);
|
|
1118
|
+
};
|
|
1119
|
+
|
|
1120
|
+
await createHandlerTxBuilder({
|
|
1121
|
+
createUnitOfWork: () => {
|
|
1122
|
+
currentUow = createUnitOfWork(compiler, executor, decoder);
|
|
1123
|
+
return currentUow;
|
|
1124
|
+
},
|
|
1125
|
+
})
|
|
1126
|
+
.withServiceCalls(() => [getUserById()])
|
|
1127
|
+
.mutate(({ serviceIntermediateResult }) => {
|
|
1128
|
+
// Should receive the transformed (transformRetrieve) result
|
|
1129
|
+
capturedServiceIntermediateResult = [...serviceIntermediateResult];
|
|
1130
|
+
return { done: true };
|
|
1131
|
+
})
|
|
1132
|
+
.execute();
|
|
1385
1133
|
|
|
1386
|
-
expect(
|
|
1387
|
-
expect(result).toEqual([{ result: "value" }]);
|
|
1134
|
+
expect(capturedServiceIntermediateResult[0]).toEqual({ transformed: true, user: mockUser });
|
|
1388
1135
|
});
|
|
1389
1136
|
|
|
1390
|
-
it("should
|
|
1137
|
+
it("serviceResult in transform should contain final (mutate) results", async () => {
|
|
1391
1138
|
const compiler = createMockCompiler();
|
|
1139
|
+
const mockUser = {
|
|
1140
|
+
id: FragnoId.fromExternal("1", 1),
|
|
1141
|
+
email: "test@example.com",
|
|
1142
|
+
name: "Test",
|
|
1143
|
+
balance: 100,
|
|
1144
|
+
};
|
|
1392
1145
|
const executor: UOWExecutor<unknown, unknown> = {
|
|
1393
|
-
executeRetrievalPhase: async () => [],
|
|
1394
|
-
executeMutationPhase: async () => ({ success:
|
|
1146
|
+
executeRetrievalPhase: async () => [[mockUser]],
|
|
1147
|
+
executeMutationPhase: async () => ({ success: true, createdInternalIds: [] }),
|
|
1395
1148
|
};
|
|
1396
1149
|
const decoder = createMockDecoder();
|
|
1397
1150
|
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1151
|
+
let currentUow: IUnitOfWork | null = null;
|
|
1152
|
+
let capturedServiceResult: unknown[] = [];
|
|
1153
|
+
|
|
1154
|
+
const getUserById = () => {
|
|
1155
|
+
return (
|
|
1156
|
+
createServiceTxBuilder(testSchema, currentUow!)
|
|
1157
|
+
.retrieve((uow) => uow.find("users", (b) => b.whereIndex("idx_email")))
|
|
1158
|
+
.transformRetrieve(([users]) => users[0])
|
|
1159
|
+
// This mutate returns a different result
|
|
1160
|
+
.mutate(({ retrieveResult: user }) => ({ mutated: true, userId: user?.id }))
|
|
1161
|
+
.build()
|
|
1162
|
+
);
|
|
1163
|
+
};
|
|
1164
|
+
|
|
1165
|
+
await createHandlerTxBuilder({
|
|
1166
|
+
createUnitOfWork: () => {
|
|
1167
|
+
currentUow = createUnitOfWork(compiler, executor, decoder);
|
|
1168
|
+
return currentUow;
|
|
1169
|
+
},
|
|
1170
|
+
})
|
|
1171
|
+
.withServiceCalls(() => [getUserById()])
|
|
1172
|
+
.transform(({ serviceResult }) => {
|
|
1173
|
+
// Should receive the mutate result (final), not transformRetrieve result
|
|
1174
|
+
capturedServiceResult = [...serviceResult];
|
|
1175
|
+
return { done: true };
|
|
1176
|
+
})
|
|
1177
|
+
.execute();
|
|
1178
|
+
|
|
1179
|
+
expect(capturedServiceResult[0]).toEqual({ mutated: true, userId: mockUser.id });
|
|
1404
1180
|
});
|
|
1405
|
-
});
|
|
1406
1181
|
|
|
1407
|
-
|
|
1408
|
-
|
|
1182
|
+
it("serviceResult in transform should contain mutateResult for mutate-only serviceCalls (via processTxResultAfterMutate)", async () => {
|
|
1183
|
+
// This test exercises the buggy code path in processTxResultAfterMutate:
|
|
1184
|
+
// When a nested TxResult has a transform callback and its serviceCall is mutate-only,
|
|
1185
|
+
// the transform callback should receive the mutateResult in
|
|
1186
|
+
// serviceResult, NOT the empty array.
|
|
1187
|
+
//
|
|
1188
|
+
// The key is that the nested service itself (wrapperService) has a transform callback,
|
|
1189
|
+
// so processTxResultAfterMutate is called for it.
|
|
1409
1190
|
const compiler = createMockCompiler();
|
|
1410
1191
|
const executor: UOWExecutor<unknown, unknown> = {
|
|
1411
1192
|
executeRetrievalPhase: async () => [],
|
|
1412
1193
|
executeMutationPhase: async () => ({
|
|
1413
1194
|
success: true,
|
|
1414
|
-
createdInternalIds: [],
|
|
1195
|
+
createdInternalIds: [BigInt(1)],
|
|
1415
1196
|
}),
|
|
1416
1197
|
};
|
|
1417
1198
|
const decoder = createMockDecoder();
|
|
1418
1199
|
|
|
1419
|
-
|
|
1200
|
+
let currentUow: IUnitOfWork | null = null;
|
|
1201
|
+
let capturedServiceIntermediateResultInNestedTransform: unknown[] = [];
|
|
1202
|
+
|
|
1203
|
+
// Mutate-only service - no retrieve or transformRetrieve callbacks
|
|
1204
|
+
const createItem = () => {
|
|
1205
|
+
return (
|
|
1206
|
+
createServiceTxBuilder(testSchema, currentUow!)
|
|
1207
|
+
// NO retrieve or transformRetrieve - this is a mutate-only dep
|
|
1208
|
+
.mutate(({ uow }) => {
|
|
1209
|
+
const itemId = uow.create("users", {
|
|
1210
|
+
email: "new-item@example.com",
|
|
1211
|
+
name: "New Item",
|
|
1212
|
+
balance: 0,
|
|
1213
|
+
});
|
|
1214
|
+
return { created: true, itemId };
|
|
1215
|
+
})
|
|
1216
|
+
.build()
|
|
1217
|
+
);
|
|
1218
|
+
};
|
|
1420
1219
|
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1220
|
+
// Wrapper service that has a transform callback and uses createItem as a dep
|
|
1221
|
+
// This forces processTxResultAfterMutate to be called for this TxResult
|
|
1222
|
+
const wrapperService = () => {
|
|
1223
|
+
return (
|
|
1224
|
+
createServiceTxBuilder(testSchema, currentUow!)
|
|
1225
|
+
.withServiceCalls(() => [createItem()] as const)
|
|
1226
|
+
// NO mutate callback - just pass through
|
|
1227
|
+
// The transform callback is the key: it makes processTxResultAfterMutate get called
|
|
1228
|
+
.transform(({ serviceResult, serviceIntermediateResult }) => {
|
|
1229
|
+
capturedServiceIntermediateResultInNestedTransform = [...serviceIntermediateResult];
|
|
1230
|
+
// serviceResult should equal serviceIntermediateResult since dep has no transform callback
|
|
1231
|
+
expect(serviceResult[0]).toEqual(serviceIntermediateResult[0]);
|
|
1232
|
+
return { wrapped: true, innerResult: serviceIntermediateResult[0] };
|
|
1233
|
+
})
|
|
1234
|
+
.build()
|
|
1235
|
+
);
|
|
1236
|
+
};
|
|
1237
|
+
|
|
1238
|
+
const result = await createHandlerTxBuilder({
|
|
1239
|
+
createUnitOfWork: () => {
|
|
1240
|
+
currentUow = createUnitOfWork(compiler, executor, decoder);
|
|
1241
|
+
return currentUow;
|
|
1438
1242
|
},
|
|
1439
|
-
)
|
|
1243
|
+
})
|
|
1244
|
+
.withServiceCalls(() => [wrapperService()] as const)
|
|
1245
|
+
.transform(({ serviceResult: [wrapperResult] }) => wrapperResult)
|
|
1246
|
+
.execute();
|
|
1247
|
+
|
|
1248
|
+
// The wrapper service's transform callback should have received the mutateResult, NOT empty array
|
|
1249
|
+
expect(capturedServiceIntermediateResultInNestedTransform[0]).toEqual({
|
|
1250
|
+
created: true,
|
|
1251
|
+
itemId: expect.any(FragnoId),
|
|
1252
|
+
});
|
|
1253
|
+
// Verify it's not the empty array sentinel
|
|
1254
|
+
expect(capturedServiceIntermediateResultInNestedTransform[0]).not.toEqual([]);
|
|
1440
1255
|
|
|
1441
|
-
|
|
1442
|
-
expect(result).toEqual({
|
|
1256
|
+
// And the handler should get the wrapped result
|
|
1257
|
+
expect(result).toEqual({
|
|
1258
|
+
wrapped: true,
|
|
1259
|
+
innerResult: { created: true, itemId: expect.any(FragnoId) },
|
|
1260
|
+
});
|
|
1443
1261
|
});
|
|
1262
|
+
});
|
|
1444
1263
|
|
|
1445
|
-
|
|
1264
|
+
describe("nested TxResult serviceCalls (service composition)", () => {
|
|
1265
|
+
it("should collect nested serviceCalls in dependency order", async () => {
|
|
1266
|
+
// Simpler test to verify collectAllTxResults works correctly
|
|
1446
1267
|
const compiler = createMockCompiler();
|
|
1268
|
+
const mockUser = {
|
|
1269
|
+
id: FragnoId.fromExternal("user-1", 1),
|
|
1270
|
+
email: "test@example.com",
|
|
1271
|
+
name: "Test User",
|
|
1272
|
+
balance: 100,
|
|
1273
|
+
};
|
|
1274
|
+
let retrievePhaseExecuted = false;
|
|
1447
1275
|
const executor: UOWExecutor<unknown, unknown> = {
|
|
1448
|
-
executeRetrievalPhase: async () =>
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
email: "test@example.com",
|
|
1453
|
-
name: "Test",
|
|
1454
|
-
balance: 0,
|
|
1455
|
-
},
|
|
1456
|
-
],
|
|
1457
|
-
],
|
|
1276
|
+
executeRetrievalPhase: async () => {
|
|
1277
|
+
retrievePhaseExecuted = true;
|
|
1278
|
+
return [[mockUser]];
|
|
1279
|
+
},
|
|
1458
1280
|
executeMutationPhase: async () => ({
|
|
1459
1281
|
success: true,
|
|
1460
1282
|
createdInternalIds: [],
|
|
@@ -1462,26 +1284,80 @@ describe("executeRestrictedUnitOfWork", () => {
|
|
|
1462
1284
|
};
|
|
1463
1285
|
const decoder = createMockDecoder();
|
|
1464
1286
|
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1287
|
+
let currentUow: IUnitOfWork | null = null;
|
|
1288
|
+
let getUserByIdCalled = false;
|
|
1289
|
+
let validateUserCalled = false;
|
|
1290
|
+
|
|
1291
|
+
// Simple service that retrieves - no nested serviceCalls
|
|
1292
|
+
const getUserById = (userId: string) => {
|
|
1293
|
+
getUserByIdCalled = true;
|
|
1294
|
+
if (!currentUow) {
|
|
1295
|
+
throw new Error("currentUow is null in getUserById!");
|
|
1296
|
+
}
|
|
1297
|
+
return createServiceTxBuilder(testSchema, currentUow)
|
|
1298
|
+
.retrieve((uow) =>
|
|
1299
|
+
uow.find("users", (b) => b.whereIndex("primary", (eb) => eb("id", "=", userId))),
|
|
1300
|
+
)
|
|
1301
|
+
.transformRetrieve(([users]) => users[0] ?? null)
|
|
1302
|
+
.build();
|
|
1303
|
+
};
|
|
1477
1304
|
|
|
1478
|
-
|
|
1479
|
-
|
|
1305
|
+
// Service with serviceCalls - depends on getUserById
|
|
1306
|
+
const validateUser = (userId: string) => {
|
|
1307
|
+
validateUserCalled = true;
|
|
1308
|
+
if (!currentUow) {
|
|
1309
|
+
throw new Error("currentUow is null in validateUser!");
|
|
1310
|
+
}
|
|
1311
|
+
return (
|
|
1312
|
+
createServiceTxBuilder(testSchema, currentUow)
|
|
1313
|
+
.withServiceCalls(() => [getUserById(userId)] as const)
|
|
1314
|
+
// mutate callback receives serviceIntermediateResult
|
|
1315
|
+
.mutate(({ serviceIntermediateResult: [user] }) => {
|
|
1316
|
+
return { valid: user !== null, user };
|
|
1317
|
+
})
|
|
1318
|
+
.build()
|
|
1319
|
+
);
|
|
1320
|
+
};
|
|
1321
|
+
|
|
1322
|
+
// Handler calls executeTx with serviceCalls containing validateUser
|
|
1323
|
+
// This tests 2-level nesting: handler -> validateUser -> getUserById
|
|
1324
|
+
const result = await createHandlerTxBuilder({
|
|
1325
|
+
createUnitOfWork: () => {
|
|
1326
|
+
currentUow = createUnitOfWork(compiler, executor, decoder);
|
|
1327
|
+
return currentUow;
|
|
1328
|
+
},
|
|
1329
|
+
})
|
|
1330
|
+
.withServiceCalls(() => [validateUser("user-1")] as const)
|
|
1331
|
+
.transform(({ serviceResult: [validationResult] }) => {
|
|
1332
|
+
return {
|
|
1333
|
+
isValid: validationResult.valid,
|
|
1334
|
+
userName: validationResult.user?.name,
|
|
1335
|
+
};
|
|
1336
|
+
})
|
|
1337
|
+
.execute();
|
|
1338
|
+
|
|
1339
|
+
// Verify services were called
|
|
1340
|
+
expect(getUserByIdCalled).toBe(true);
|
|
1341
|
+
expect(validateUserCalled).toBe(true);
|
|
1342
|
+
expect(retrievePhaseExecuted).toBe(true);
|
|
1343
|
+
expect(result.isValid).toBe(true);
|
|
1344
|
+
expect(result.userName).toBe("Test User");
|
|
1345
|
+
}, 500);
|
|
1346
|
+
|
|
1347
|
+
it("should handle a TxResult with serviceCalls that returns another TxResult", async () => {
|
|
1348
|
+
// This test reproduces the integration test scenario where:
|
|
1349
|
+
// - orderService.createOrderWithValidation has serviceCalls: () => [userService.getUserById(...)]
|
|
1350
|
+
// - handler has serviceCalls: () => [orderService.createOrderWithValidation(...)]
|
|
1480
1351
|
|
|
1481
|
-
it("should handle mutate-only transactions", async () => {
|
|
1482
1352
|
const compiler = createMockCompiler();
|
|
1353
|
+
const mockUser = {
|
|
1354
|
+
id: FragnoId.fromExternal("user-1", 1),
|
|
1355
|
+
email: "test@example.com",
|
|
1356
|
+
name: "Test User",
|
|
1357
|
+
balance: 100,
|
|
1358
|
+
};
|
|
1483
1359
|
const executor: UOWExecutor<unknown, unknown> = {
|
|
1484
|
-
executeRetrievalPhase: async () => [],
|
|
1360
|
+
executeRetrievalPhase: async () => [[mockUser]],
|
|
1485
1361
|
executeMutationPhase: async () => ({
|
|
1486
1362
|
success: true,
|
|
1487
1363
|
createdInternalIds: [BigInt(1)],
|
|
@@ -1489,94 +1365,146 @@ describe("executeRestrictedUnitOfWork", () => {
|
|
|
1489
1365
|
};
|
|
1490
1366
|
const decoder = createMockDecoder();
|
|
1491
1367
|
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1368
|
+
let currentUow: IUnitOfWork | null = null;
|
|
1369
|
+
|
|
1370
|
+
// Simulates userService.getUserById - returns a TxResult that retrieves a user
|
|
1371
|
+
const getUserById = (userId: string) => {
|
|
1372
|
+
return createServiceTxBuilder(testSchema, currentUow!)
|
|
1373
|
+
.retrieve((uow) =>
|
|
1374
|
+
uow.find("users", (b) => b.whereIndex("primary", (eb) => eb("id", "=", userId))),
|
|
1375
|
+
)
|
|
1376
|
+
.transformRetrieve(([users]) => users[0] ?? null)
|
|
1377
|
+
.build();
|
|
1378
|
+
};
|
|
1379
|
+
|
|
1380
|
+
// Simulates orderService.createOrderWithValidation - has serviceCalls on getUserById
|
|
1381
|
+
const createOrderWithValidation = (userId: string, productName: string) => {
|
|
1382
|
+
return createServiceTxBuilder(testSchema, currentUow!)
|
|
1383
|
+
.withServiceCalls(() => [getUserById(userId)] as const)
|
|
1384
|
+
.mutate(({ uow, serviceIntermediateResult: [user] }) => {
|
|
1385
|
+
if (!user) {
|
|
1386
|
+
throw new Error("User not found");
|
|
1387
|
+
}
|
|
1388
|
+
// Create an order (simulated by creating a user for simplicity)
|
|
1389
|
+
const orderId = uow.create("users", {
|
|
1390
|
+
email: `order-${productName}@example.com`,
|
|
1391
|
+
name: productName,
|
|
1392
|
+
balance: 0,
|
|
1393
|
+
});
|
|
1394
|
+
return { orderId, forUser: user.email };
|
|
1395
|
+
})
|
|
1396
|
+
.build();
|
|
1397
|
+
};
|
|
1398
|
+
|
|
1399
|
+
// Handler calls executeTx with serviceCalls containing the order service
|
|
1400
|
+
const result = await createHandlerTxBuilder({
|
|
1401
|
+
createUnitOfWork: () => {
|
|
1402
|
+
currentUow = createUnitOfWork(compiler, executor, decoder);
|
|
1403
|
+
return currentUow;
|
|
1502
1404
|
},
|
|
1503
|
-
)
|
|
1405
|
+
})
|
|
1406
|
+
.withServiceCalls(() => [createOrderWithValidation("user-1", "TypeScript Book")] as const)
|
|
1407
|
+
.transform(({ serviceResult: [orderResult] }) => {
|
|
1408
|
+
return {
|
|
1409
|
+
orderId: orderResult.orderId,
|
|
1410
|
+
forUser: orderResult.forUser,
|
|
1411
|
+
completed: true,
|
|
1412
|
+
};
|
|
1413
|
+
})
|
|
1414
|
+
.execute();
|
|
1504
1415
|
|
|
1505
|
-
expect(result).
|
|
1506
|
-
|
|
1416
|
+
expect(result.completed).toBe(true);
|
|
1417
|
+
expect(result.forUser).toBe("test@example.com");
|
|
1418
|
+
expect(result.orderId).toBeInstanceOf(FragnoId);
|
|
1419
|
+
}, 500); // Set 500ms timeout to catch deadlock
|
|
1507
1420
|
|
|
1508
|
-
it("should
|
|
1421
|
+
it("should handle deeply nested TxResult serviceCalls (3 levels)", async () => {
|
|
1509
1422
|
const compiler = createMockCompiler();
|
|
1423
|
+
const mockUser = {
|
|
1424
|
+
id: FragnoId.fromExternal("user-1", 1),
|
|
1425
|
+
email: "test@example.com",
|
|
1426
|
+
name: "Test User",
|
|
1427
|
+
balance: 100,
|
|
1428
|
+
};
|
|
1510
1429
|
const executor: UOWExecutor<unknown, unknown> = {
|
|
1511
|
-
executeRetrievalPhase: async () => [],
|
|
1430
|
+
executeRetrievalPhase: async () => [[mockUser]],
|
|
1512
1431
|
executeMutationPhase: async () => ({
|
|
1513
1432
|
success: true,
|
|
1514
|
-
createdInternalIds: [],
|
|
1433
|
+
createdInternalIds: [BigInt(1)],
|
|
1515
1434
|
}),
|
|
1516
1435
|
};
|
|
1517
1436
|
const decoder = createMockDecoder();
|
|
1518
1437
|
|
|
1519
|
-
|
|
1520
|
-
{
|
|
1521
|
-
mutate: () => {
|
|
1522
|
-
return Promise.resolve({ value: "async result" });
|
|
1523
|
-
},
|
|
1524
|
-
},
|
|
1525
|
-
{
|
|
1526
|
-
createUnitOfWork: () => createUnitOfWork(compiler, executor, decoder),
|
|
1527
|
-
},
|
|
1528
|
-
);
|
|
1438
|
+
let currentUow: IUnitOfWork | null = null;
|
|
1529
1439
|
|
|
1530
|
-
|
|
1531
|
-
|
|
1440
|
+
// Level 3: Basic user retrieval
|
|
1441
|
+
const getUserById = (userId: string) => {
|
|
1442
|
+
return createServiceTxBuilder(testSchema, currentUow!)
|
|
1443
|
+
.retrieve((uow) =>
|
|
1444
|
+
uow.find("users", (b) => b.whereIndex("primary", (eb) => eb("id", "=", userId))),
|
|
1445
|
+
)
|
|
1446
|
+
.transformRetrieve(([users]) => users[0] ?? null)
|
|
1447
|
+
.build();
|
|
1448
|
+
};
|
|
1532
1449
|
|
|
1533
|
-
|
|
1534
|
-
const
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
return {
|
|
1542
|
-
}
|
|
1543
|
-
|
|
1544
|
-
success: true,
|
|
1545
|
-
createdInternalIds: [],
|
|
1546
|
-
};
|
|
1547
|
-
},
|
|
1450
|
+
// Level 2: Depends on getUserById
|
|
1451
|
+
const validateUser = (userId: string) => {
|
|
1452
|
+
return createServiceTxBuilder(testSchema, currentUow!)
|
|
1453
|
+
.withServiceCalls(() => [getUserById(userId)] as const)
|
|
1454
|
+
.mutate(({ serviceIntermediateResult: [user] }) => {
|
|
1455
|
+
if (!user) {
|
|
1456
|
+
return { valid: false as const, reason: "User not found" };
|
|
1457
|
+
}
|
|
1458
|
+
return { valid: true as const, user };
|
|
1459
|
+
})
|
|
1460
|
+
.build();
|
|
1548
1461
|
};
|
|
1549
|
-
const decoder = createMockDecoder();
|
|
1550
1462
|
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1463
|
+
// Level 1: Depends on validateUser
|
|
1464
|
+
const createOrder = (userId: string, productName: string) => {
|
|
1465
|
+
return createServiceTxBuilder(testSchema, currentUow!)
|
|
1466
|
+
.withServiceCalls(() => [validateUser(userId)] as const)
|
|
1467
|
+
.mutate(({ uow, serviceIntermediateResult: [validation] }) => {
|
|
1468
|
+
if (!validation.valid) {
|
|
1469
|
+
throw new Error(validation.reason);
|
|
1470
|
+
}
|
|
1471
|
+
const orderId = uow.create("users", {
|
|
1472
|
+
email: `order-${productName}@example.com`,
|
|
1473
|
+
name: productName,
|
|
1474
|
+
balance: 0,
|
|
1475
|
+
});
|
|
1476
|
+
return { orderId, forUser: validation.user.email };
|
|
1477
|
+
})
|
|
1478
|
+
.build();
|
|
1479
|
+
};
|
|
1560
1480
|
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1481
|
+
// Handler: Depends on createOrder (3 levels deep)
|
|
1482
|
+
const result = await createHandlerTxBuilder({
|
|
1483
|
+
createUnitOfWork: () => {
|
|
1484
|
+
currentUow = createUnitOfWork(compiler, executor, decoder);
|
|
1485
|
+
return currentUow;
|
|
1486
|
+
},
|
|
1487
|
+
})
|
|
1488
|
+
.withServiceCalls(() => [createOrder("user-1", "Advanced TypeScript")] as const)
|
|
1489
|
+
.transform(({ serviceResult: [orderResult] }) => ({
|
|
1490
|
+
orderId: orderResult.orderId,
|
|
1491
|
+
forUser: orderResult.forUser,
|
|
1492
|
+
completed: true,
|
|
1493
|
+
}))
|
|
1494
|
+
.execute();
|
|
1495
|
+
|
|
1496
|
+
expect(result.completed).toBe(true);
|
|
1497
|
+
expect(result.forUser).toBe("test@example.com");
|
|
1498
|
+
expect(result.orderId).toBeInstanceOf(FragnoId);
|
|
1499
|
+
}, 500); // Set 500ms timeout to catch deadlock
|
|
1564
1500
|
});
|
|
1565
1501
|
|
|
1566
|
-
describe("
|
|
1567
|
-
it("should
|
|
1502
|
+
describe("triggerHook in serviceTx", () => {
|
|
1503
|
+
it("should record triggered hooks on the base UOW when using createServiceTx with retrieve and mutate", async () => {
|
|
1568
1504
|
const compiler = createMockCompiler();
|
|
1505
|
+
// Return empty array to simulate no existing user, so the hook will be triggered
|
|
1569
1506
|
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
|
-
],
|
|
1507
|
+
executeRetrievalPhase: async () => [[]],
|
|
1580
1508
|
executeMutationPhase: async () => ({
|
|
1581
1509
|
success: true,
|
|
1582
1510
|
createdInternalIds: [],
|
|
@@ -1584,167 +1512,179 @@ describe("executeRestrictedUnitOfWork", () => {
|
|
|
1584
1512
|
};
|
|
1585
1513
|
const decoder = createMockDecoder();
|
|
1586
1514
|
|
|
1587
|
-
|
|
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));
|
|
1515
|
+
let currentUow: IUnitOfWork | null = null;
|
|
1612
1516
|
|
|
1613
|
-
//
|
|
1614
|
-
|
|
1517
|
+
// Define hooks type for this test
|
|
1518
|
+
type TestHooks = {
|
|
1519
|
+
onSubscribe: (payload: { email: string }) => void;
|
|
1520
|
+
};
|
|
1615
1521
|
|
|
1616
|
-
|
|
1617
|
-
|
|
1522
|
+
const hooks: TestHooks = {
|
|
1523
|
+
onSubscribe: (payload: { email: string }) => {
|
|
1524
|
+
console.log(`onSubscribe: ${payload.email}`);
|
|
1525
|
+
},
|
|
1526
|
+
};
|
|
1618
1527
|
|
|
1619
|
-
//
|
|
1620
|
-
|
|
1528
|
+
// Service that retrieves a user and triggers a hook in mutate
|
|
1529
|
+
const subscribeUser = (email: string) => {
|
|
1530
|
+
return createServiceTxBuilder(testSchema, currentUow!, hooks)
|
|
1531
|
+
.retrieve((uow) =>
|
|
1532
|
+
uow.find("users", (b) => b.whereIndex("idx_email", (eb) => eb("email", "=", email))),
|
|
1533
|
+
)
|
|
1534
|
+
.transformRetrieve(([users]) => users[0] ?? null)
|
|
1535
|
+
.mutate(({ uow, retrieveResult: existingUser }) => {
|
|
1536
|
+
if (existingUser) {
|
|
1537
|
+
return { subscribed: false, email };
|
|
1538
|
+
}
|
|
1539
|
+
// Trigger hook when subscribing a new user
|
|
1540
|
+
uow.triggerHook("onSubscribe", { email });
|
|
1541
|
+
return { subscribed: true, email };
|
|
1542
|
+
})
|
|
1543
|
+
.build();
|
|
1544
|
+
};
|
|
1621
1545
|
|
|
1622
|
-
//
|
|
1623
|
-
|
|
1624
|
-
|
|
1546
|
+
// Execute the transaction
|
|
1547
|
+
await createHandlerTxBuilder({
|
|
1548
|
+
createUnitOfWork: () => {
|
|
1549
|
+
currentUow = createUnitOfWork(compiler, executor, decoder);
|
|
1550
|
+
return currentUow;
|
|
1551
|
+
},
|
|
1552
|
+
})
|
|
1553
|
+
.withServiceCalls(() => [subscribeUser("new@example.com")] as const)
|
|
1554
|
+
.transform(({ serviceResult: [result] }) => result)
|
|
1555
|
+
.execute();
|
|
1556
|
+
|
|
1557
|
+
// Verify that the hook was triggered and recorded on the base UOW
|
|
1558
|
+
const triggeredHooks = currentUow!.getTriggeredHooks();
|
|
1559
|
+
expect(triggeredHooks).toHaveLength(1);
|
|
1560
|
+
expect(triggeredHooks[0]).toMatchObject({
|
|
1561
|
+
hookName: "onSubscribe",
|
|
1562
|
+
payload: { email: "new@example.com" },
|
|
1563
|
+
});
|
|
1625
1564
|
});
|
|
1626
1565
|
|
|
1627
|
-
it("should
|
|
1566
|
+
it("should record triggered hooks when service has only retrieve (no retrieveSuccess) and mutate", async () => {
|
|
1628
1567
|
const compiler = createMockCompiler();
|
|
1568
|
+
const mockUser = {
|
|
1569
|
+
id: FragnoId.fromExternal("user-1", 1),
|
|
1570
|
+
email: "test@example.com",
|
|
1571
|
+
name: "Test User",
|
|
1572
|
+
balance: 100,
|
|
1573
|
+
};
|
|
1629
1574
|
const executor: UOWExecutor<unknown, unknown> = {
|
|
1630
|
-
executeRetrievalPhase: async () => [[]],
|
|
1575
|
+
executeRetrievalPhase: async () => [[mockUser]],
|
|
1631
1576
|
executeMutationPhase: async () => ({
|
|
1632
1577
|
success: true,
|
|
1633
|
-
createdInternalIds: [
|
|
1578
|
+
createdInternalIds: [],
|
|
1634
1579
|
}),
|
|
1635
1580
|
};
|
|
1636
1581
|
const decoder = createMockDecoder();
|
|
1637
1582
|
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
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
|
-
},
|
|
1583
|
+
let currentUow: IUnitOfWork | null = null;
|
|
1584
|
+
|
|
1585
|
+
// Define hooks type for this test
|
|
1586
|
+
type TestHooks = {
|
|
1587
|
+
onUserUpdated: (payload: { userId: string }) => void;
|
|
1588
|
+
};
|
|
1589
|
+
|
|
1590
|
+
const hooks: TestHooks = {
|
|
1591
|
+
onUserUpdated: (payload: { userId: string }) => {
|
|
1592
|
+
console.log(`onUserUpdated: ${payload.userId}`);
|
|
1665
1593
|
},
|
|
1666
|
-
|
|
1667
|
-
);
|
|
1594
|
+
};
|
|
1668
1595
|
|
|
1669
|
-
//
|
|
1670
|
-
|
|
1596
|
+
// Service that uses raw retrieve results (no transformRetrieve) and triggers hook
|
|
1597
|
+
const updateUser = (userId: string) => {
|
|
1598
|
+
return (
|
|
1599
|
+
createServiceTxBuilder(testSchema, currentUow!, hooks)
|
|
1600
|
+
.retrieve((uow) =>
|
|
1601
|
+
uow.find("users", (b) => b.whereIndex("primary", (eb) => eb("id", "=", userId))),
|
|
1602
|
+
)
|
|
1603
|
+
// NO transformRetrieve - mutate receives raw [users[]] array
|
|
1604
|
+
.mutate(({ uow, retrieveResult: [users] }) => {
|
|
1605
|
+
const user = users[0];
|
|
1606
|
+
if (!user) {
|
|
1607
|
+
return { updated: false };
|
|
1608
|
+
}
|
|
1609
|
+
uow.triggerHook("onUserUpdated", { userId: user.id.toString() });
|
|
1610
|
+
return { updated: true };
|
|
1611
|
+
})
|
|
1612
|
+
.build()
|
|
1613
|
+
);
|
|
1614
|
+
};
|
|
1671
1615
|
|
|
1672
|
-
|
|
1673
|
-
|
|
1616
|
+
await createHandlerTxBuilder({
|
|
1617
|
+
createUnitOfWork: () => {
|
|
1618
|
+
currentUow = createUnitOfWork(compiler, executor, decoder);
|
|
1619
|
+
return currentUow;
|
|
1620
|
+
},
|
|
1621
|
+
})
|
|
1622
|
+
.withServiceCalls(() => [updateUser("user-1")] as const)
|
|
1623
|
+
.transform(({ serviceResult: [result] }) => result)
|
|
1624
|
+
.execute();
|
|
1625
|
+
|
|
1626
|
+
// Verify hook was triggered
|
|
1627
|
+
const triggeredHooks = currentUow!.getTriggeredHooks();
|
|
1628
|
+
expect(triggeredHooks).toHaveLength(1);
|
|
1629
|
+
expect(triggeredHooks[0]).toMatchObject({
|
|
1630
|
+
hookName: "onUserUpdated",
|
|
1631
|
+
payload: { userId: expect.any(String) },
|
|
1632
|
+
});
|
|
1674
1633
|
});
|
|
1634
|
+
});
|
|
1635
|
+
|
|
1636
|
+
describe("error handling in createServiceTx", () => {
|
|
1637
|
+
it("should not cause unhandled rejection when retrieve callback throws synchronously in serviceCalls", async () => {
|
|
1638
|
+
// This test verifies that when a service's retrieve callback throws synchronously,
|
|
1639
|
+
// the error is properly propagated without causing an unhandled rejection warning.
|
|
1640
|
+
// Without the fix (adding retrievePhase.catch(() => {}) before rejecting and throwing),
|
|
1641
|
+
// this test would cause an "Unhandled Rejection" warning from Vitest.
|
|
1675
1642
|
|
|
1676
|
-
it("should prevent anti-pattern: service async work completes before mutations execute", async () => {
|
|
1677
1643
|
const compiler = createMockCompiler();
|
|
1678
1644
|
const executor: UOWExecutor<unknown, unknown> = {
|
|
1679
|
-
executeRetrievalPhase: async () => [
|
|
1645
|
+
executeRetrievalPhase: async () => [],
|
|
1680
1646
|
executeMutationPhase: async () => ({
|
|
1681
1647
|
success: true,
|
|
1682
|
-
createdInternalIds: [
|
|
1648
|
+
createdInternalIds: [],
|
|
1683
1649
|
}),
|
|
1684
1650
|
};
|
|
1685
1651
|
const decoder = createMockDecoder();
|
|
1686
1652
|
|
|
1687
|
-
|
|
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
|
-
);
|
|
1653
|
+
let currentUow: IUnitOfWork | null = null;
|
|
1725
1654
|
|
|
1726
|
-
|
|
1727
|
-
|
|
1655
|
+
const syncError = new Error("Retrieve callback threw synchronously");
|
|
1656
|
+
|
|
1657
|
+
// Service that throws synchronously in retrieve callback
|
|
1658
|
+
const failingService = () => {
|
|
1659
|
+
return createServiceTxBuilder(testSchema, currentUow!)
|
|
1660
|
+
.retrieve(() => {
|
|
1661
|
+
throw syncError;
|
|
1662
|
+
})
|
|
1663
|
+
.build();
|
|
1664
|
+
};
|
|
1728
1665
|
|
|
1729
|
-
|
|
1730
|
-
|
|
1666
|
+
// Execute with serviceCalls that contain the failing service
|
|
1667
|
+
// The error should be properly caught and re-thrown without unhandled rejection
|
|
1668
|
+
await expect(
|
|
1669
|
+
createHandlerTxBuilder({
|
|
1670
|
+
createUnitOfWork: () => {
|
|
1671
|
+
currentUow = createUnitOfWork(compiler, executor, decoder);
|
|
1672
|
+
return currentUow;
|
|
1673
|
+
},
|
|
1674
|
+
})
|
|
1675
|
+
.withServiceCalls(() => [failingService()] as const)
|
|
1676
|
+
.transform(({ serviceResult: [result] }) => result)
|
|
1677
|
+
.execute(),
|
|
1678
|
+
).rejects.toThrow("Retrieve callback threw synchronously");
|
|
1731
1679
|
});
|
|
1732
|
-
});
|
|
1733
1680
|
|
|
1734
|
-
|
|
1735
|
-
|
|
1681
|
+
it("should not cause unhandled rejection when serviceCalls callback throws synchronously", async () => {
|
|
1682
|
+
// This test verifies that when a service's serviceCalls callback throws synchronously,
|
|
1683
|
+
// the error is properly propagated without causing an unhandled rejection warning.
|
|
1684
|
+
|
|
1736
1685
|
const compiler = createMockCompiler();
|
|
1737
1686
|
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
|
-
],
|
|
1687
|
+
executeRetrievalPhase: async () => [],
|
|
1748
1688
|
executeMutationPhase: async () => ({
|
|
1749
1689
|
success: true,
|
|
1750
1690
|
createdInternalIds: [],
|
|
@@ -1754,63 +1694,52 @@ describe("executeRestrictedUnitOfWork", () => {
|
|
|
1754
1694
|
|
|
1755
1695
|
let currentUow: IUnitOfWork | null = null;
|
|
1756
1696
|
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
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
|
-
{
|
|
1697
|
+
const syncError = new Error("Deps callback threw synchronously");
|
|
1698
|
+
|
|
1699
|
+
// Service that throws synchronously in serviceCalls callback
|
|
1700
|
+
const failingService = () => {
|
|
1701
|
+
return createServiceTxBuilder(testSchema, currentUow!)
|
|
1702
|
+
.withServiceCalls(() => {
|
|
1703
|
+
throw syncError;
|
|
1704
|
+
})
|
|
1705
|
+
.mutate(() => ({ done: true }))
|
|
1706
|
+
.build();
|
|
1707
|
+
};
|
|
1708
|
+
|
|
1709
|
+
// Execute with serviceCalls that contain the failing service
|
|
1710
|
+
await expect(
|
|
1711
|
+
createHandlerTxBuilder({
|
|
1781
1712
|
createUnitOfWork: () => {
|
|
1782
1713
|
currentUow = createUnitOfWork(compiler, executor, decoder);
|
|
1783
1714
|
return currentUow;
|
|
1784
1715
|
},
|
|
1785
|
-
}
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1716
|
+
})
|
|
1717
|
+
.withServiceCalls(() => [failingService()] as const)
|
|
1718
|
+
.transform(({ serviceResult: [result] }) => result)
|
|
1719
|
+
.execute(),
|
|
1720
|
+
).rejects.toThrow("Deps callback threw synchronously");
|
|
1790
1721
|
});
|
|
1722
|
+
});
|
|
1791
1723
|
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1724
|
+
describe("mutate-only service type inference", () => {
|
|
1725
|
+
it("should correctly type serviceIntermediateResult when dependent service only has mutate (no retrieve)", async () => {
|
|
1726
|
+
// This test verifies that when a service has ONLY a mutate callback (no retrieve),
|
|
1727
|
+
// the mutate result is correctly typed as the serviceIntermediateResult for dependent services.
|
|
1728
|
+
//
|
|
1729
|
+
// Execution order:
|
|
1730
|
+
// 1. generateOTP's retrieve phase runs (empty - no retrieve callback)
|
|
1731
|
+
// 2. generateOTP's mutate runs → returns { otpId, code }
|
|
1732
|
+
// 3. sendOTPEmail's mutate runs → serviceIntermediateResult[0] is { otpId, code, userId }
|
|
1733
|
+
//
|
|
1734
|
+
// Without the InferBuilderRetrieveSuccessResult fix, serviceIntermediateResult[0] would be
|
|
1735
|
+
// typed as `[]` (empty tuple) even though at runtime it's the mutate result.
|
|
1796
1736
|
|
|
1737
|
+
const compiler = createMockCompiler();
|
|
1797
1738
|
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
|
-
],
|
|
1739
|
+
executeRetrievalPhase: async () => {
|
|
1740
|
+
return [];
|
|
1741
|
+
},
|
|
1808
1742
|
executeMutationPhase: async () => {
|
|
1809
|
-
executionAttemptCount++;
|
|
1810
|
-
// Fail on first 2 attempts, succeed on 3rd
|
|
1811
|
-
if (executionAttemptCount < 3) {
|
|
1812
|
-
return { success: false };
|
|
1813
|
-
}
|
|
1814
1743
|
return {
|
|
1815
1744
|
success: true,
|
|
1816
1745
|
createdInternalIds: [],
|
|
@@ -1821,44 +1750,169 @@ describe("executeRestrictedUnitOfWork", () => {
|
|
|
1821
1750
|
|
|
1822
1751
|
let currentUow: IUnitOfWork | null = null;
|
|
1823
1752
|
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
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
|
-
);
|
|
1753
|
+
// Capture the runtime value of serviceResult to verify it contains the mutate result
|
|
1754
|
+
let capturedOtpResult: unknown = null;
|
|
1755
|
+
|
|
1756
|
+
// Mutate-only service - simulates OTP generation
|
|
1757
|
+
// No .retrieve() - this service doesn't need to read anything first
|
|
1758
|
+
const generateOTP = (userId: string) => {
|
|
1759
|
+
return createServiceTxBuilder(testSchema, currentUow!)
|
|
1760
|
+
.mutate(({ uow }) => {
|
|
1761
|
+
const otpId = uow.create("users", {
|
|
1762
|
+
email: `otp-${userId}@example.com`,
|
|
1763
|
+
name: "OTP Record",
|
|
1764
|
+
balance: 0,
|
|
1765
|
+
});
|
|
1766
|
+
// Return data that dependents need - this becomes serviceResult for them
|
|
1767
|
+
return { otpId, code: "ABC123", userId };
|
|
1768
|
+
})
|
|
1769
|
+
.build();
|
|
1770
|
+
};
|
|
1857
1771
|
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1772
|
+
// Service that depends on generateOTP
|
|
1773
|
+
// The key test: serviceIntermediateResult[0] should be typed as { otpId, code, userId }
|
|
1774
|
+
const sendOTPEmail = (userId: string, email: string) => {
|
|
1775
|
+
return createServiceTxBuilder(testSchema, currentUow!)
|
|
1776
|
+
.withServiceCalls(() => [generateOTP(userId)] as const)
|
|
1777
|
+
.mutate(({ uow, serviceIntermediateResult: [otpResult] }) => {
|
|
1778
|
+
// RUNTIME CAPTURE: Store the actual runtime value for verification
|
|
1779
|
+
capturedOtpResult = otpResult;
|
|
1780
|
+
|
|
1781
|
+
// TYPE TEST: Without the fix, this would require a manual cast.
|
|
1782
|
+
// With the fix, TypeScript knows otpResult is { otpId: FragnoId, code: string, userId: string }
|
|
1783
|
+
expectTypeOf(otpResult).toEqualTypeOf<{
|
|
1784
|
+
otpId: FragnoId;
|
|
1785
|
+
code: string;
|
|
1786
|
+
userId: string;
|
|
1787
|
+
}>();
|
|
1788
|
+
|
|
1789
|
+
// Access properties without type errors - proves the type inference works
|
|
1790
|
+
const message = `Your OTP code is: ${otpResult.code}`;
|
|
1791
|
+
|
|
1792
|
+
uow.create("users", {
|
|
1793
|
+
email,
|
|
1794
|
+
name: message,
|
|
1795
|
+
balance: 0,
|
|
1796
|
+
});
|
|
1797
|
+
|
|
1798
|
+
return { sent: true, forUser: otpResult.userId };
|
|
1799
|
+
})
|
|
1800
|
+
.build();
|
|
1801
|
+
};
|
|
1802
|
+
|
|
1803
|
+
const result = await createHandlerTxBuilder({
|
|
1804
|
+
createUnitOfWork: () => {
|
|
1805
|
+
currentUow = createUnitOfWork(compiler, executor, decoder);
|
|
1806
|
+
return currentUow;
|
|
1807
|
+
},
|
|
1808
|
+
})
|
|
1809
|
+
.withServiceCalls(() => [sendOTPEmail("user-1", "test@example.com")] as const)
|
|
1810
|
+
.transform(({ serviceResult: [emailResult] }) => emailResult)
|
|
1811
|
+
.execute();
|
|
1812
|
+
|
|
1813
|
+
expect(result.sent).toBe(true);
|
|
1814
|
+
expect(result.forUser).toBe("user-1");
|
|
1815
|
+
|
|
1816
|
+
// RUNTIME VERIFICATION: Verify the actual runtime value of serviceIntermediateResult
|
|
1817
|
+
// This proves generateOTP's mutate result is actually passed as serviceIntermediateResult
|
|
1818
|
+
expect(capturedOtpResult).not.toBeNull();
|
|
1819
|
+
expect(capturedOtpResult).toMatchObject({
|
|
1820
|
+
code: "ABC123",
|
|
1821
|
+
userId: "user-1",
|
|
1822
|
+
});
|
|
1823
|
+
expect((capturedOtpResult as { otpId: FragnoId }).otpId).toBeInstanceOf(FragnoId);
|
|
1824
|
+
});
|
|
1825
|
+
|
|
1826
|
+
it("should correctly type serviceIntermediateResult with multiple mutate-only service calls", async () => {
|
|
1827
|
+
// Test with multiple mutate-only dependencies to verify tuple typing works
|
|
1828
|
+
|
|
1829
|
+
const compiler = createMockCompiler();
|
|
1830
|
+
const executor: UOWExecutor<unknown, unknown> = {
|
|
1831
|
+
executeRetrievalPhase: async () => [],
|
|
1832
|
+
executeMutationPhase: async () => ({
|
|
1833
|
+
success: true,
|
|
1834
|
+
createdInternalIds: [],
|
|
1835
|
+
}),
|
|
1836
|
+
};
|
|
1837
|
+
const decoder = createMockDecoder();
|
|
1838
|
+
|
|
1839
|
+
let currentUow: IUnitOfWork | null = null;
|
|
1840
|
+
|
|
1841
|
+
// Capture runtime values for verification
|
|
1842
|
+
let capturedAuditResult: unknown = null;
|
|
1843
|
+
let capturedCounterResult: unknown = null;
|
|
1844
|
+
|
|
1845
|
+
// First mutate-only service
|
|
1846
|
+
const createAuditLog = (action: string) => {
|
|
1847
|
+
return createServiceTxBuilder(testSchema, currentUow!)
|
|
1848
|
+
.mutate(({ uow }) => {
|
|
1849
|
+
const logId = uow.create("users", {
|
|
1850
|
+
email: `audit-${action}@example.com`,
|
|
1851
|
+
name: action,
|
|
1852
|
+
balance: 0,
|
|
1853
|
+
});
|
|
1854
|
+
return { logId, action, timestamp: 1234567890 };
|
|
1855
|
+
})
|
|
1856
|
+
.build();
|
|
1857
|
+
};
|
|
1858
|
+
|
|
1859
|
+
// Second mutate-only service
|
|
1860
|
+
const incrementCounter = (name: string) => {
|
|
1861
|
+
return createServiceTxBuilder(testSchema, currentUow!)
|
|
1862
|
+
.mutate(() => {
|
|
1863
|
+
// Simulates incrementing a counter - returns the new value
|
|
1864
|
+
return { counterName: name, newValue: 42 };
|
|
1865
|
+
})
|
|
1866
|
+
.build();
|
|
1867
|
+
};
|
|
1868
|
+
|
|
1869
|
+
const result = await createHandlerTxBuilder({
|
|
1870
|
+
createUnitOfWork: () => {
|
|
1871
|
+
currentUow = createUnitOfWork(compiler, executor, decoder);
|
|
1872
|
+
return currentUow;
|
|
1873
|
+
},
|
|
1874
|
+
})
|
|
1875
|
+
.withServiceCalls(
|
|
1876
|
+
() => [createAuditLog("user_login"), incrementCounter("login_count")] as const,
|
|
1877
|
+
)
|
|
1878
|
+
.mutate(({ serviceIntermediateResult: [auditResult, counterResult] }) => {
|
|
1879
|
+
// RUNTIME CAPTURE: Store the actual runtime values
|
|
1880
|
+
capturedAuditResult = auditResult;
|
|
1881
|
+
capturedCounterResult = counterResult;
|
|
1882
|
+
|
|
1883
|
+
// TYPE TESTS: Both should be correctly typed from their mutate results
|
|
1884
|
+
expectTypeOf(auditResult).toEqualTypeOf<{
|
|
1885
|
+
logId: FragnoId;
|
|
1886
|
+
action: string;
|
|
1887
|
+
timestamp: number;
|
|
1888
|
+
}>();
|
|
1889
|
+
expectTypeOf(counterResult).toEqualTypeOf<{
|
|
1890
|
+
counterName: string;
|
|
1891
|
+
newValue: number;
|
|
1892
|
+
}>();
|
|
1893
|
+
|
|
1894
|
+
// Access properties - proves type inference works
|
|
1895
|
+
return {
|
|
1896
|
+
auditAction: auditResult.action,
|
|
1897
|
+
loginCount: counterResult.newValue,
|
|
1898
|
+
};
|
|
1899
|
+
})
|
|
1900
|
+
.execute();
|
|
1901
|
+
|
|
1902
|
+
expect(result.auditAction).toBe("user_login");
|
|
1903
|
+
expect(result.loginCount).toBe(42);
|
|
1904
|
+
|
|
1905
|
+
// RUNTIME VERIFICATION: Verify the actual runtime values
|
|
1906
|
+
expect(capturedAuditResult).toMatchObject({
|
|
1907
|
+
action: "user_login",
|
|
1908
|
+
timestamp: 1234567890,
|
|
1909
|
+
});
|
|
1910
|
+
expect((capturedAuditResult as { logId: FragnoId }).logId).toBeInstanceOf(FragnoId);
|
|
1911
|
+
|
|
1912
|
+
expect(capturedCounterResult).toEqual({
|
|
1913
|
+
counterName: "login_count",
|
|
1914
|
+
newValue: 42,
|
|
1915
|
+
});
|
|
1862
1916
|
});
|
|
1863
1917
|
});
|
|
1864
1918
|
});
|