@fragno-dev/db 0.2.2 → 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 +202 -140
- package/CHANGELOG.md +35 -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 +39 -29
- 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 +18 -7
- package/dist/db-fragment-definition-builder.d.ts.map +1 -1
- package/dist/db-fragment-definition-builder.js +116 -54
- 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 +79 -2
- package/dist/fragments/internal-fragment.d.ts.map +1 -1
- package/dist/fragments/internal-fragment.js +150 -32
- 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 +42 -1
- package/dist/hooks/hooks.d.ts.map +1 -1
- package/dist/hooks/hooks.js +72 -6
- 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 +15 -8
- 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.map +1 -1
- package/dist/query/unit-of-work/execute-unit-of-work.js +11 -6
- 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 +50 -14
- package/dist/query/unit-of-work/unit-of-work.d.ts.map +1 -1
- package/dist/query/unit-of-work/unit-of-work.js +86 -5
- package/dist/query/unit-of-work/unit-of-work.js.map +1 -1
- package/dist/query/value-decoding.js +9 -6
- 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 +38 -14
- package/dist/schema/create.d.ts.map +1 -1
- package/dist/schema/create.js +81 -42
- package/dist/schema/create.js.map +1 -1
- package/dist/schema/generate-id.js +2 -2
- package/dist/schema/generate-id.js.map +1 -1
- 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} +10 -10
- package/src/adapters/{drizzle/drizzle-adapter-sqlite3.test.ts → generic-sql/sql-adapter-sqlite3-uow.test.ts} +7 -7
- 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 +30 -12
- package/src/db-fragment-definition-builder.ts +142 -73
- package/src/db-fragment-instantiator.test.ts +105 -13
- package/src/db-fragment-integration.test.ts +9 -7
- 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 +458 -8
- package/src/fragments/internal-fragment.ts +322 -63
- 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 +165 -5
- package/src/hooks/hooks.ts +197 -9
- 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 +64 -26
- 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 +25 -1
- package/src/query/unit-of-work/execute-unit-of-work.ts +25 -8
- package/src/query/unit-of-work/unit-of-work-coordinator.test.ts +12 -12
- 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 +168 -37
- package/src/query/unit-of-work/unit-of-work.ts +203 -18
- 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 +185 -47
- package/src/schema/generate-id.test.ts +2 -2
- package/src/schema/generate-id.ts +2 -2
- 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
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
1
4
|
import { describe, it, expect, vi, assert } from "vitest";
|
|
2
5
|
import { instantiate, defineFragment } from "@fragno-dev/core";
|
|
3
6
|
import { defineRoutes } from "@fragno-dev/core/route";
|
|
@@ -7,14 +10,22 @@ import type { DatabaseAdapter } from "./adapters/adapters";
|
|
|
7
10
|
import type { SimpleQueryInterface } from "./query/simple-query-interface";
|
|
8
11
|
import { RequestContextStorage } from "@fragno-dev/core/internal/request-context-storage";
|
|
9
12
|
import { z } from "zod";
|
|
13
|
+
import { suffixNamingStrategy } from "./naming/sql-naming";
|
|
10
14
|
|
|
11
15
|
// Create a test schema
|
|
12
|
-
const testSchema = schema((s) => {
|
|
16
|
+
const testSchema = schema("test", (s) => {
|
|
13
17
|
return s.addTable("users", (t) => {
|
|
14
18
|
return t.addColumn("id", idColumn()).addColumn("name", column("string"));
|
|
15
19
|
});
|
|
16
20
|
});
|
|
17
21
|
|
|
22
|
+
// Schema with dashes in the name (used to test namespace sanitization)
|
|
23
|
+
const dashedSchema = schema("my-fragment", (s) => {
|
|
24
|
+
return s.addTable("items", (t) => {
|
|
25
|
+
return t.addColumn("id", idColumn()).addColumn("label", column("string"));
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
18
29
|
type TestSchema = typeof testSchema;
|
|
19
30
|
|
|
20
31
|
// Mock database adapter
|
|
@@ -35,6 +46,8 @@ function createMockAdapter(): DatabaseAdapter {
|
|
|
35
46
|
mutationPhase: Promise.resolve(),
|
|
36
47
|
})),
|
|
37
48
|
restrict: vi.fn(() => createMockRestrictedUow()),
|
|
49
|
+
getRetrievalOperations: vi.fn(() => []),
|
|
50
|
+
getMutationOperations: vi.fn(() => []),
|
|
38
51
|
table: vi.fn(() => ({
|
|
39
52
|
findMany: vi.fn(),
|
|
40
53
|
})),
|
|
@@ -61,7 +74,11 @@ function createMockAdapter(): DatabaseAdapter {
|
|
|
61
74
|
executeMutations: vi.fn(async () => ({ success: true })),
|
|
62
75
|
commit: vi.fn(),
|
|
63
76
|
rollback: vi.fn(),
|
|
77
|
+
registerSchema: vi.fn(),
|
|
64
78
|
reset: vi.fn(),
|
|
79
|
+
getRetrievalOperations: vi.fn(() => []),
|
|
80
|
+
getMutationOperations: vi.fn(() => []),
|
|
81
|
+
getCreatedIds: vi.fn(() => []),
|
|
65
82
|
table: vi.fn(() => ({
|
|
66
83
|
findMany: vi.fn(),
|
|
67
84
|
})),
|
|
@@ -74,10 +91,11 @@ function createMockAdapter(): DatabaseAdapter {
|
|
|
74
91
|
|
|
75
92
|
return {
|
|
76
93
|
createQueryEngine: vi.fn(() => mockdb),
|
|
77
|
-
|
|
94
|
+
getSchemaVersion: vi.fn(async () => undefined),
|
|
78
95
|
close: vi.fn(),
|
|
79
96
|
type: "mock",
|
|
80
97
|
contextStorage: new RequestContextStorage(),
|
|
98
|
+
namingStrategy: suffixNamingStrategy,
|
|
81
99
|
} as unknown as DatabaseAdapter;
|
|
82
100
|
}
|
|
83
101
|
|
|
@@ -206,7 +224,7 @@ describe("db-fragment-instantiator", () => {
|
|
|
206
224
|
|
|
207
225
|
describe("database operations with UOW", () => {
|
|
208
226
|
it("should allow accessing schema-typed UOW in handlers via handlerTx", async () => {
|
|
209
|
-
const testSchemaWithCounter = schema((s) => {
|
|
227
|
+
const testSchemaWithCounter = schema("testschemawithcounter", (s) => {
|
|
210
228
|
return s.addTable("counters", (t) => {
|
|
211
229
|
return t.addColumn("id", idColumn()).addColumn("value", column("integer"));
|
|
212
230
|
});
|
|
@@ -447,7 +465,73 @@ describe("db-fragment-instantiator", () => {
|
|
|
447
465
|
.build();
|
|
448
466
|
|
|
449
467
|
expect(fragment.$internal.deps.schema).toBe(testSchema);
|
|
450
|
-
expect(fragment.$internal.deps.namespace).toBe("test
|
|
468
|
+
expect(fragment.$internal.deps.namespace).toBe("test");
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
it("should use databaseNamespace override when provided", () => {
|
|
472
|
+
const definition = defineFragment("test-db-fragment")
|
|
473
|
+
.extend(withDatabase(testSchema))
|
|
474
|
+
.build();
|
|
475
|
+
|
|
476
|
+
const mockAdapter = createMockAdapter();
|
|
477
|
+
const fragment = instantiate(definition)
|
|
478
|
+
.withOptions({
|
|
479
|
+
mountRoute: "/api",
|
|
480
|
+
databaseAdapter: mockAdapter,
|
|
481
|
+
databaseNamespace: "custom-namespace",
|
|
482
|
+
})
|
|
483
|
+
.build();
|
|
484
|
+
|
|
485
|
+
expect(fragment.$internal.deps.namespace).toBe("custom-namespace");
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
it("should allow explicit null namespace override", () => {
|
|
489
|
+
const definition = defineFragment("test-db-fragment")
|
|
490
|
+
.extend(withDatabase(testSchema))
|
|
491
|
+
.build();
|
|
492
|
+
|
|
493
|
+
const mockAdapter = createMockAdapter();
|
|
494
|
+
const fragment = instantiate(definition)
|
|
495
|
+
.withOptions({
|
|
496
|
+
mountRoute: "/api",
|
|
497
|
+
databaseAdapter: mockAdapter,
|
|
498
|
+
databaseNamespace: null,
|
|
499
|
+
})
|
|
500
|
+
.build();
|
|
501
|
+
|
|
502
|
+
expect(fragment.$internal.deps.namespace).toBeNull();
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
it("should sanitize dashes in schema.name when used as default namespace", () => {
|
|
506
|
+
const definition = defineFragment("test-dashed-fragment")
|
|
507
|
+
.extend(withDatabase(dashedSchema))
|
|
508
|
+
.build();
|
|
509
|
+
|
|
510
|
+
const mockAdapter = createMockAdapter();
|
|
511
|
+
const fragment = instantiate(definition)
|
|
512
|
+
.withOptions({ mountRoute: "/api", databaseAdapter: mockAdapter })
|
|
513
|
+
.build();
|
|
514
|
+
|
|
515
|
+
// schema.name is "my-fragment", but default namespace should be sanitized to "my_fragment"
|
|
516
|
+
expect(fragment.$internal.deps.namespace).toBe("my_fragment");
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
it("should NOT sanitize explicit databaseNamespace even when it contains dashes", () => {
|
|
520
|
+
const definition = defineFragment("test-dashed-fragment")
|
|
521
|
+
.extend(withDatabase(dashedSchema))
|
|
522
|
+
.build();
|
|
523
|
+
|
|
524
|
+
const mockAdapter = createMockAdapter();
|
|
525
|
+
const fragment = instantiate(definition)
|
|
526
|
+
.withOptions({
|
|
527
|
+
mountRoute: "/api",
|
|
528
|
+
databaseAdapter: mockAdapter,
|
|
529
|
+
databaseNamespace: "my-fragment",
|
|
530
|
+
})
|
|
531
|
+
.build();
|
|
532
|
+
|
|
533
|
+
// Explicit override should be used as-is, dashes preserved
|
|
534
|
+
expect(fragment.$internal.deps.namespace).toBe("my-fragment");
|
|
451
535
|
});
|
|
452
536
|
|
|
453
537
|
it("should populate $internal when using providesBaseService without withDependencies", () => {
|
|
@@ -478,18 +562,26 @@ describe("db-fragment-instantiator", () => {
|
|
|
478
562
|
});
|
|
479
563
|
});
|
|
480
564
|
|
|
481
|
-
describe("
|
|
482
|
-
it("should
|
|
565
|
+
describe("default adapter", () => {
|
|
566
|
+
it("should default to sqlite adapter when databaseAdapter is not provided", () => {
|
|
483
567
|
const definition = defineFragment("test-db-fragment")
|
|
484
568
|
.extend(withDatabase(testSchema))
|
|
485
569
|
.build();
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
570
|
+
const previous = process.env["FRAGNO_DATA_DIR"];
|
|
571
|
+
const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), "fragno-db-default-"));
|
|
572
|
+
process.env["FRAGNO_DATA_DIR"] = dataDir;
|
|
573
|
+
|
|
574
|
+
try {
|
|
575
|
+
const fragment = instantiate(definition).withOptions({}).build();
|
|
576
|
+
expect(fragment.$internal.options.databaseAdapter).toBeDefined();
|
|
577
|
+
expect(fragment.$internal.deps.db).toBeDefined();
|
|
578
|
+
} finally {
|
|
579
|
+
if (previous === undefined) {
|
|
580
|
+
delete process.env["FRAGNO_DATA_DIR"];
|
|
581
|
+
} else {
|
|
582
|
+
process.env["FRAGNO_DATA_DIR"] = previous;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
493
585
|
});
|
|
494
586
|
|
|
495
587
|
it("should throw when serviceTx called outside request context", () => {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { SQLocalKysely } from "sqlocal/kysely";
|
|
2
2
|
import { assert, beforeAll, describe, expect, it } from "vitest";
|
|
3
3
|
import { z } from "zod";
|
|
4
|
-
import {
|
|
4
|
+
import { SqlAdapter } from "./adapters/generic-sql/generic-sql-adapter";
|
|
5
5
|
import { column, idColumn, referenceColumn, schema, type FragnoId } from "./schema/create";
|
|
6
6
|
import { defineFragment, instantiate } from "@fragno-dev/core";
|
|
7
7
|
import { defineRoutes } from "@fragno-dev/core/route";
|
|
@@ -12,7 +12,7 @@ import { SQLocalDriverConfig } from "./adapters/generic-sql/driver-config";
|
|
|
12
12
|
|
|
13
13
|
describe.sequential("Database Fragment Integration", () => {
|
|
14
14
|
// Schema 1: Users fragment
|
|
15
|
-
const usersSchema = schema((s) => {
|
|
15
|
+
const usersSchema = schema("users", (s) => {
|
|
16
16
|
return s
|
|
17
17
|
.addTable("users", (t) => {
|
|
18
18
|
return t
|
|
@@ -36,7 +36,7 @@ describe.sequential("Database Fragment Integration", () => {
|
|
|
36
36
|
});
|
|
37
37
|
|
|
38
38
|
// Schema 2: Orders fragment
|
|
39
|
-
const ordersSchema = schema((s) => {
|
|
39
|
+
const ordersSchema = schema("orders", (s) => {
|
|
40
40
|
return s.addTable("orders", (t) => {
|
|
41
41
|
return t
|
|
42
42
|
.addColumn("id", idColumn())
|
|
@@ -50,7 +50,7 @@ describe.sequential("Database Fragment Integration", () => {
|
|
|
50
50
|
|
|
51
51
|
// Define Users Fragment using the new unified serviceTx API
|
|
52
52
|
const usersFragmentDef = defineFragment("users-fragment")
|
|
53
|
-
.extend(withDatabase(usersSchema
|
|
53
|
+
.extend(withDatabase(usersSchema))
|
|
54
54
|
.providesService("userService", ({ defineService }) => {
|
|
55
55
|
return defineService({
|
|
56
56
|
// Creates a user - returns TxResult<FragnoId>
|
|
@@ -118,7 +118,7 @@ describe.sequential("Database Fragment Integration", () => {
|
|
|
118
118
|
|
|
119
119
|
// Define Orders Fragment with cross-fragment service dependency using new serviceTx API
|
|
120
120
|
const ordersFragmentDef = defineFragment("orders-fragment")
|
|
121
|
-
.extend(withDatabase(ordersSchema
|
|
121
|
+
.extend(withDatabase(ordersSchema))
|
|
122
122
|
.usesService<
|
|
123
123
|
"userService",
|
|
124
124
|
{
|
|
@@ -209,7 +209,7 @@ describe.sequential("Database Fragment Integration", () => {
|
|
|
209
209
|
}),
|
|
210
210
|
]);
|
|
211
211
|
|
|
212
|
-
let adapter:
|
|
212
|
+
let adapter: SqlAdapter;
|
|
213
213
|
let usersFragment: ReturnType<typeof instantiateUsersFragment>;
|
|
214
214
|
let ordersFragment: ReturnType<typeof instantiateOrdersFragment>;
|
|
215
215
|
|
|
@@ -239,7 +239,7 @@ describe.sequential("Database Fragment Integration", () => {
|
|
|
239
239
|
beforeAll(async () => {
|
|
240
240
|
// Create in-memory SQLite database with Kysely
|
|
241
241
|
const { dialect } = new SQLocalKysely(":memory:");
|
|
242
|
-
adapter = new
|
|
242
|
+
adapter = new SqlAdapter({
|
|
243
243
|
dialect,
|
|
244
244
|
driverConfig: new SQLocalDriverConfig(),
|
|
245
245
|
});
|
|
@@ -414,6 +414,8 @@ describe.sequential("Database Fragment Integration", () => {
|
|
|
414
414
|
|
|
415
415
|
const result = await usersFragment.inContext(async function () {
|
|
416
416
|
return await this.handlerTx()
|
|
417
|
+
// Add a retrieve op so retry is allowed for the forced conflict below.
|
|
418
|
+
.retrieve(({ forSchema }) => forSchema(usersSchema).find("users"))
|
|
417
419
|
.mutate(({ forSchema, idempotencyKey, currentAttempt }) => {
|
|
418
420
|
if (currentAttempt === 0) {
|
|
419
421
|
firstIdempotencyKey = idempotencyKey;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { createDurableHooksDispatcherDurableObject } from "./index";
|
|
3
|
+
|
|
4
|
+
describe("createDurableHooksDispatcherDurableObject", () => {
|
|
5
|
+
it("should schedule an initial alarm on creation", async () => {
|
|
6
|
+
const process = vi.fn().mockResolvedValue(0);
|
|
7
|
+
const getNextWakeAt = vi.fn().mockResolvedValue(new Date());
|
|
8
|
+
const drain = vi.fn().mockResolvedValue(undefined);
|
|
9
|
+
const setAlarm = vi.fn().mockResolvedValue(undefined);
|
|
10
|
+
|
|
11
|
+
const handlerFactory = createDurableHooksDispatcherDurableObject({
|
|
12
|
+
createProcessor: () => ({ process, getNextWakeAt, drain, namespace: "test" }),
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
handlerFactory({ storage: { setAlarm } }, {});
|
|
16
|
+
|
|
17
|
+
await Promise.resolve();
|
|
18
|
+
expect(setAlarm).toHaveBeenCalledTimes(1);
|
|
19
|
+
expect(process).not.toHaveBeenCalled();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("should delete the alarm when no pending hooks exist", async () => {
|
|
23
|
+
const process = vi.fn().mockResolvedValue(0);
|
|
24
|
+
const getNextWakeAt = vi.fn().mockResolvedValue(null);
|
|
25
|
+
const drain = vi.fn().mockResolvedValue(undefined);
|
|
26
|
+
const setAlarm = vi.fn().mockResolvedValue(undefined);
|
|
27
|
+
const deleteAlarm = vi.fn().mockResolvedValue(undefined);
|
|
28
|
+
|
|
29
|
+
const handlerFactory = createDurableHooksDispatcherDurableObject({
|
|
30
|
+
createProcessor: () => ({ process, getNextWakeAt, drain, namespace: "test" }),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const handler = handlerFactory({ storage: { setAlarm, deleteAlarm } }, {});
|
|
34
|
+
|
|
35
|
+
await Promise.resolve();
|
|
36
|
+
expect(getNextWakeAt).toHaveBeenCalledTimes(1);
|
|
37
|
+
|
|
38
|
+
await handler.alarm?.();
|
|
39
|
+
|
|
40
|
+
expect(process).toHaveBeenCalledTimes(1);
|
|
41
|
+
expect(deleteAlarm).toHaveBeenCalledTimes(2);
|
|
42
|
+
expect(setAlarm).not.toHaveBeenCalled();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("should schedule alarm using max(nextWakeAt, now)", async () => {
|
|
46
|
+
vi.useFakeTimers();
|
|
47
|
+
const now = new Date("2024-01-01T00:00:00Z");
|
|
48
|
+
vi.setSystemTime(now);
|
|
49
|
+
|
|
50
|
+
const process = vi.fn().mockResolvedValue(0);
|
|
51
|
+
const getNextWakeAt = vi.fn().mockResolvedValue(new Date(now.getTime() - 10000));
|
|
52
|
+
const drain = vi.fn().mockResolvedValue(undefined);
|
|
53
|
+
const setAlarm = vi.fn().mockResolvedValue(undefined);
|
|
54
|
+
const deleteAlarm = vi.fn().mockResolvedValue(undefined);
|
|
55
|
+
|
|
56
|
+
const handlerFactory = createDurableHooksDispatcherDurableObject({
|
|
57
|
+
createProcessor: () => ({ process, getNextWakeAt, drain, namespace: "test" }),
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const handler = handlerFactory({ storage: { setAlarm, deleteAlarm } }, {});
|
|
61
|
+
|
|
62
|
+
await Promise.resolve();
|
|
63
|
+
expect(setAlarm).toHaveBeenCalledTimes(1);
|
|
64
|
+
|
|
65
|
+
await handler.alarm?.();
|
|
66
|
+
|
|
67
|
+
expect(setAlarm).toHaveBeenCalledTimes(2);
|
|
68
|
+
for (const [scheduledAt] of setAlarm.mock.calls) {
|
|
69
|
+
expect(scheduledAt.getTime()).toBeGreaterThanOrEqual(now.getTime());
|
|
70
|
+
}
|
|
71
|
+
vi.useRealTimers();
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { DurableHooksProcessor } from "../../hooks/durable-hooks-processor";
|
|
2
|
+
|
|
3
|
+
type AlarmStorage = {
|
|
4
|
+
setAlarm?: (timestamp: number | Date) => Promise<void>;
|
|
5
|
+
deleteAlarm?: () => Promise<void>;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export type DurableHooksDispatcherDurableObjectState = {
|
|
9
|
+
readonly storage: AlarmStorage;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type DurableHooksDispatcherDurableObjectHandler = {
|
|
13
|
+
fetch?: (request: Request) => Promise<Response>;
|
|
14
|
+
alarm?: () => Promise<void>;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type DurableHooksDispatcherDurableObjectFactory<TEnv = unknown> = (
|
|
18
|
+
state: DurableHooksDispatcherDurableObjectState,
|
|
19
|
+
env: TEnv,
|
|
20
|
+
) => DurableHooksDispatcherDurableObjectHandler;
|
|
21
|
+
|
|
22
|
+
export type DurableHooksDispatcherDurableObjectOptions<TEnv = unknown> = {
|
|
23
|
+
createProcessor: (context: {
|
|
24
|
+
state: DurableHooksDispatcherDurableObjectState;
|
|
25
|
+
env: TEnv;
|
|
26
|
+
}) => DurableHooksProcessor;
|
|
27
|
+
onProcessError?: (error: unknown) => void;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export function createDurableHooksDispatcherDurableObject<TEnv>(
|
|
31
|
+
options: DurableHooksDispatcherDurableObjectOptions<TEnv>,
|
|
32
|
+
): DurableHooksDispatcherDurableObjectFactory<TEnv> {
|
|
33
|
+
return (state, env) => {
|
|
34
|
+
const processor = options.createProcessor({ state, env });
|
|
35
|
+
const onProcessError =
|
|
36
|
+
options.onProcessError ??
|
|
37
|
+
((error: unknown) => {
|
|
38
|
+
console.error("Durable hooks dispatcher error", error);
|
|
39
|
+
});
|
|
40
|
+
const rawSetAlarm = state.storage.setAlarm;
|
|
41
|
+
const rawDeleteAlarm = state.storage.deleteAlarm;
|
|
42
|
+
|
|
43
|
+
if (!rawSetAlarm) {
|
|
44
|
+
throw new Error(
|
|
45
|
+
"Durable hooks dispatcher requires state.storage.setAlarm to schedule alarms.",
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
const setAlarm = rawSetAlarm.bind(state.storage);
|
|
49
|
+
const deleteAlarm = rawDeleteAlarm?.bind(state.storage);
|
|
50
|
+
|
|
51
|
+
let processing = false;
|
|
52
|
+
let queued = false;
|
|
53
|
+
let currentPromise: Promise<void> | undefined;
|
|
54
|
+
|
|
55
|
+
const runProcess = () => {
|
|
56
|
+
if (processing) {
|
|
57
|
+
queued = true;
|
|
58
|
+
return currentPromise ?? Promise.resolve();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
processing = true;
|
|
62
|
+
currentPromise = (async () => {
|
|
63
|
+
do {
|
|
64
|
+
queued = false;
|
|
65
|
+
try {
|
|
66
|
+
await processor.process();
|
|
67
|
+
} catch (error) {
|
|
68
|
+
onProcessError(error);
|
|
69
|
+
}
|
|
70
|
+
} while (queued);
|
|
71
|
+
processing = false;
|
|
72
|
+
})();
|
|
73
|
+
|
|
74
|
+
return currentPromise;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const scheduleNextAlarm = async () => {
|
|
78
|
+
const nextWakeAt = await processor.getNextWakeAt();
|
|
79
|
+
if (!nextWakeAt) {
|
|
80
|
+
await deleteAlarm?.();
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const now = Date.now();
|
|
85
|
+
const scheduledAt = new Date(Math.max(nextWakeAt.getTime(), now));
|
|
86
|
+
await setAlarm(scheduledAt);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
void scheduleNextAlarm().catch((error) => {
|
|
90
|
+
onProcessError(error);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
alarm: async () => {
|
|
95
|
+
try {
|
|
96
|
+
await runProcess();
|
|
97
|
+
await scheduleNextAlarm();
|
|
98
|
+
} catch (error) {
|
|
99
|
+
onProcessError(error);
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
};
|
|
104
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { createDurableHooksDispatcher } from "./index";
|
|
3
|
+
|
|
4
|
+
describe("createDurableHooksDispatcher", () => {
|
|
5
|
+
it("should wake and process hooks", async () => {
|
|
6
|
+
const process = vi.fn().mockResolvedValue(0);
|
|
7
|
+
const getNextWakeAt = vi.fn().mockResolvedValue(null);
|
|
8
|
+
const drain = vi.fn().mockResolvedValue(undefined);
|
|
9
|
+
|
|
10
|
+
const dispatcher = createDurableHooksDispatcher({
|
|
11
|
+
processor: { process, getNextWakeAt, drain, namespace: "test" },
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
await dispatcher.wake();
|
|
15
|
+
|
|
16
|
+
expect(process).toHaveBeenCalledTimes(1);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("should coalesce overlapping wake calls", async () => {
|
|
20
|
+
let resolveFirst!: (value: number) => void;
|
|
21
|
+
const firstPromise = new Promise<number>((resolve) => {
|
|
22
|
+
resolveFirst = resolve;
|
|
23
|
+
});
|
|
24
|
+
const process = vi.fn().mockReturnValueOnce(firstPromise).mockResolvedValue(0);
|
|
25
|
+
const drain = vi.fn().mockResolvedValue(undefined);
|
|
26
|
+
|
|
27
|
+
const dispatcher = createDurableHooksDispatcher({
|
|
28
|
+
processor: {
|
|
29
|
+
process,
|
|
30
|
+
getNextWakeAt: vi.fn().mockResolvedValue(null),
|
|
31
|
+
drain,
|
|
32
|
+
namespace: "test",
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const first = dispatcher.wake();
|
|
37
|
+
const second = dispatcher.wake();
|
|
38
|
+
|
|
39
|
+
expect(process).toHaveBeenCalledTimes(1);
|
|
40
|
+
|
|
41
|
+
resolveFirst(0);
|
|
42
|
+
await first;
|
|
43
|
+
await second;
|
|
44
|
+
|
|
45
|
+
expect(process).toHaveBeenCalledTimes(2);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("should poll and process when due", async () => {
|
|
49
|
+
vi.useFakeTimers();
|
|
50
|
+
const now = new Date("2024-01-01T00:00:00Z");
|
|
51
|
+
vi.setSystemTime(now);
|
|
52
|
+
|
|
53
|
+
const process = vi.fn().mockResolvedValue(0);
|
|
54
|
+
const getNextWakeAt = vi.fn().mockResolvedValue(new Date(now.getTime() - 1000));
|
|
55
|
+
const drain = vi.fn().mockResolvedValue(undefined);
|
|
56
|
+
|
|
57
|
+
const dispatcher = createDurableHooksDispatcher({
|
|
58
|
+
processor: { process, getNextWakeAt, drain, namespace: "test" },
|
|
59
|
+
pollIntervalMs: 1000,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
dispatcher.startPolling();
|
|
63
|
+
await vi.advanceTimersByTimeAsync(1000);
|
|
64
|
+
dispatcher.stopPolling();
|
|
65
|
+
|
|
66
|
+
expect(process).toHaveBeenCalledTimes(1);
|
|
67
|
+
vi.useRealTimers();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("should skip polling when next wake is in the future", async () => {
|
|
71
|
+
vi.useFakeTimers();
|
|
72
|
+
const now = new Date("2024-01-01T00:00:00Z");
|
|
73
|
+
vi.setSystemTime(now);
|
|
74
|
+
|
|
75
|
+
const process = vi.fn().mockResolvedValue(0);
|
|
76
|
+
const getNextWakeAt = vi.fn().mockResolvedValue(new Date(now.getTime() + 60000));
|
|
77
|
+
const drain = vi.fn().mockResolvedValue(undefined);
|
|
78
|
+
|
|
79
|
+
const dispatcher = createDurableHooksDispatcher({
|
|
80
|
+
processor: { process, getNextWakeAt, drain, namespace: "test" },
|
|
81
|
+
pollIntervalMs: 1000,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
dispatcher.startPolling();
|
|
85
|
+
await vi.advanceTimersByTimeAsync(1000);
|
|
86
|
+
dispatcher.stopPolling();
|
|
87
|
+
|
|
88
|
+
expect(process).not.toHaveBeenCalled();
|
|
89
|
+
vi.useRealTimers();
|
|
90
|
+
});
|
|
91
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { DurableHooksProcessor } from "../../hooks/durable-hooks-processor";
|
|
2
|
+
|
|
3
|
+
export type DurableHooksDispatcher = {
|
|
4
|
+
wake: () => Promise<void>;
|
|
5
|
+
startPolling: () => void;
|
|
6
|
+
stopPolling: () => void;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export type DurableHooksDispatcherOptions = {
|
|
10
|
+
processor: DurableHooksProcessor;
|
|
11
|
+
pollIntervalMs?: number;
|
|
12
|
+
onError?: (error: unknown) => void;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export function createDurableHooksDispatcher(
|
|
16
|
+
options: DurableHooksDispatcherOptions,
|
|
17
|
+
): DurableHooksDispatcher {
|
|
18
|
+
const pollIntervalMs = options.pollIntervalMs ?? 5000;
|
|
19
|
+
const onError =
|
|
20
|
+
options.onError ??
|
|
21
|
+
((error: unknown) => {
|
|
22
|
+
console.error("Durable hooks dispatcher error", error);
|
|
23
|
+
});
|
|
24
|
+
let timer: ReturnType<typeof setInterval> | undefined;
|
|
25
|
+
let processing = false;
|
|
26
|
+
let queued = false;
|
|
27
|
+
let currentPromise: Promise<void> | undefined;
|
|
28
|
+
|
|
29
|
+
const runProcess = () => {
|
|
30
|
+
if (processing) {
|
|
31
|
+
queued = true;
|
|
32
|
+
return currentPromise ?? Promise.resolve();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
processing = true;
|
|
36
|
+
currentPromise = (async () => {
|
|
37
|
+
do {
|
|
38
|
+
queued = false;
|
|
39
|
+
try {
|
|
40
|
+
await options.processor.process();
|
|
41
|
+
} catch (error) {
|
|
42
|
+
onError(error);
|
|
43
|
+
}
|
|
44
|
+
} while (queued);
|
|
45
|
+
processing = false;
|
|
46
|
+
})();
|
|
47
|
+
|
|
48
|
+
return currentPromise;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const poll = async () => {
|
|
52
|
+
try {
|
|
53
|
+
const nextWakeAt = await options.processor.getNextWakeAt();
|
|
54
|
+
if (!nextWakeAt) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (Date.now() >= nextWakeAt.getTime()) {
|
|
58
|
+
await runProcess();
|
|
59
|
+
}
|
|
60
|
+
} catch (error) {
|
|
61
|
+
onError(error);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
wake: async () => {
|
|
67
|
+
await runProcess();
|
|
68
|
+
},
|
|
69
|
+
startPolling: () => {
|
|
70
|
+
if (timer) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
timer = setInterval(() => {
|
|
75
|
+
void poll();
|
|
76
|
+
}, pollIntervalMs);
|
|
77
|
+
},
|
|
78
|
+
stopPolling: () => {
|
|
79
|
+
if (!timer) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
clearInterval(timer);
|
|
84
|
+
timer = undefined;
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { defineRoutes } from "@fragno-dev/core";
|
|
2
|
+
import { internalFragmentDef } from "./internal-fragment";
|
|
3
|
+
|
|
4
|
+
export const internalFragmentRoutes = defineRoutes(internalFragmentDef).create(
|
|
5
|
+
({ defineRoute, services }) => [
|
|
6
|
+
defineRoute({
|
|
7
|
+
method: "GET",
|
|
8
|
+
path: "/outbox",
|
|
9
|
+
handler: async function (input, { json }) {
|
|
10
|
+
// We intentionally skip input/output schemas here to keep the internal route lightweight.
|
|
11
|
+
// Query params are validated manually and the response shape is stable (OutboxEntry[]),
|
|
12
|
+
// while the public API surface is still gated behind adapter config.
|
|
13
|
+
const afterVersionstamp = input.query.get("afterVersionstamp") ?? undefined;
|
|
14
|
+
const limitValue = input.query.get("limit");
|
|
15
|
+
let limit: number | undefined;
|
|
16
|
+
|
|
17
|
+
if (limitValue !== null) {
|
|
18
|
+
const parsed = Number.parseInt(limitValue, 10);
|
|
19
|
+
if (!Number.isFinite(parsed) || parsed < 1) {
|
|
20
|
+
return json(
|
|
21
|
+
{
|
|
22
|
+
error: "Invalid limit query parameter.",
|
|
23
|
+
code: "INVALID_LIMIT",
|
|
24
|
+
},
|
|
25
|
+
{ status: 400 },
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
limit = parsed;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const entries = await this.handlerTx()
|
|
32
|
+
.withServiceCalls(
|
|
33
|
+
() => [services.outboxService.list({ afterVersionstamp, limit })] as const,
|
|
34
|
+
)
|
|
35
|
+
.transform(({ serviceResult: [result] }) => result)
|
|
36
|
+
.execute();
|
|
37
|
+
|
|
38
|
+
return json(entries);
|
|
39
|
+
},
|
|
40
|
+
}),
|
|
41
|
+
],
|
|
42
|
+
);
|