@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 +1 @@
|
|
|
1
|
-
{"version":3,"file":"internal-fragment.js","names":["version"],"sources":["../../src/fragments/internal-fragment.ts"],"sourcesContent":["import { FragmentDefinitionBuilder } from \"@fragno-dev/core\";\nimport type { InstantiatedFragmentFromDefinition } from \"@fragno-dev/core\";\nimport {\n DatabaseFragmentDefinitionBuilder,\n type DatabaseHandlerContext,\n type DatabaseRequestStorage,\n type DatabaseServiceContext,\n type FragnoPublicConfigWithDatabase,\n type ImplicitDatabaseDependencies,\n} from \"../db-fragment-definition-builder\";\nimport type { FragnoId } from \"../schema/create\";\nimport { schema, idColumn, column } from \"../schema/create\";\nimport type { RetryPolicy } from \"../query/unit-of-work/retry-policy\";\n\n// Constants for Fragno's internal settings table\nexport const SETTINGS_TABLE_NAME = \"fragno_db_settings\" as const;\n// FIXME: In some places we simply use empty string \"\" as namespace, which is not correct.\nexport const SETTINGS_NAMESPACE = \"fragno-db-settings\" as const;\n\nexport const internalSchema = schema((s) => {\n return s\n .addTable(SETTINGS_TABLE_NAME, (t) => {\n return t\n .addColumn(\"id\", idColumn())\n .addColumn(\"key\", column(\"string\"))\n .addColumn(\"value\", column(\"string\"))\n .createIndex(\"unique_key\", [\"key\"], { unique: true });\n })\n .addTable(\"fragno_hooks\", (t) => {\n return t\n .addColumn(\"id\", idColumn())\n .addColumn(\"namespace\", column(\"string\"))\n .addColumn(\"hookName\", column(\"string\"))\n .addColumn(\"payload\", column(\"json\"))\n .addColumn(\"status\", column(\"string\")) // \"pending\" | \"processing\" | \"completed\" | \"failed\"\n .addColumn(\"attempts\", column(\"integer\").defaultTo(0))\n .addColumn(\"maxAttempts\", column(\"integer\").defaultTo(5))\n .addColumn(\"lastAttemptAt\", column(\"timestamp\").nullable())\n .addColumn(\"nextRetryAt\", column(\"timestamp\").nullable())\n .addColumn(\"error\", column(\"string\").nullable())\n .addColumn(\n \"createdAt\",\n column(\"timestamp\").defaultTo((b) => b.now()),\n )\n .addColumn(\"nonce\", column(\"string\"))\n .createIndex(\"idx_namespace_status_retry\", [\"namespace\", \"status\", \"nextRetryAt\"])\n .createIndex(\"idx_nonce\", [\"nonce\"]);\n });\n});\n\n// This uses DatabaseFragmentDefinitionBuilder directly\n// to avoid circular dependency (it doesn't need to link to itself)\nexport const internalFragmentDef = new DatabaseFragmentDefinitionBuilder(\n new FragmentDefinitionBuilder<\n {},\n FragnoPublicConfigWithDatabase,\n ImplicitDatabaseDependencies<typeof internalSchema>,\n {},\n {},\n {},\n {},\n DatabaseServiceContext<{}>,\n DatabaseHandlerContext,\n DatabaseRequestStorage\n >(\"$fragno-internal-fragment\"),\n internalSchema,\n \"\", // intentionally blank namespace so there is no prefix\n)\n .providesService(\"settingsService\", ({ defineService }) => {\n return defineService({\n async get(\n namespace: string,\n key: string,\n ): Promise<{ id: FragnoId; key: string; value: string } | undefined> {\n const fullKey = `${namespace}.${key}`;\n const uow = this.uow(internalSchema).find(SETTINGS_TABLE_NAME, (b) =>\n b.whereIndex(\"unique_key\", (eb) => eb(\"key\", \"=\", fullKey)),\n );\n const [results] = await uow.retrievalPhase;\n return results?.[0];\n },\n\n async set(namespace: string, key: string, value: string) {\n const fullKey = `${namespace}.${key}`;\n const uow = this.uow(internalSchema);\n\n // First, find if the key already exists\n const findUow = uow.find(SETTINGS_TABLE_NAME, (b) =>\n b.whereIndex(\"unique_key\", (eb) => eb(\"key\", \"=\", fullKey)),\n );\n const [existing] = await findUow.retrievalPhase;\n\n if (existing?.[0]) {\n uow.update(SETTINGS_TABLE_NAME, existing[0].id, (b) => b.set({ value }).check());\n } else {\n uow.create(SETTINGS_TABLE_NAME, {\n key: fullKey,\n value,\n });\n }\n\n // Await mutation phase - will throw if mutation fails\n await uow.mutationPhase;\n },\n\n async delete(id: FragnoId) {\n const uow = this.uow(internalSchema);\n uow.delete(SETTINGS_TABLE_NAME, id);\n await uow.mutationPhase;\n },\n });\n })\n .providesService(\"hookService\", ({ defineService }) => {\n return defineService({\n /**\n * Get pending hook events for processing.\n * Returns all pending events for the given namespace that are ready to be processed.\n */\n async getPendingHookEvents(namespace: string): Promise<\n {\n id: FragnoId;\n hookName: string;\n payload: unknown;\n attempts: number;\n maxAttempts: number;\n nonce: string;\n }[]\n > {\n const uow = this.uow(internalSchema).find(\"fragno_hooks\", (b) =>\n b.whereIndex(\"idx_namespace_status_retry\", (eb) =>\n eb.and(eb(\"namespace\", \"=\", namespace), eb(\"status\", \"=\", \"pending\")),\n ),\n );\n\n const [events] = await uow.retrievalPhase;\n\n // Filter for pending status and events ready for retry\n const now = new Date();\n const ready = events.filter((event) => {\n // FIXME(Wilco): this should be handled by the database query, but there seems to be an issue.\n if (!event.nextRetryAt) {\n return true; // Newly created events (nextRetryAt = null) are ready\n }\n return event.nextRetryAt <= now; // Only include if retry time has passed\n });\n\n return ready.map((event) => ({\n id: event.id,\n hookName: event.hookName,\n payload: event.payload,\n attempts: event.attempts,\n maxAttempts: event.maxAttempts,\n nonce: event.nonce,\n }));\n },\n\n /**\n * Mark a hook event as completed.\n */\n markHookCompleted(eventId: FragnoId): void {\n const uow = this.uow(internalSchema);\n uow.update(\"fragno_hooks\", eventId, (b) =>\n b.set({ status: \"completed\", lastAttemptAt: new Date() }).check(),\n );\n },\n\n /**\n * Mark a hook event as failed and schedule next retry.\n */\n markHookFailed(\n eventId: FragnoId,\n error: string,\n attempts: number,\n retryPolicy: RetryPolicy,\n ): void {\n const uow = this.uow(internalSchema);\n\n const newAttempts = attempts + 1;\n const shouldRetry = retryPolicy.shouldRetry(newAttempts - 1);\n\n if (shouldRetry) {\n const delayMs = retryPolicy.getDelayMs(newAttempts - 1);\n const nextRetryAt = new Date(Date.now() + delayMs);\n uow.update(\"fragno_hooks\", eventId, (b) =>\n b\n .set({\n status: \"pending\",\n attempts: newAttempts,\n lastAttemptAt: new Date(),\n nextRetryAt,\n error,\n })\n .check(),\n );\n } else {\n uow.update(\"fragno_hooks\", eventId, (b) =>\n b\n .set({\n status: \"failed\",\n attempts: newAttempts,\n lastAttemptAt: new Date(),\n error,\n })\n .check(),\n );\n }\n },\n\n /**\n * Mark a hook event as processing (to prevent concurrent execution).\n */\n markHookProcessing(eventId: FragnoId): void {\n const uow = this.uow(internalSchema);\n uow.update(\"fragno_hooks\", eventId, (b) =>\n b.set({ status: \"processing\", lastAttemptAt: new Date() }).check(),\n );\n },\n });\n })\n .build();\n\n/**\n * Type representing an instantiated internal fragment.\n * This is the fragment that manages Fragno's internal settings table.\n */\nexport type InternalFragmentInstance = InstantiatedFragmentFromDefinition<\n typeof internalFragmentDef\n>;\n\nexport async function getSchemaVersionFromDatabase(\n fragment: InternalFragmentInstance,\n namespace: string,\n): Promise<number> {\n try {\n const version = await fragment.inContext(async function () {\n const version = await this.uow(async ({ executeRetrieve }) => {\n const version = fragment.services.settingsService.get(namespace, \"schema_version\");\n await executeRetrieve();\n return version;\n });\n\n return version ? parseInt(version.value, 10) : 0;\n });\n return version;\n } catch {\n return 0;\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"internal-fragment.js","names":["earliestStaleAt: Date | null","earliestPendingAt: Date | null"],"sources":["../../src/fragments/internal-fragment.ts"],"sourcesContent":["import { FragmentDefinitionBuilder } from \"@fragno-dev/core\";\nimport type { InstantiatedFragmentFromDefinition } from \"@fragno-dev/core\";\nimport {\n DatabaseFragmentDefinitionBuilder,\n type DatabaseHandlerContext,\n type DatabaseRequestStorage,\n type DatabaseServiceContext,\n type FragnoPublicConfigWithDatabase,\n type ImplicitDatabaseDependencies,\n} from \"../db-fragment-definition-builder\";\nimport { FragnoId } from \"../schema/create\";\nimport type { RetryPolicy } from \"../query/unit-of-work/retry-policy\";\nimport { dbNow } from \"../query/db-now\";\nimport {\n internalSchema,\n SETTINGS_NAMESPACE,\n SETTINGS_TABLE_NAME,\n} from \"./internal-fragment.schema\";\n\nexport {\n internalSchema,\n SETTINGS_NAMESPACE,\n SETTINGS_TABLE_NAME,\n} from \"./internal-fragment.schema\";\n\n// This uses DatabaseFragmentDefinitionBuilder directly\n// to avoid circular dependency (it doesn't need to link to itself)\nexport const internalFragmentDef = new DatabaseFragmentDefinitionBuilder(\n new FragmentDefinitionBuilder<\n {},\n FragnoPublicConfigWithDatabase,\n ImplicitDatabaseDependencies<typeof internalSchema>,\n {},\n {},\n {},\n {},\n DatabaseServiceContext<{}>,\n DatabaseHandlerContext,\n DatabaseRequestStorage\n >(\"$fragno-internal-fragment\"),\n internalSchema,\n)\n .providesBaseService(({ deps }) => ({\n getDbNow: async () => {\n if (deps.db.now) {\n return deps.db.now();\n }\n return new Date();\n },\n }))\n .providesService(\"settingsService\", ({ defineService }) => {\n return defineService({\n /**\n * Get a setting by namespace and key.\n */\n get(namespace: string, key: string) {\n const fullKey = `${namespace}.${key}`;\n return this.serviceTx(internalSchema)\n .retrieve((uow) =>\n uow.findFirst(SETTINGS_TABLE_NAME, (b) =>\n b.whereIndex(\"unique_key\", (eb) => eb(\"key\", \"=\", fullKey)),\n ),\n )\n .transformRetrieve(\n ([result]): { id: FragnoId; key: string; value: string } | undefined =>\n result ?? undefined,\n )\n .build();\n },\n\n /**\n * Set a setting value by namespace and key.\n */\n set(namespace: string, key: string, value: string) {\n const fullKey = `${namespace}.${key}`;\n return this.serviceTx(internalSchema)\n .retrieve((uow) =>\n uow.findFirst(SETTINGS_TABLE_NAME, (b) =>\n b.whereIndex(\"unique_key\", (eb) => eb(\"key\", \"=\", fullKey)),\n ),\n )\n .transformRetrieve(([result]) => result)\n .mutate(({ uow, retrieveResult }) => {\n if (retrieveResult) {\n uow.update(SETTINGS_TABLE_NAME, retrieveResult.id, (b) => b.set({ value }).check());\n } else {\n uow.create(SETTINGS_TABLE_NAME, {\n key: fullKey,\n value,\n });\n }\n })\n .build();\n },\n\n /**\n * Delete a setting by ID.\n */\n delete(id: FragnoId) {\n return this.serviceTx(internalSchema)\n .mutate(({ uow }) => uow.delete(SETTINGS_TABLE_NAME, id))\n .build();\n },\n });\n })\n .providesService(\"hookService\", ({ defineService }) => {\n return defineService({\n /**\n * Get pending hook events for processing.\n * Returns all pending events for the given namespace that are ready to be processed.\n */\n getPendingHookEvents(namespace: string) {\n const now = dbNow();\n return this.serviceTx(internalSchema)\n .retrieve((uow) =>\n uow.find(\"fragno_hooks\", (b) =>\n b.whereIndex(\"idx_namespace_status_retry\", (eb) =>\n eb.and(\n eb(\"namespace\", \"=\", namespace),\n eb(\"status\", \"=\", \"pending\"),\n eb.or(eb.isNull(\"nextRetryAt\"), eb(\"nextRetryAt\", \"<=\", now)),\n ),\n ),\n ),\n )\n .transformRetrieve(([events]) => {\n return events.map((event) => ({\n id: event.id,\n hookName: event.hookName,\n payload: event.payload as unknown,\n attempts: event.attempts,\n maxAttempts: event.maxAttempts,\n idempotencyKey: event.nonce,\n }));\n })\n .build();\n },\n\n /**\n * Claim pending hook events for processing.\n * Returns ready events and marks them as processing in the same transaction.\n */\n claimPendingHookEvents(namespace: string) {\n const now = dbNow();\n return this.serviceTx(internalSchema)\n .retrieve((uow) =>\n uow.find(\"fragno_hooks\", (b) =>\n b.whereIndex(\"idx_namespace_status_retry\", (eb) =>\n eb.and(\n eb(\"namespace\", \"=\", namespace),\n eb(\"status\", \"=\", \"pending\"),\n eb.or(eb.isNull(\"nextRetryAt\"), eb(\"nextRetryAt\", \"<=\", now)),\n ),\n ),\n ),\n )\n .transformRetrieve(([events]) => {\n return events.map((event) => ({\n id: event.id,\n hookName: event.hookName,\n payload: event.payload,\n attempts: event.attempts,\n maxAttempts: event.maxAttempts,\n idempotencyKey: event.nonce,\n }));\n })\n .mutate(({ uow, retrieveResult }) => {\n if (retrieveResult.length === 0) {\n return;\n }\n for (const event of retrieveResult) {\n uow.update(\"fragno_hooks\", event.id, (b) =>\n b.set({ status: \"processing\", lastAttemptAt: now }).check(),\n );\n }\n })\n .transform(({ retrieveResult }) =>\n retrieveResult.map((event) => ({\n ...event,\n id: new FragnoId({\n externalId: event.id.externalId,\n internalId: event.id.internalId,\n version: event.id.version + 1,\n }),\n })),\n )\n .build();\n },\n\n /**\n * Re-queue hook events that have been stuck in processing for too long.\n */\n requeueStuckProcessingHooks(namespace: string, staleBefore: Date) {\n return this.serviceTx(internalSchema)\n .retrieve((uow) =>\n uow.find(\"fragno_hooks\", (b) =>\n b.whereIndex(\"idx_namespace_status_retry\", (eb) =>\n eb.and(eb(\"namespace\", \"=\", namespace), eb(\"status\", \"=\", \"processing\")),\n ),\n ),\n )\n .transformRetrieve(([events]) => {\n const stuck = events.filter((event) => {\n if (!event.lastAttemptAt) {\n return true;\n }\n return event.lastAttemptAt <= staleBefore;\n });\n\n return stuck.map((event) => ({\n id: event.id,\n hookName: event.hookName,\n attempts: event.attempts,\n maxAttempts: event.maxAttempts,\n lastAttemptAt: event.lastAttemptAt,\n nextRetryAt: event.nextRetryAt,\n }));\n })\n .mutate(({ uow, retrieveResult }) => {\n for (const event of retrieveResult) {\n uow.update(\"fragno_hooks\", event.id, (b) =>\n b.set({ status: \"pending\", nextRetryAt: null }).check(),\n );\n }\n })\n .transform(({ retrieveResult }) => retrieveResult)\n .build();\n },\n\n /**\n * Get the next time a processing hook becomes stale.\n */\n getNextProcessingStaleAt(namespace: string, timeoutMinutes: number, now?: Date) {\n return this.serviceTx(internalSchema)\n .retrieve((uow) =>\n uow.find(\"fragno_hooks\", (b) =>\n b.whereIndex(\"idx_namespace_status_retry\", (eb) =>\n eb.and(eb(\"namespace\", \"=\", namespace), eb(\"status\", \"=\", \"processing\")),\n ),\n ),\n )\n .transformRetrieve(([events]) => {\n if (events.length === 0) {\n return null;\n }\n\n const baseNow = now ?? new Date();\n const nowMs = baseNow.getTime();\n const timeoutMs = timeoutMinutes * 60_000;\n let earliestStaleAt: Date | null = null;\n\n for (const event of events) {\n if (!event.lastAttemptAt) {\n return baseNow;\n }\n\n const staleAtMs = event.lastAttemptAt.getTime() + timeoutMs;\n if (staleAtMs <= nowMs) {\n return baseNow;\n }\n\n const staleAt = new Date(staleAtMs);\n if (!earliestStaleAt || staleAt < earliestStaleAt) {\n earliestStaleAt = staleAt;\n }\n }\n\n return earliestStaleAt;\n })\n .build();\n },\n\n /**\n * Get the earliest pending hook wake time for a namespace.\n * Optionally considers processing hooks becoming stale when timeoutMinutes is provided.\n */\n getNextHookWakeAt(namespace: string, timeoutMinutes?: number | false, now?: Date) {\n const baseNow = now ?? new Date();\n const includeProcessing = typeof timeoutMinutes === \"number\" && timeoutMinutes > 0;\n const timeoutMs = includeProcessing ? timeoutMinutes * 60_000 : 0;\n\n return this.serviceTx(internalSchema)\n .retrieve((uow) =>\n uow.find(\"fragno_hooks\", (b) =>\n b\n .whereIndex(\"idx_namespace_status_retry\", (eb) => {\n if (includeProcessing) {\n return eb.and(\n eb(\"namespace\", \"=\", namespace),\n eb.or(eb(\"status\", \"=\", \"pending\"), eb(\"status\", \"=\", \"processing\")),\n );\n }\n return eb.and(eb(\"namespace\", \"=\", namespace), eb(\"status\", \"=\", \"pending\"));\n })\n .select([\"status\", \"nextRetryAt\", \"lastAttemptAt\"]),\n ),\n )\n .transformRetrieve(([events]) => {\n if (events.length === 0) {\n return null;\n }\n\n const nowMs = baseNow.getTime();\n let earliestPendingAt: Date | null = null;\n let earliestStaleAt: Date | null = null;\n\n for (const event of events) {\n if (event.status === \"pending\") {\n const nextRetryAt = event.nextRetryAt;\n if (!nextRetryAt || nextRetryAt.getTime() <= nowMs) {\n return baseNow;\n }\n if (!earliestPendingAt || nextRetryAt < earliestPendingAt) {\n earliestPendingAt = nextRetryAt;\n }\n continue;\n }\n\n if (!includeProcessing || event.status !== \"processing\") {\n continue;\n }\n\n const lastAttemptAt = event.lastAttemptAt;\n if (!lastAttemptAt) {\n return baseNow;\n }\n\n const staleAtMs = lastAttemptAt.getTime() + timeoutMs;\n if (staleAtMs <= nowMs) {\n return baseNow;\n }\n\n const staleAt = new Date(staleAtMs);\n if (!earliestStaleAt || staleAt < earliestStaleAt) {\n earliestStaleAt = staleAt;\n }\n }\n\n if (!earliestPendingAt) {\n return earliestStaleAt ?? null;\n }\n if (!earliestStaleAt) {\n return earliestPendingAt;\n }\n return earliestPendingAt <= earliestStaleAt ? earliestPendingAt : earliestStaleAt;\n })\n .build();\n },\n\n /**\n * Mark a hook event as completed.\n */\n markHookCompleted(eventId: FragnoId) {\n return this.serviceTx(internalSchema)\n .mutate(({ uow }) =>\n uow.update(\"fragno_hooks\", eventId, (b) =>\n b.set({ status: \"completed\", lastAttemptAt: dbNow() }).check(),\n ),\n )\n .build();\n },\n\n /**\n * Mark a hook event as failed and schedule next retry.\n */\n markHookFailed(\n eventId: FragnoId,\n error: string,\n attempts: number,\n retryPolicy: RetryPolicy,\n now?: Date,\n ) {\n const newAttempts = attempts + 1;\n const shouldRetry = retryPolicy.shouldRetry(newAttempts - 1);\n\n return this.serviceTx(internalSchema)\n .mutate(({ uow }) => {\n if (shouldRetry) {\n const delayMs = retryPolicy.getDelayMs(newAttempts - 1);\n const baseNow = now ?? new Date();\n const nextRetryAt = new Date(baseNow.getTime() + delayMs);\n uow.update(\"fragno_hooks\", eventId, (b) =>\n b\n .set({\n status: \"pending\",\n attempts: newAttempts,\n lastAttemptAt: dbNow(),\n nextRetryAt,\n error,\n })\n .check(),\n );\n } else {\n uow.update(\"fragno_hooks\", eventId, (b) =>\n b\n .set({\n status: \"failed\",\n attempts: newAttempts,\n lastAttemptAt: dbNow(),\n error,\n })\n .check(),\n );\n }\n })\n .build();\n },\n\n /**\n * Mark a hook event as processing (to prevent concurrent execution).\n */\n markHookProcessing(eventId: FragnoId) {\n return this.serviceTx(internalSchema)\n .mutate(({ uow }) =>\n uow.update(\"fragno_hooks\", eventId, (b) =>\n b.set({ status: \"processing\", lastAttemptAt: dbNow() }).check(),\n ),\n )\n .build();\n },\n\n /**\n * Get a hook event by ID (for testing/verification purposes).\n */\n getHookById(eventId: FragnoId) {\n return this.serviceTx(internalSchema)\n .retrieve((uow) =>\n uow.findFirst(\"fragno_hooks\", (b) =>\n b.whereIndex(\"primary\", (eb) => eb(\"id\", \"=\", eventId)),\n ),\n )\n .transformRetrieve(([result]) => result ?? undefined)\n .build();\n },\n\n /**\n * Get all hook events for a namespace (for testing/verification purposes).\n */\n getHooksByNamespace(namespace: string) {\n return this.serviceTx(internalSchema)\n .retrieve((uow) =>\n uow.find(\"fragno_hooks\", (b) =>\n b.whereIndex(\"idx_namespace_status_retry\", (eb) => eb(\"namespace\", \"=\", namespace)),\n ),\n )\n .transformRetrieve(([events]) => events)\n .build();\n },\n });\n })\n .providesService(\"outboxService\", ({ defineService }) => {\n return defineService({\n /**\n * List outbox entries ordered by versionstamp (ascending).\n */\n list({ afterVersionstamp, limit }: { afterVersionstamp?: string; limit?: number } = {}) {\n const afterValue = afterVersionstamp?.toLowerCase();\n\n return this.serviceTx(internalSchema)\n .retrieve((uow) =>\n uow.find(\"fragno_db_outbox\", (b) => {\n let builder = afterValue\n ? b.whereIndex(\"idx_outbox_versionstamp\", (eb) =>\n eb(\"versionstamp\", \">\", afterValue),\n )\n : b.whereIndex(\"idx_outbox_versionstamp\");\n\n builder = builder.orderByIndex(\"idx_outbox_versionstamp\", \"asc\");\n if (limit !== undefined) {\n builder = builder.pageSize(limit);\n }\n return builder;\n }),\n )\n .transformRetrieve(([entries]) =>\n entries.map((entry) => ({\n id: entry.id,\n versionstamp: entry.versionstamp,\n uowId: entry.uowId,\n payload: entry.payload,\n refMap: entry.refMap ?? undefined,\n createdAt: entry.createdAt,\n })),\n )\n .build();\n },\n });\n })\n .build();\n\n/**\n * Type representing an instantiated internal fragment.\n * This is the fragment that manages Fragno's internal settings table.\n */\nexport type InternalFragmentInstance = InstantiatedFragmentFromDefinition<\n typeof internalFragmentDef\n>;\n\nexport async function getSchemaVersionFromDatabase(\n fragment: InternalFragmentInstance,\n namespace: string,\n): Promise<number> {\n try {\n const readSchemaVersion = async (targetNamespace: string): Promise<number | undefined> => {\n const setting = await fragment.inContext(async function () {\n return await this.handlerTx()\n .withServiceCalls(\n () =>\n [fragment.services.settingsService.get(targetNamespace, \"schema_version\")] as const,\n )\n .transform(({ serviceResult: [result] }) => result)\n .execute();\n });\n if (!setting) {\n return undefined;\n }\n const parsed = parseInt(setting.value, 10);\n return Number.isNaN(parsed) ? undefined : parsed;\n };\n\n const primary = await readSchemaVersion(namespace);\n if (primary !== undefined) {\n return primary;\n }\n\n // Back-compat: some installs stored internal schema version under a different namespace.\n // Check the alternate key (empty string ↔ schema name) so we find the version either way.\n const legacyNamespace =\n namespace === \"\" ? internalSchema.name : namespace === internalSchema.name ? \"\" : null;\n if (legacyNamespace !== null) {\n const legacy = await readSchemaVersion(legacyNamespace);\n if (legacy !== undefined) {\n return legacy;\n }\n }\n\n return 0;\n } catch {\n return 0;\n }\n}\n"],"mappings":";;;;;;;AA2BA,MAAa,sBAAsB,IAAI,kCACrC,IAAI,0BAWF,4BAA4B,EAC9B,eACD,CACE,qBAAqB,EAAE,YAAY,EAClC,UAAU,YAAY;AACpB,KAAI,KAAK,GAAG,IACV,QAAO,KAAK,GAAG,KAAK;AAEtB,wBAAO,IAAI,MAAM;GAEpB,EAAE,CACF,gBAAgB,oBAAoB,EAAE,oBAAoB;AACzD,QAAO,cAAc;EAInB,IAAI,WAAmB,KAAa;GAClC,MAAM,UAAU,GAAG,UAAU,GAAG;AAChC,UAAO,KAAK,UAAU,eAAe,CAClC,UAAU,QACT,IAAI,UAAU,sBAAsB,MAClC,EAAE,WAAW,eAAe,OAAO,GAAG,OAAO,KAAK,QAAQ,CAAC,CAC5D,CACF,CACA,mBACE,CAAC,YACA,UAAU,OACb,CACA,OAAO;;EAMZ,IAAI,WAAmB,KAAa,OAAe;GACjD,MAAM,UAAU,GAAG,UAAU,GAAG;AAChC,UAAO,KAAK,UAAU,eAAe,CAClC,UAAU,QACT,IAAI,UAAU,sBAAsB,MAClC,EAAE,WAAW,eAAe,OAAO,GAAG,OAAO,KAAK,QAAQ,CAAC,CAC5D,CACF,CACA,mBAAmB,CAAC,YAAY,OAAO,CACvC,QAAQ,EAAE,KAAK,qBAAqB;AACnC,QAAI,eACF,KAAI,OAAO,qBAAqB,eAAe,KAAK,MAAM,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC,OAAO,CAAC;QAEnF,KAAI,OAAO,qBAAqB;KAC9B,KAAK;KACL;KACD,CAAC;KAEJ,CACD,OAAO;;EAMZ,OAAO,IAAc;AACnB,UAAO,KAAK,UAAU,eAAe,CAClC,QAAQ,EAAE,UAAU,IAAI,OAAO,qBAAqB,GAAG,CAAC,CACxD,OAAO;;EAEb,CAAC;EACF,CACD,gBAAgB,gBAAgB,EAAE,oBAAoB;AACrD,QAAO,cAAc;EAKnB,qBAAqB,WAAmB;GACtC,MAAM,MAAM,OAAO;AACnB,UAAO,KAAK,UAAU,eAAe,CAClC,UAAU,QACT,IAAI,KAAK,iBAAiB,MACxB,EAAE,WAAW,+BAA+B,OAC1C,GAAG,IACD,GAAG,aAAa,KAAK,UAAU,EAC/B,GAAG,UAAU,KAAK,UAAU,EAC5B,GAAG,GAAG,GAAG,OAAO,cAAc,EAAE,GAAG,eAAe,MAAM,IAAI,CAAC,CAC9D,CACF,CACF,CACF,CACA,mBAAmB,CAAC,YAAY;AAC/B,WAAO,OAAO,KAAK,WAAW;KAC5B,IAAI,MAAM;KACV,UAAU,MAAM;KAChB,SAAS,MAAM;KACf,UAAU,MAAM;KAChB,aAAa,MAAM;KACnB,gBAAgB,MAAM;KACvB,EAAE;KACH,CACD,OAAO;;EAOZ,uBAAuB,WAAmB;GACxC,MAAM,MAAM,OAAO;AACnB,UAAO,KAAK,UAAU,eAAe,CAClC,UAAU,QACT,IAAI,KAAK,iBAAiB,MACxB,EAAE,WAAW,+BAA+B,OAC1C,GAAG,IACD,GAAG,aAAa,KAAK,UAAU,EAC/B,GAAG,UAAU,KAAK,UAAU,EAC5B,GAAG,GAAG,GAAG,OAAO,cAAc,EAAE,GAAG,eAAe,MAAM,IAAI,CAAC,CAC9D,CACF,CACF,CACF,CACA,mBAAmB,CAAC,YAAY;AAC/B,WAAO,OAAO,KAAK,WAAW;KAC5B,IAAI,MAAM;KACV,UAAU,MAAM;KAChB,SAAS,MAAM;KACf,UAAU,MAAM;KAChB,aAAa,MAAM;KACnB,gBAAgB,MAAM;KACvB,EAAE;KACH,CACD,QAAQ,EAAE,KAAK,qBAAqB;AACnC,QAAI,eAAe,WAAW,EAC5B;AAEF,SAAK,MAAM,SAAS,eAClB,KAAI,OAAO,gBAAgB,MAAM,KAAK,MACpC,EAAE,IAAI;KAAE,QAAQ;KAAc,eAAe;KAAK,CAAC,CAAC,OAAO,CAC5D;KAEH,CACD,WAAW,EAAE,qBACZ,eAAe,KAAK,WAAW;IAC7B,GAAG;IACH,IAAI,IAAI,SAAS;KACf,YAAY,MAAM,GAAG;KACrB,YAAY,MAAM,GAAG;KACrB,SAAS,MAAM,GAAG,UAAU;KAC7B,CAAC;IACH,EAAE,CACJ,CACA,OAAO;;EAMZ,4BAA4B,WAAmB,aAAmB;AAChE,UAAO,KAAK,UAAU,eAAe,CAClC,UAAU,QACT,IAAI,KAAK,iBAAiB,MACxB,EAAE,WAAW,+BAA+B,OAC1C,GAAG,IAAI,GAAG,aAAa,KAAK,UAAU,EAAE,GAAG,UAAU,KAAK,aAAa,CAAC,CACzE,CACF,CACF,CACA,mBAAmB,CAAC,YAAY;AAQ/B,WAPc,OAAO,QAAQ,UAAU;AACrC,SAAI,CAAC,MAAM,cACT,QAAO;AAET,YAAO,MAAM,iBAAiB;MAC9B,CAEW,KAAK,WAAW;KAC3B,IAAI,MAAM;KACV,UAAU,MAAM;KAChB,UAAU,MAAM;KAChB,aAAa,MAAM;KACnB,eAAe,MAAM;KACrB,aAAa,MAAM;KACpB,EAAE;KACH,CACD,QAAQ,EAAE,KAAK,qBAAqB;AACnC,SAAK,MAAM,SAAS,eAClB,KAAI,OAAO,gBAAgB,MAAM,KAAK,MACpC,EAAE,IAAI;KAAE,QAAQ;KAAW,aAAa;KAAM,CAAC,CAAC,OAAO,CACxD;KAEH,CACD,WAAW,EAAE,qBAAqB,eAAe,CACjD,OAAO;;EAMZ,yBAAyB,WAAmB,gBAAwB,KAAY;AAC9E,UAAO,KAAK,UAAU,eAAe,CAClC,UAAU,QACT,IAAI,KAAK,iBAAiB,MACxB,EAAE,WAAW,+BAA+B,OAC1C,GAAG,IAAI,GAAG,aAAa,KAAK,UAAU,EAAE,GAAG,UAAU,KAAK,aAAa,CAAC,CACzE,CACF,CACF,CACA,mBAAmB,CAAC,YAAY;AAC/B,QAAI,OAAO,WAAW,EACpB,QAAO;IAGT,MAAM,UAAU,uBAAO,IAAI,MAAM;IACjC,MAAM,QAAQ,QAAQ,SAAS;IAC/B,MAAM,YAAY,iBAAiB;IACnC,IAAIA,kBAA+B;AAEnC,SAAK,MAAM,SAAS,QAAQ;AAC1B,SAAI,CAAC,MAAM,cACT,QAAO;KAGT,MAAM,YAAY,MAAM,cAAc,SAAS,GAAG;AAClD,SAAI,aAAa,MACf,QAAO;KAGT,MAAM,UAAU,IAAI,KAAK,UAAU;AACnC,SAAI,CAAC,mBAAmB,UAAU,gBAChC,mBAAkB;;AAItB,WAAO;KACP,CACD,OAAO;;EAOZ,kBAAkB,WAAmB,gBAAiC,KAAY;GAChF,MAAM,UAAU,uBAAO,IAAI,MAAM;GACjC,MAAM,oBAAoB,OAAO,mBAAmB,YAAY,iBAAiB;GACjF,MAAM,YAAY,oBAAoB,iBAAiB,MAAS;AAEhE,UAAO,KAAK,UAAU,eAAe,CAClC,UAAU,QACT,IAAI,KAAK,iBAAiB,MACxB,EACG,WAAW,+BAA+B,OAAO;AAChD,QAAI,kBACF,QAAO,GAAG,IACR,GAAG,aAAa,KAAK,UAAU,EAC/B,GAAG,GAAG,GAAG,UAAU,KAAK,UAAU,EAAE,GAAG,UAAU,KAAK,aAAa,CAAC,CACrE;AAEH,WAAO,GAAG,IAAI,GAAG,aAAa,KAAK,UAAU,EAAE,GAAG,UAAU,KAAK,UAAU,CAAC;KAC5E,CACD,OAAO;IAAC;IAAU;IAAe;IAAgB,CAAC,CACtD,CACF,CACA,mBAAmB,CAAC,YAAY;AAC/B,QAAI,OAAO,WAAW,EACpB,QAAO;IAGT,MAAM,QAAQ,QAAQ,SAAS;IAC/B,IAAIC,oBAAiC;IACrC,IAAID,kBAA+B;AAEnC,SAAK,MAAM,SAAS,QAAQ;AAC1B,SAAI,MAAM,WAAW,WAAW;MAC9B,MAAM,cAAc,MAAM;AAC1B,UAAI,CAAC,eAAe,YAAY,SAAS,IAAI,MAC3C,QAAO;AAET,UAAI,CAAC,qBAAqB,cAAc,kBACtC,qBAAoB;AAEtB;;AAGF,SAAI,CAAC,qBAAqB,MAAM,WAAW,aACzC;KAGF,MAAM,gBAAgB,MAAM;AAC5B,SAAI,CAAC,cACH,QAAO;KAGT,MAAM,YAAY,cAAc,SAAS,GAAG;AAC5C,SAAI,aAAa,MACf,QAAO;KAGT,MAAM,UAAU,IAAI,KAAK,UAAU;AACnC,SAAI,CAAC,mBAAmB,UAAU,gBAChC,mBAAkB;;AAItB,QAAI,CAAC,kBACH,QAAO,mBAAmB;AAE5B,QAAI,CAAC,gBACH,QAAO;AAET,WAAO,qBAAqB,kBAAkB,oBAAoB;KAClE,CACD,OAAO;;EAMZ,kBAAkB,SAAmB;AACnC,UAAO,KAAK,UAAU,eAAe,CAClC,QAAQ,EAAE,UACT,IAAI,OAAO,gBAAgB,UAAU,MACnC,EAAE,IAAI;IAAE,QAAQ;IAAa,eAAe,OAAO;IAAE,CAAC,CAAC,OAAO,CAC/D,CACF,CACA,OAAO;;EAMZ,eACE,SACA,OACA,UACA,aACA,KACA;GACA,MAAM,cAAc,WAAW;GAC/B,MAAM,cAAc,YAAY,YAAY,cAAc,EAAE;AAE5D,UAAO,KAAK,UAAU,eAAe,CAClC,QAAQ,EAAE,UAAU;AACnB,QAAI,aAAa;KACf,MAAM,UAAU,YAAY,WAAW,cAAc,EAAE;KACvD,MAAM,UAAU,uBAAO,IAAI,MAAM;KACjC,MAAM,cAAc,IAAI,KAAK,QAAQ,SAAS,GAAG,QAAQ;AACzD,SAAI,OAAO,gBAAgB,UAAU,MACnC,EACG,IAAI;MACH,QAAQ;MACR,UAAU;MACV,eAAe,OAAO;MACtB;MACA;MACD,CAAC,CACD,OAAO,CACX;UAED,KAAI,OAAO,gBAAgB,UAAU,MACnC,EACG,IAAI;KACH,QAAQ;KACR,UAAU;KACV,eAAe,OAAO;KACtB;KACD,CAAC,CACD,OAAO,CACX;KAEH,CACD,OAAO;;EAMZ,mBAAmB,SAAmB;AACpC,UAAO,KAAK,UAAU,eAAe,CAClC,QAAQ,EAAE,UACT,IAAI,OAAO,gBAAgB,UAAU,MACnC,EAAE,IAAI;IAAE,QAAQ;IAAc,eAAe,OAAO;IAAE,CAAC,CAAC,OAAO,CAChE,CACF,CACA,OAAO;;EAMZ,YAAY,SAAmB;AAC7B,UAAO,KAAK,UAAU,eAAe,CAClC,UAAU,QACT,IAAI,UAAU,iBAAiB,MAC7B,EAAE,WAAW,YAAY,OAAO,GAAG,MAAM,KAAK,QAAQ,CAAC,CACxD,CACF,CACA,mBAAmB,CAAC,YAAY,UAAU,OAAU,CACpD,OAAO;;EAMZ,oBAAoB,WAAmB;AACrC,UAAO,KAAK,UAAU,eAAe,CAClC,UAAU,QACT,IAAI,KAAK,iBAAiB,MACxB,EAAE,WAAW,+BAA+B,OAAO,GAAG,aAAa,KAAK,UAAU,CAAC,CACpF,CACF,CACA,mBAAmB,CAAC,YAAY,OAAO,CACvC,OAAO;;EAEb,CAAC;EACF,CACD,gBAAgB,kBAAkB,EAAE,oBAAoB;AACvD,QAAO,cAAc,EAInB,KAAK,EAAE,mBAAmB,UAA0D,EAAE,EAAE;EACtF,MAAM,aAAa,mBAAmB,aAAa;AAEnD,SAAO,KAAK,UAAU,eAAe,CAClC,UAAU,QACT,IAAI,KAAK,qBAAqB,MAAM;GAClC,IAAI,UAAU,aACV,EAAE,WAAW,4BAA4B,OACvC,GAAG,gBAAgB,KAAK,WAAW,CACpC,GACD,EAAE,WAAW,0BAA0B;AAE3C,aAAU,QAAQ,aAAa,2BAA2B,MAAM;AAChE,OAAI,UAAU,OACZ,WAAU,QAAQ,SAAS,MAAM;AAEnC,UAAO;IACP,CACH,CACA,mBAAmB,CAAC,aACnB,QAAQ,KAAK,WAAW;GACtB,IAAI,MAAM;GACV,cAAc,MAAM;GACpB,OAAO,MAAM;GACb,SAAS,MAAM;GACf,QAAQ,MAAM,UAAU;GACxB,WAAW,MAAM;GAClB,EAAE,CACJ,CACA,OAAO;IAEb,CAAC;EACF,CACD,OAAO;AAUV,eAAsB,6BACpB,UACA,WACiB;AACjB,KAAI;EACF,MAAM,oBAAoB,OAAO,oBAAyD;GACxF,MAAM,UAAU,MAAM,SAAS,UAAU,iBAAkB;AACzD,WAAO,MAAM,KAAK,WAAW,CAC1B,uBAEG,CAAC,SAAS,SAAS,gBAAgB,IAAI,iBAAiB,iBAAiB,CAAC,CAC7E,CACA,WAAW,EAAE,eAAe,CAAC,cAAc,OAAO,CAClD,SAAS;KACZ;AACF,OAAI,CAAC,QACH;GAEF,MAAM,SAAS,SAAS,QAAQ,OAAO,GAAG;AAC1C,UAAO,OAAO,MAAM,OAAO,GAAG,SAAY;;EAG5C,MAAM,UAAU,MAAM,kBAAkB,UAAU;AAClD,MAAI,YAAY,OACd,QAAO;EAKT,MAAM,kBACJ,cAAc,KAAK,eAAe,OAAO,cAAc,eAAe,OAAO,KAAK;AACpF,MAAI,oBAAoB,MAAM;GAC5B,MAAM,SAAS,MAAM,kBAAkB,gBAAgB;AACvD,OAAI,WAAW,OACb,QAAO;;AAIX,SAAO;SACD;AACN,SAAO"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { defineRoutes } from "../packages/fragno/dist/api/route.js";
|
|
2
|
+
import { internalFragmentDef } from "./internal-fragment.js";
|
|
3
|
+
|
|
4
|
+
//#region src/fragments/internal-fragment.routes.ts
|
|
5
|
+
const internalFragmentRoutes = defineRoutes(internalFragmentDef).create(({ defineRoute, services }) => [defineRoute({
|
|
6
|
+
method: "GET",
|
|
7
|
+
path: "/outbox",
|
|
8
|
+
handler: async function(input, { json }) {
|
|
9
|
+
const afterVersionstamp = input.query.get("afterVersionstamp") ?? void 0;
|
|
10
|
+
const limitValue = input.query.get("limit");
|
|
11
|
+
let limit;
|
|
12
|
+
if (limitValue !== null) {
|
|
13
|
+
const parsed = Number.parseInt(limitValue, 10);
|
|
14
|
+
if (!Number.isFinite(parsed) || parsed < 1) return json({
|
|
15
|
+
error: "Invalid limit query parameter.",
|
|
16
|
+
code: "INVALID_LIMIT"
|
|
17
|
+
}, { status: 400 });
|
|
18
|
+
limit = parsed;
|
|
19
|
+
}
|
|
20
|
+
return json(await this.handlerTx().withServiceCalls(() => [services.outboxService.list({
|
|
21
|
+
afterVersionstamp,
|
|
22
|
+
limit
|
|
23
|
+
})]).transform(({ serviceResult: [result] }) => result).execute());
|
|
24
|
+
}
|
|
25
|
+
})]);
|
|
26
|
+
|
|
27
|
+
//#endregion
|
|
28
|
+
export { internalFragmentRoutes };
|
|
29
|
+
//# sourceMappingURL=internal-fragment.routes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"internal-fragment.routes.js","names":["limit: number | undefined"],"sources":["../../src/fragments/internal-fragment.routes.ts"],"sourcesContent":["import { defineRoutes } from \"@fragno-dev/core\";\nimport { internalFragmentDef } from \"./internal-fragment\";\n\nexport const internalFragmentRoutes = defineRoutes(internalFragmentDef).create(\n ({ defineRoute, services }) => [\n defineRoute({\n method: \"GET\",\n path: \"/outbox\",\n handler: async function (input, { json }) {\n // We intentionally skip input/output schemas here to keep the internal route lightweight.\n // Query params are validated manually and the response shape is stable (OutboxEntry[]),\n // while the public API surface is still gated behind adapter config.\n const afterVersionstamp = input.query.get(\"afterVersionstamp\") ?? undefined;\n const limitValue = input.query.get(\"limit\");\n let limit: number | undefined;\n\n if (limitValue !== null) {\n const parsed = Number.parseInt(limitValue, 10);\n if (!Number.isFinite(parsed) || parsed < 1) {\n return json(\n {\n error: \"Invalid limit query parameter.\",\n code: \"INVALID_LIMIT\",\n },\n { status: 400 },\n );\n }\n limit = parsed;\n }\n\n const entries = await this.handlerTx()\n .withServiceCalls(\n () => [services.outboxService.list({ afterVersionstamp, limit })] as const,\n )\n .transform(({ serviceResult: [result] }) => result)\n .execute();\n\n return json(entries);\n },\n }),\n ],\n);\n"],"mappings":";;;;AAGA,MAAa,yBAAyB,aAAa,oBAAoB,CAAC,QACrE,EAAE,aAAa,eAAe,CAC7B,YAAY;CACV,QAAQ;CACR,MAAM;CACN,SAAS,eAAgB,OAAO,EAAE,QAAQ;EAIxC,MAAM,oBAAoB,MAAM,MAAM,IAAI,oBAAoB,IAAI;EAClE,MAAM,aAAa,MAAM,MAAM,IAAI,QAAQ;EAC3C,IAAIA;AAEJ,MAAI,eAAe,MAAM;GACvB,MAAM,SAAS,OAAO,SAAS,YAAY,GAAG;AAC9C,OAAI,CAAC,OAAO,SAAS,OAAO,IAAI,SAAS,EACvC,QAAO,KACL;IACE,OAAO;IACP,MAAM;IACP,EACD,EAAE,QAAQ,KAAK,CAChB;AAEH,WAAQ;;AAUV,SAAO,KAPS,MAAM,KAAK,WAAW,CACnC,uBACO,CAAC,SAAS,cAAc,KAAK;GAAE;GAAmB;GAAO,CAAC,CAAC,CAClE,CACA,WAAW,EAAE,eAAe,CAAC,cAAc,OAAO,CAClD,SAAS,CAEQ;;CAEvB,CAAC,CACH,CACF"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { DbNow } from "../query/db-now.js";
|
|
2
|
+
import { AnyColumn, AnyRelation, Column, FragnoId, IdColumn, Index, Schema, Table } from "../schema/create.js";
|
|
3
|
+
import "../mod.js";
|
|
4
|
+
|
|
5
|
+
//#region src/fragments/internal-fragment.schema.d.ts
|
|
6
|
+
declare const internalSchema: Schema<Record<"fragno_db_settings", Table<Record<string, AnyColumn> & Record<"id", IdColumn<"varchar(30)", string | FragnoId | null, FragnoId>> & Record<"key", Column<"string", string, string>> & Record<"value", Column<"string", string, string>>, Record<string, AnyRelation>, Record<string, Index<AnyColumn[], readonly string[]>> & Record<"unique_key", Index<readonly [Column<"string", string, string>] & AnyColumn[], readonly ["key"]>>>> & Record<"fragno_hooks", Table<Record<string, AnyColumn> & Record<"id", IdColumn<"varchar(30)", string | FragnoId | null, FragnoId>> & Record<"namespace", Column<"string", string, string>> & Record<"hookName", Column<"string", string, string>> & Record<"payload", Column<"json", unknown, unknown>> & Record<"status", Column<"string", string, string>> & Record<"attempts", Column<"integer", number | null, number>> & Record<"maxAttempts", Column<"integer", number | null, number>> & Record<"lastAttemptAt", Column<"timestamp", (DbNow | Date) | null, Date | null>> & Record<"nextRetryAt", Column<"timestamp", (DbNow | Date) | null, Date | null>> & Record<"error", Column<"string", string | null, string | null>> & Record<"createdAt", Column<"timestamp", (DbNow | Date) | null, Date>> & Record<"nonce", Column<"string", string, string>>, Record<string, AnyRelation>, Record<string, Index<AnyColumn[], readonly string[]>> & Record<"idx_namespace_status_retry", Index<readonly [Column<"string", string, string>, Column<"string", string, string>, Column<"timestamp", (DbNow | Date) | null, Date | null>] & AnyColumn[], readonly ["namespace", "status", "nextRetryAt"]>> & Record<"idx_nonce", Index<readonly [Column<"string", string, string>] & AnyColumn[], readonly ["nonce"]>>>> & Record<"fragno_db_outbox", Table<Record<string, AnyColumn> & Record<"id", IdColumn<"varchar(30)", string | FragnoId | null, FragnoId>> & Record<"versionstamp", Column<"string", string, string>> & Record<"uowId", Column<"string", string, string>> & Record<"payload", Column<"json", unknown, unknown>> & Record<"refMap", Column<"json", unknown, unknown>> & Record<"createdAt", Column<"timestamp", (DbNow | Date) | null, Date>>, Record<string, AnyRelation>, Record<string, Index<AnyColumn[], readonly string[]>> & Record<"idx_outbox_versionstamp", Index<readonly [Column<"string", string, string>] & AnyColumn[], readonly ["versionstamp"]>> & Record<"idx_outbox_uow", Index<readonly [Column<"string", string, string>] & AnyColumn[], readonly ["uowId"]>>>>>;
|
|
7
|
+
//#endregion
|
|
8
|
+
export { internalSchema };
|
|
9
|
+
//# sourceMappingURL=internal-fragment.schema.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"internal-fragment.schema.d.ts","names":[],"sources":["../../src/fragments/internal-fragment.schema.ts"],"sourcesContent":[],"mappings":";;;;;cAOa,gBAAc,OAAA,6BAAA,MAAA,eA2CzB,SAAA,IA3CyB,aAAA,iCAAA,QAAA,SAAA,QAAA,KAAA,cAAA,oCAAA,gBAAA,mCAAA,eAAA,WAAA,GAAA,eAAA,MAAA,SAAA,0BAAA,qBAAA,gBAAA,oCAAA,SAAA,2BAAA,uBAAA,MAAA,eAAA,SAAA,IAAA,aAAA,iCAAA,QAAA,SAAA,QAAA,KAAA,oBAAA,oCAAA,mBAAA,oCAAA,kBAAA,oCAAA,iBAAA,oCAAA,mBAAA,4CAAA,sBAAA,4CAAA,wBAAA,qBAAA,KAAA,GAAA,cAAA,gBAAA,sBAAA,qBAAA,KAAA,GAAA,cAAA,gBAAA,gBAAA,kDAAA,oBAAA,qBAAA,KAAA,GAAA,cAAA,SAAA,gBAAA,mCAAA,eAAA,WAAA,GAAA,eAAA,MAAA,SAAA,0BAAA,qCAAA,gBAAA,kCAAA,kCAAA,qBAAA,KAAA,GAAA,cAAA,gBAAA,SAAA,wDAAA,oBAAA,gBAAA,oCAAA,SAAA,6BAAA,2BAAA,MAAA,eAAA,SAAA,IAAA,aAAA,iCAAA,QAAA,SAAA,QAAA,KAAA,uBAAA,oCAAA,gBAAA,oCAAA,kBAAA,oCAAA,iBAAA,oCAAA,oBAAA,qBAAA,KAAA,GAAA,cAAA,QAAA,eAAA,WAAA,GAAA,eAAA,MAAA,SAAA,0BAAA,kCAAA,gBAAA,oCAAA,SAAA,kCAAA,yBAAA,gBAAA,oCAAA,SAAA"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { column, idColumn, schema } from "../schema/create.js";
|
|
2
|
+
|
|
3
|
+
//#region src/fragments/internal-fragment.schema.ts
|
|
4
|
+
const SETTINGS_TABLE_NAME = "fragno_db_settings";
|
|
5
|
+
const SETTINGS_NAMESPACE = "fragno-db-settings";
|
|
6
|
+
const internalSchema = schema("fragno_internal", (s) => {
|
|
7
|
+
return s.addTable(SETTINGS_TABLE_NAME, (t) => {
|
|
8
|
+
return t.addColumn("id", idColumn()).addColumn("key", column("string")).addColumn("value", column("string")).createIndex("unique_key", ["key"], { unique: true });
|
|
9
|
+
}).addTable("fragno_hooks", (t) => {
|
|
10
|
+
return t.addColumn("id", idColumn()).addColumn("namespace", column("string")).addColumn("hookName", column("string")).addColumn("payload", column("json")).addColumn("status", column("string")).addColumn("attempts", column("integer").defaultTo(0)).addColumn("maxAttempts", column("integer").defaultTo(5)).addColumn("lastAttemptAt", column("timestamp").nullable()).addColumn("nextRetryAt", column("timestamp").nullable()).addColumn("error", column("string").nullable()).addColumn("createdAt", column("timestamp").defaultTo((b) => b.now())).addColumn("nonce", column("string")).createIndex("idx_namespace_status_retry", [
|
|
11
|
+
"namespace",
|
|
12
|
+
"status",
|
|
13
|
+
"nextRetryAt"
|
|
14
|
+
]).createIndex("idx_nonce", ["nonce"]);
|
|
15
|
+
}).addTable("fragno_db_outbox", (t) => {
|
|
16
|
+
return t.addColumn("id", idColumn()).addColumn("versionstamp", column("string")).addColumn("uowId", column("string")).addColumn("payload", column("json")).addColumn("refMap", column("json").nullable()).addColumn("createdAt", column("timestamp").defaultTo((b) => b.now())).createIndex("idx_outbox_versionstamp", ["versionstamp"], { unique: true }).createIndex("idx_outbox_uow", ["uowId"]);
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
//#endregion
|
|
21
|
+
export { SETTINGS_NAMESPACE, SETTINGS_TABLE_NAME, internalSchema };
|
|
22
|
+
//# sourceMappingURL=internal-fragment.schema.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"internal-fragment.schema.js","names":[],"sources":["../../src/fragments/internal-fragment.schema.ts"],"sourcesContent":["import { schema, idColumn, column } from \"../schema/create\";\n\n// Constants for Fragno's internal settings table\nexport const SETTINGS_TABLE_NAME = \"fragno_db_settings\" as const;\n// FIXME: In some places we simply use empty string \"\" as namespace, which is not correct.\nexport const SETTINGS_NAMESPACE = \"fragno-db-settings\" as const;\n\nexport const internalSchema = schema(\"fragno_internal\", (s) => {\n return s\n .addTable(SETTINGS_TABLE_NAME, (t) => {\n return t\n .addColumn(\"id\", idColumn())\n .addColumn(\"key\", column(\"string\"))\n .addColumn(\"value\", column(\"string\"))\n .createIndex(\"unique_key\", [\"key\"], { unique: true });\n })\n .addTable(\"fragno_hooks\", (t) => {\n return t\n .addColumn(\"id\", idColumn())\n .addColumn(\"namespace\", column(\"string\"))\n .addColumn(\"hookName\", column(\"string\"))\n .addColumn(\"payload\", column(\"json\"))\n .addColumn(\"status\", column(\"string\")) // \"pending\" | \"processing\" | \"completed\" | \"failed\"\n .addColumn(\"attempts\", column(\"integer\").defaultTo(0))\n .addColumn(\"maxAttempts\", column(\"integer\").defaultTo(5))\n .addColumn(\"lastAttemptAt\", column(\"timestamp\").nullable())\n .addColumn(\"nextRetryAt\", column(\"timestamp\").nullable())\n .addColumn(\"error\", column(\"string\").nullable())\n .addColumn(\n \"createdAt\",\n column(\"timestamp\").defaultTo((b) => b.now()),\n )\n .addColumn(\"nonce\", column(\"string\"))\n .createIndex(\"idx_namespace_status_retry\", [\"namespace\", \"status\", \"nextRetryAt\"])\n .createIndex(\"idx_nonce\", [\"nonce\"]);\n })\n .addTable(\"fragno_db_outbox\", (t) => {\n return t\n .addColumn(\"id\", idColumn())\n .addColumn(\"versionstamp\", column(\"string\"))\n .addColumn(\"uowId\", column(\"string\"))\n .addColumn(\"payload\", column(\"json\"))\n .addColumn(\"refMap\", column(\"json\").nullable())\n .addColumn(\n \"createdAt\",\n column(\"timestamp\").defaultTo((b) => b.now()),\n )\n .createIndex(\"idx_outbox_versionstamp\", [\"versionstamp\"], { unique: true })\n .createIndex(\"idx_outbox_uow\", [\"uowId\"]);\n });\n});\n"],"mappings":";;;AAGA,MAAa,sBAAsB;AAEnC,MAAa,qBAAqB;AAElC,MAAa,iBAAiB,OAAO,oBAAoB,MAAM;AAC7D,QAAO,EACJ,SAAS,sBAAsB,MAAM;AACpC,SAAO,EACJ,UAAU,MAAM,UAAU,CAAC,CAC3B,UAAU,OAAO,OAAO,SAAS,CAAC,CAClC,UAAU,SAAS,OAAO,SAAS,CAAC,CACpC,YAAY,cAAc,CAAC,MAAM,EAAE,EAAE,QAAQ,MAAM,CAAC;GACvD,CACD,SAAS,iBAAiB,MAAM;AAC/B,SAAO,EACJ,UAAU,MAAM,UAAU,CAAC,CAC3B,UAAU,aAAa,OAAO,SAAS,CAAC,CACxC,UAAU,YAAY,OAAO,SAAS,CAAC,CACvC,UAAU,WAAW,OAAO,OAAO,CAAC,CACpC,UAAU,UAAU,OAAO,SAAS,CAAC,CACrC,UAAU,YAAY,OAAO,UAAU,CAAC,UAAU,EAAE,CAAC,CACrD,UAAU,eAAe,OAAO,UAAU,CAAC,UAAU,EAAE,CAAC,CACxD,UAAU,iBAAiB,OAAO,YAAY,CAAC,UAAU,CAAC,CAC1D,UAAU,eAAe,OAAO,YAAY,CAAC,UAAU,CAAC,CACxD,UAAU,SAAS,OAAO,SAAS,CAAC,UAAU,CAAC,CAC/C,UACC,aACA,OAAO,YAAY,CAAC,WAAW,MAAM,EAAE,KAAK,CAAC,CAC9C,CACA,UAAU,SAAS,OAAO,SAAS,CAAC,CACpC,YAAY,8BAA8B;GAAC;GAAa;GAAU;GAAc,CAAC,CACjF,YAAY,aAAa,CAAC,QAAQ,CAAC;GACtC,CACD,SAAS,qBAAqB,MAAM;AACnC,SAAO,EACJ,UAAU,MAAM,UAAU,CAAC,CAC3B,UAAU,gBAAgB,OAAO,SAAS,CAAC,CAC3C,UAAU,SAAS,OAAO,SAAS,CAAC,CACpC,UAAU,WAAW,OAAO,OAAO,CAAC,CACpC,UAAU,UAAU,OAAO,OAAO,CAAC,UAAU,CAAC,CAC9C,UACC,aACA,OAAO,YAAY,CAAC,WAAW,MAAM,EAAE,KAAK,CAAC,CAC9C,CACA,YAAY,2BAA2B,CAAC,eAAe,EAAE,EAAE,QAAQ,MAAM,CAAC,CAC1E,YAAY,kBAAkB,CAAC,QAAQ,CAAC;GAC3C;EACJ"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { AnySchema } from "../schema/create.js";
|
|
2
|
+
import { AnyFragnoInstantiatedDatabaseFragment } from "../mod.js";
|
|
3
|
+
|
|
4
|
+
//#region src/hooks/durable-hooks-processor.d.ts
|
|
5
|
+
type DurableHooksProcessor = {
|
|
6
|
+
process: () => Promise<number>;
|
|
7
|
+
getNextWakeAt: () => Promise<Date | null>;
|
|
8
|
+
drain: () => Promise<void>;
|
|
9
|
+
namespace: string;
|
|
10
|
+
};
|
|
11
|
+
declare function createDurableHooksProcessor<TSchema extends AnySchema>(fragment: AnyFragnoInstantiatedDatabaseFragment<TSchema>): DurableHooksProcessor | null;
|
|
12
|
+
//#endregion
|
|
13
|
+
export { DurableHooksProcessor, createDurableHooksProcessor };
|
|
14
|
+
//# sourceMappingURL=durable-hooks-processor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"durable-hooks-processor.d.ts","names":[],"sources":["../../src/hooks/durable-hooks-processor.ts"],"sourcesContent":[],"mappings":";;;;KAIY,qBAAA;iBACK;EADL,aAAA,EAAA,GAAA,GAEW,OAFU,CAEF,IAFE,GAAA,IAAA,CAAA;EAChB,KAAA,EAAA,GAAA,GAEF,OAFE,CAAA,IAAA,CAAA;EACc,SAAA,EAAA,MAAA;CAAR;AACR,iBAoBC,2BApBD,CAAA,gBAoB6C,SApB7C,CAAA,CAAA,QAAA,EAqBH,qCArBG,CAqBmC,OArBnC,CAAA,CAAA,EAsBZ,qBAtBY,GAAA,IAAA"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { createHookScheduler } from "./hooks.js";
|
|
2
|
+
|
|
3
|
+
//#region src/hooks/durable-hooks-processor.ts
|
|
4
|
+
const DEFAULT_STUCK_PROCESSING_TIMEOUT_MINUTES = 10;
|
|
5
|
+
function resolveStuckProcessingTimeoutMinutes(value) {
|
|
6
|
+
if (value === false) return false;
|
|
7
|
+
if (typeof value === "number") return value > 0 ? value : false;
|
|
8
|
+
return DEFAULT_STUCK_PROCESSING_TIMEOUT_MINUTES;
|
|
9
|
+
}
|
|
10
|
+
function createDurableHooksProcessor(fragment) {
|
|
11
|
+
const durableHooks = fragment.$internal.durableHooks;
|
|
12
|
+
if (!durableHooks) return null;
|
|
13
|
+
const { namespace, internalFragment } = durableHooks;
|
|
14
|
+
const stuckProcessingTimeoutMinutes = resolveStuckProcessingTimeoutMinutes(durableHooks.stuckProcessingTimeoutMinutes);
|
|
15
|
+
const scheduler = durableHooks.scheduler ?? (durableHooks.scheduler = createHookScheduler(durableHooks));
|
|
16
|
+
return {
|
|
17
|
+
namespace,
|
|
18
|
+
process: async () => scheduler.schedule(),
|
|
19
|
+
drain: async () => scheduler.drain(),
|
|
20
|
+
getNextWakeAt: async () => {
|
|
21
|
+
const services = internalFragment.services;
|
|
22
|
+
const now = services.getDbNow ? await services.getDbNow() : /* @__PURE__ */ new Date();
|
|
23
|
+
return await internalFragment.inContext(async function() {
|
|
24
|
+
return await this.handlerTx().withServiceCalls(() => [internalFragment.services.hookService.getNextHookWakeAt(namespace, stuckProcessingTimeoutMinutes, now)]).transform(({ serviceResult: [result] }) => result).execute();
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
//#endregion
|
|
31
|
+
export { createDurableHooksProcessor };
|
|
32
|
+
//# sourceMappingURL=durable-hooks-processor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"durable-hooks-processor.js","names":[],"sources":["../../src/hooks/durable-hooks-processor.ts"],"sourcesContent":["import type { AnySchema } from \"../schema/create\";\nimport type { AnyFragnoInstantiatedDatabaseFragment } from \"../mod\";\nimport { createHookScheduler, type HookProcessorConfig } from \"./hooks\";\n\nexport type DurableHooksProcessor = {\n process: () => Promise<number>;\n getNextWakeAt: () => Promise<Date | null>;\n drain: () => Promise<void>;\n namespace: string;\n};\n\ntype DurableHooksInternal = {\n durableHooks?: HookProcessorConfig;\n};\n\nconst DEFAULT_STUCK_PROCESSING_TIMEOUT_MINUTES = 10;\n\nfunction resolveStuckProcessingTimeoutMinutes(value: number | false | undefined): number | false {\n if (value === false) {\n return false;\n }\n if (typeof value === \"number\") {\n return value > 0 ? value : false;\n }\n return DEFAULT_STUCK_PROCESSING_TIMEOUT_MINUTES;\n}\n\nexport function createDurableHooksProcessor<TSchema extends AnySchema>(\n fragment: AnyFragnoInstantiatedDatabaseFragment<TSchema>,\n): DurableHooksProcessor | null {\n const durableHooks = (fragment.$internal as DurableHooksInternal).durableHooks;\n if (!durableHooks) {\n return null;\n }\n\n const { namespace, internalFragment } = durableHooks;\n const stuckProcessingTimeoutMinutes = resolveStuckProcessingTimeoutMinutes(\n durableHooks.stuckProcessingTimeoutMinutes,\n );\n const scheduler =\n durableHooks.scheduler ?? (durableHooks.scheduler = createHookScheduler(durableHooks));\n\n return {\n namespace,\n process: async () => scheduler.schedule(),\n drain: async () => scheduler.drain(),\n getNextWakeAt: async () => {\n const services = internalFragment.services as { getDbNow?: () => Promise<Date> };\n const now = services.getDbNow ? await services.getDbNow() : new Date();\n return await internalFragment.inContext(async function () {\n return await this.handlerTx()\n .withServiceCalls(\n () =>\n [\n internalFragment.services.hookService.getNextHookWakeAt(\n namespace,\n stuckProcessingTimeoutMinutes,\n now,\n ),\n ] as const,\n )\n .transform(({ serviceResult: [result] }) => result)\n .execute();\n });\n },\n };\n}\n"],"mappings":";;;AAeA,MAAM,2CAA2C;AAEjD,SAAS,qCAAqC,OAAmD;AAC/F,KAAI,UAAU,MACZ,QAAO;AAET,KAAI,OAAO,UAAU,SACnB,QAAO,QAAQ,IAAI,QAAQ;AAE7B,QAAO;;AAGT,SAAgB,4BACd,UAC8B;CAC9B,MAAM,eAAgB,SAAS,UAAmC;AAClE,KAAI,CAAC,aACH,QAAO;CAGT,MAAM,EAAE,WAAW,qBAAqB;CACxC,MAAM,gCAAgC,qCACpC,aAAa,8BACd;CACD,MAAM,YACJ,aAAa,cAAc,aAAa,YAAY,oBAAoB,aAAa;AAEvF,QAAO;EACL;EACA,SAAS,YAAY,UAAU,UAAU;EACzC,OAAO,YAAY,UAAU,OAAO;EACpC,eAAe,YAAY;GACzB,MAAM,WAAW,iBAAiB;GAClC,MAAM,MAAM,SAAS,WAAW,MAAM,SAAS,UAAU,mBAAG,IAAI,MAAM;AACtE,UAAO,MAAM,iBAAiB,UAAU,iBAAkB;AACxD,WAAO,MAAM,KAAK,WAAW,CAC1B,uBAEG,CACE,iBAAiB,SAAS,YAAY,kBACpC,WACA,+BACA,IACD,CACF,CACJ,CACA,WAAW,EAAE,eAAe,CAAC,cAAc,OAAO,CAClD,SAAS;KACZ;;EAEL"}
|
package/dist/hooks/hooks.d.ts
CHANGED
|
@@ -1,17 +1,25 @@
|
|
|
1
|
+
import { FragnoId } from "../schema/create.js";
|
|
1
2
|
import { RetryPolicy } from "../query/unit-of-work/retry-policy.js";
|
|
3
|
+
import { ExecuteTxOptions, HandlerTxBuilder } from "../query/unit-of-work/execute-unit-of-work.js";
|
|
4
|
+
import "../fragments/internal-fragment.js";
|
|
5
|
+
import "../query/unit-of-work/unit-of-work.js";
|
|
2
6
|
|
|
3
7
|
//#region src/hooks/hooks.d.ts
|
|
4
8
|
|
|
5
9
|
/**
|
|
6
10
|
* Context available in hook functions via `this`.
|
|
7
|
-
* Contains the
|
|
11
|
+
* Contains the idempotency key for idempotency and database access.
|
|
8
12
|
*/
|
|
9
13
|
interface HookContext {
|
|
10
14
|
/**
|
|
11
|
-
* Unique
|
|
15
|
+
* Unique idempotency key for this transaction.
|
|
12
16
|
* Use this for idempotency checks in your hook implementation.
|
|
13
17
|
*/
|
|
14
|
-
|
|
18
|
+
idempotencyKey: string;
|
|
19
|
+
/**
|
|
20
|
+
* Create a handler transaction builder to run atomic operations.
|
|
21
|
+
*/
|
|
22
|
+
handlerTx: HookHandlerTx;
|
|
15
23
|
}
|
|
16
24
|
/**
|
|
17
25
|
* A hook function signature.
|
|
@@ -36,6 +44,11 @@ interface TriggerHookOptions {
|
|
|
36
44
|
* If not provided, uses the default retry policy.
|
|
37
45
|
*/
|
|
38
46
|
retryPolicy?: RetryPolicy;
|
|
47
|
+
/**
|
|
48
|
+
* Absolute time for the first attempt. If in the future, the hook is
|
|
49
|
+
* scheduled for that time; if in the past, it runs immediately.
|
|
50
|
+
*/
|
|
51
|
+
processAt?: Date;
|
|
39
52
|
}
|
|
40
53
|
/**
|
|
41
54
|
* Internal representation of a triggered hook.
|
|
@@ -46,6 +59,36 @@ interface TriggeredHook {
|
|
|
46
59
|
payload: unknown;
|
|
47
60
|
options?: TriggerHookOptions;
|
|
48
61
|
}
|
|
62
|
+
type StuckHookProcessingTimeoutMinutes = number | false;
|
|
63
|
+
type StuckHookProcessingEvent = {
|
|
64
|
+
id: FragnoId;
|
|
65
|
+
hookName: string;
|
|
66
|
+
attempts: number;
|
|
67
|
+
maxAttempts: number;
|
|
68
|
+
lastAttemptAt: Date | null;
|
|
69
|
+
nextRetryAt: Date | null;
|
|
70
|
+
};
|
|
71
|
+
type StuckHookProcessingInfo = {
|
|
72
|
+
namespace: string;
|
|
73
|
+
timeoutMinutes: number;
|
|
74
|
+
events: StuckHookProcessingEvent[];
|
|
75
|
+
};
|
|
76
|
+
type DurableHooksProcessingOptions = {
|
|
77
|
+
/**
|
|
78
|
+
* Re-queue hooks that have been in `processing` for at least this many minutes.
|
|
79
|
+
* Use `false` to disable stuck-processing recovery entirely.
|
|
80
|
+
* Values <= 0 are treated as `false`.
|
|
81
|
+
*
|
|
82
|
+
* Default: 10 minutes.
|
|
83
|
+
*/
|
|
84
|
+
stuckProcessingTimeoutMinutes?: StuckHookProcessingTimeoutMinutes;
|
|
85
|
+
/**
|
|
86
|
+
* Called when stuck processing hooks are detected and re-queued.
|
|
87
|
+
* Invoked after the hooks are moved back to `pending`.
|
|
88
|
+
*/
|
|
89
|
+
onStuckProcessingHooks?: (info: StuckHookProcessingInfo) => void;
|
|
90
|
+
};
|
|
91
|
+
type HookHandlerTx = (execOptions?: Omit<ExecuteTxOptions, "createUnitOfWork">) => HandlerTxBuilder<readonly [], [], [], unknown, unknown, false, false, false, false, HooksMap>;
|
|
49
92
|
//#endregion
|
|
50
|
-
export { HookContext, HookFn, HookPayload, HooksMap, TriggerHookOptions, TriggeredHook };
|
|
93
|
+
export { DurableHooksProcessingOptions, HookContext, HookFn, HookPayload, HooksMap, StuckHookProcessingEvent, StuckHookProcessingInfo, StuckHookProcessingTimeoutMinutes, TriggerHookOptions, TriggeredHook };
|
|
51
94
|
//# sourceMappingURL=hooks.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hooks.d.ts","names":[],"sources":["../../src/hooks/hooks.ts"],"sourcesContent":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"hooks.d.ts","names":[],"sources":["../../src/hooks/hooks.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;AAeA;AAgBA;AAOY,UAvBK,WAAA,CAuBqB;EAK1B;;;;EAA8C,cAAA,EAAA,MAAA;EAKzC;AAiBjB;AAuCA;EAEY,SAAA,EAlFC,aAkFD;;;;;AASZ;AAMY,KA1FA,MA0FA,CAAA,WAAA,OAA6B,CAAA,GAAA,CAAA,OAQP,EAlGiB,QAkGjB,EAAA,GAAA,IAAA,GAlGqC,OAkGrC,CAAA,IAAA,CAKA;AAGlC;;;;AAEK,KArGO,QAAA,GAAW,MAqGlB,CAAA,MAAA,EArGiC,MAqGjC,CAAA,GAAA,CAAA,CAAA;;;;KAhGO,iBAAiB,UAAU,kBAAkB;;;;UAKxC,kBAAA;;;;;gBAKD;;;;;cAKF;;;;;;UAOG,aAAA;;;YAGL;;KAoCA,iCAAA;KAEA,wBAAA;MACN;;;;iBAIW;eACF;;KAGH,uBAAA;;;UAGF;;KAGE,6BAAA;;;;;;;;kCAQsB;;;;;kCAKA;;KAGtB,aAAA,kBACI,KAAK,0CAChB,oFAAoF"}
|
package/dist/hooks/hooks.js
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { ExponentialBackoffRetryPolicy } from "../query/unit-of-work/retry-policy.js";
|
|
2
2
|
|
|
3
3
|
//#region src/hooks/hooks.ts
|
|
4
|
+
const DEFAULT_STUCK_PROCESSING_TIMEOUT_MINUTES = 10;
|
|
5
|
+
function resolveStuckProcessingTimeoutMinutes(value) {
|
|
6
|
+
if (value === false) return false;
|
|
7
|
+
if (typeof value === "number") return value > 0 ? value : false;
|
|
8
|
+
return DEFAULT_STUCK_PROCESSING_TIMEOUT_MINUTES;
|
|
9
|
+
}
|
|
4
10
|
/**
|
|
5
11
|
* Add hook events as mutation operations to the UOW.
|
|
6
12
|
* This should be called before executeMutations() so hook records are created
|
|
@@ -15,6 +21,7 @@ function prepareHookMutations(uow, config) {
|
|
|
15
21
|
const internalUow = uow.forSchema(internalSchema);
|
|
16
22
|
for (const hook of triggeredHooks) {
|
|
17
23
|
const maxAttempts = (hook.options?.retryPolicy ?? retryPolicy).shouldRetry(4) ? 5 : 1;
|
|
24
|
+
const nextRetryAt = (hook.options?.processAt ? new Date(hook.options.processAt) : null) ?? null;
|
|
18
25
|
internalUow.create("fragno_hooks", {
|
|
19
26
|
namespace,
|
|
20
27
|
hookName: hook.hookName,
|
|
@@ -23,9 +30,9 @@ function prepareHookMutations(uow, config) {
|
|
|
23
30
|
attempts: 0,
|
|
24
31
|
maxAttempts,
|
|
25
32
|
lastAttemptAt: null,
|
|
26
|
-
nextRetryAt
|
|
33
|
+
nextRetryAt,
|
|
27
34
|
error: null,
|
|
28
|
-
nonce: uow.
|
|
35
|
+
nonce: uow.idempotencyKey
|
|
29
36
|
});
|
|
30
37
|
}
|
|
31
38
|
}
|
|
@@ -36,53 +43,113 @@ function prepareHookMutations(uow, config) {
|
|
|
36
43
|
async function processHooks(config) {
|
|
37
44
|
const { hooks, namespace, internalFragment, defaultRetryPolicy } = config;
|
|
38
45
|
const retryPolicy = defaultRetryPolicy ?? new ExponentialBackoffRetryPolicy({ maxRetries: 5 });
|
|
46
|
+
const stuckProcessingTimeoutMinutes = resolveStuckProcessingTimeoutMinutes(config.stuckProcessingTimeoutMinutes);
|
|
47
|
+
const getDbNow = async () => {
|
|
48
|
+
const services = internalFragment.services;
|
|
49
|
+
if (services.getDbNow) return services.getDbNow();
|
|
50
|
+
return /* @__PURE__ */ new Date();
|
|
51
|
+
};
|
|
52
|
+
const dbNow = await getDbNow();
|
|
53
|
+
if (stuckProcessingTimeoutMinutes !== false) {
|
|
54
|
+
const staleBefore = /* @__PURE__ */ new Date(dbNow.getTime() - stuckProcessingTimeoutMinutes * 6e4);
|
|
55
|
+
const stuckEvents = await internalFragment.inContext(async function() {
|
|
56
|
+
return await this.handlerTx().withServiceCalls(() => [internalFragment.services.hookService.requeueStuckProcessingHooks(namespace, staleBefore)]).transform(({ serviceResult: [events] }) => events).execute();
|
|
57
|
+
});
|
|
58
|
+
if (stuckEvents.length > 0) try {
|
|
59
|
+
config.onStuckProcessingHooks?.({
|
|
60
|
+
namespace,
|
|
61
|
+
timeoutMinutes: stuckProcessingTimeoutMinutes,
|
|
62
|
+
events: stuckEvents
|
|
63
|
+
});
|
|
64
|
+
} catch (error) {
|
|
65
|
+
console.error("Error calling onStuckProcessingHooks", error);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const pendingEvents = await internalFragment.inContext(async function() {
|
|
69
|
+
return await this.handlerTx().withServiceCalls(() => [internalFragment.services.hookService.claimPendingHookEvents(namespace)]).transform(({ serviceResult: [events] }) => events).execute();
|
|
70
|
+
});
|
|
71
|
+
if (pendingEvents.length === 0) return 0;
|
|
72
|
+
const processedEvents = await Promise.allSettled(pendingEvents.map(async (event) => {
|
|
73
|
+
const hookFn = hooks[event.hookName];
|
|
74
|
+
if (!hookFn) return {
|
|
75
|
+
eventId: event.id,
|
|
76
|
+
status: "failed",
|
|
77
|
+
error: `Hook '${event.hookName}' not found in hooks map`,
|
|
78
|
+
attempts: event.attempts,
|
|
79
|
+
maxAttempts: event.maxAttempts
|
|
80
|
+
};
|
|
81
|
+
try {
|
|
82
|
+
const hookContext = {
|
|
83
|
+
idempotencyKey: event.idempotencyKey,
|
|
84
|
+
handlerTx: config.handlerTx
|
|
85
|
+
};
|
|
86
|
+
await hookFn.call(hookContext, event.payload);
|
|
87
|
+
return {
|
|
88
|
+
eventId: event.id,
|
|
89
|
+
status: "completed"
|
|
90
|
+
};
|
|
91
|
+
} catch (error) {
|
|
92
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
93
|
+
return {
|
|
94
|
+
eventId: event.id,
|
|
95
|
+
status: "failed",
|
|
96
|
+
error: errorMessage,
|
|
97
|
+
attempts: event.attempts,
|
|
98
|
+
maxAttempts: event.maxAttempts
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
}));
|
|
39
102
|
await internalFragment.inContext(async function() {
|
|
40
|
-
|
|
41
|
-
const
|
|
42
|
-
await executeRetrieve();
|
|
43
|
-
const pendingEvents = await pendingEventsPromise;
|
|
44
|
-
if (pendingEvents.length === 0) return;
|
|
45
|
-
const processedEvents = await Promise.allSettled(pendingEvents.map(async (event) => {
|
|
46
|
-
const hookFn = hooks[event.hookName];
|
|
47
|
-
if (!hookFn) return {
|
|
48
|
-
eventId: event.id,
|
|
49
|
-
status: "failed",
|
|
50
|
-
error: `Hook '${event.hookName}' not found in hooks map`,
|
|
51
|
-
attempts: event.attempts,
|
|
52
|
-
maxAttempts: event.maxAttempts
|
|
53
|
-
};
|
|
54
|
-
try {
|
|
55
|
-
const hookContext = { nonce: event.nonce };
|
|
56
|
-
await hookFn.call(hookContext, event.payload);
|
|
57
|
-
return {
|
|
58
|
-
eventId: event.id,
|
|
59
|
-
status: "completed"
|
|
60
|
-
};
|
|
61
|
-
} catch (error) {
|
|
62
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
63
|
-
return {
|
|
64
|
-
eventId: event.id,
|
|
65
|
-
status: "failed",
|
|
66
|
-
error: errorMessage,
|
|
67
|
-
attempts: event.attempts,
|
|
68
|
-
maxAttempts: event.maxAttempts
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
}));
|
|
103
|
+
await this.handlerTx().withServiceCalls(() => {
|
|
104
|
+
const txResults = [];
|
|
72
105
|
for (const processedEvent of processedEvents) {
|
|
73
106
|
if (processedEvent.status === "rejected") continue;
|
|
74
107
|
const { eventId, status } = processedEvent.value;
|
|
75
|
-
if (status === "completed") internalFragment.services.hookService.markHookCompleted(eventId);
|
|
108
|
+
if (status === "completed") txResults.push(internalFragment.services.hookService.markHookCompleted(eventId));
|
|
76
109
|
else if (status === "failed") {
|
|
77
110
|
const { error, attempts } = processedEvent.value;
|
|
78
|
-
internalFragment.services.hookService.markHookFailed(eventId, error, attempts, retryPolicy);
|
|
111
|
+
txResults.push(internalFragment.services.hookService.markHookFailed(eventId, error, attempts, retryPolicy, dbNow));
|
|
79
112
|
}
|
|
80
113
|
}
|
|
81
|
-
|
|
82
|
-
});
|
|
114
|
+
return txResults;
|
|
115
|
+
}).execute();
|
|
83
116
|
});
|
|
117
|
+
return processedEvents.reduce((count, result) => count + (result.status === "fulfilled" ? 1 : 0), 0);
|
|
118
|
+
}
|
|
119
|
+
function createHookScheduler(config) {
|
|
120
|
+
let processing = false;
|
|
121
|
+
let queued = false;
|
|
122
|
+
let currentPromise = null;
|
|
123
|
+
const schedule = async () => {
|
|
124
|
+
if (processing) {
|
|
125
|
+
queued = true;
|
|
126
|
+
return currentPromise ?? Promise.resolve(0);
|
|
127
|
+
}
|
|
128
|
+
processing = true;
|
|
129
|
+
currentPromise = (async () => {
|
|
130
|
+
let lastCount = 0;
|
|
131
|
+
try {
|
|
132
|
+
do {
|
|
133
|
+
queued = false;
|
|
134
|
+
lastCount = await processHooks(config);
|
|
135
|
+
} while (queued);
|
|
136
|
+
return lastCount;
|
|
137
|
+
} finally {
|
|
138
|
+
processing = false;
|
|
139
|
+
queued = false;
|
|
140
|
+
}
|
|
141
|
+
})();
|
|
142
|
+
return currentPromise;
|
|
143
|
+
};
|
|
144
|
+
const drain = async () => {
|
|
145
|
+
while (true) if (await schedule() === 0) return;
|
|
146
|
+
};
|
|
147
|
+
return {
|
|
148
|
+
schedule,
|
|
149
|
+
drain
|
|
150
|
+
};
|
|
84
151
|
}
|
|
85
152
|
|
|
86
153
|
//#endregion
|
|
87
|
-
export {
|
|
154
|
+
export { createHookScheduler, prepareHookMutations };
|
|
88
155
|
//# sourceMappingURL=hooks.js.map
|
package/dist/hooks/hooks.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hooks.js","names":["hookContext: HookContext"],"sources":["../../src/hooks/hooks.ts"],"sourcesContent":["import type { RetryPolicy } from \"../query/unit-of-work/retry-policy\";\nimport { ExponentialBackoffRetryPolicy } from \"../query/unit-of-work/retry-policy\";\nimport type { IUnitOfWork } from \"../query/unit-of-work/unit-of-work\";\nimport type { InternalFragmentInstance } from \"../fragments/internal-fragment\";\n\n/**\n * Context available in hook functions via `this`.\n * Contains the nonce for idempotency and database access.\n */\nexport interface HookContext {\n /**\n * Unique nonce for this transaction.\n * Use this for idempotency checks in your hook implementation.\n */\n nonce: string;\n}\n\n/**\n * A hook function signature.\n * Hooks receive a typed payload and access context via `this`.\n */\nexport type HookFn<TPayload = unknown> = (payload: TPayload) => void | Promise<void>;\n\n/**\n * Map of hook names to hook functions.\n * Used for type-safe hook definitions and triggering.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport type HooksMap = Record<string, HookFn<any>>;\n\n/**\n * Extract the payload type from a hook function.\n */\nexport type HookPayload<T> = T extends HookFn<infer P> ? P : never;\n\n/**\n * Options for triggering a hook.\n */\nexport interface TriggerHookOptions {\n /**\n * Optional retry policy override for this specific hook trigger.\n * If not provided, uses the default retry policy.\n */\n retryPolicy?: RetryPolicy;\n}\n\n/**\n * Internal representation of a triggered hook.\n * Stored in the Unit of Work before execution.\n */\nexport interface TriggeredHook {\n hookName: string;\n payload: unknown;\n options?: TriggerHookOptions;\n}\n\n/**\n * Configuration for hook processing.\n */\nexport interface HookProcessorConfig {\n hooks: HooksMap;\n namespace: string;\n internalFragment: InternalFragmentInstance;\n defaultRetryPolicy?: RetryPolicy;\n}\n\n/**\n * Add hook events as mutation operations to the UOW.\n * This should be called before executeMutations() so hook records are created\n * in the same transaction as the user's mutations.\n */\nexport function prepareHookMutations(uow: IUnitOfWork, config: HookProcessorConfig): void {\n const { namespace, internalFragment, defaultRetryPolicy } = config;\n const retryPolicy = defaultRetryPolicy ?? new ExponentialBackoffRetryPolicy({ maxRetries: 5 });\n\n const triggeredHooks = uow.getTriggeredHooks();\n\n if (triggeredHooks.length === 0) {\n return;\n }\n\n const internalSchema = internalFragment.$internal.deps.schema;\n const internalUow = uow.forSchema(internalSchema);\n\n for (const hook of triggeredHooks) {\n const hookRetryPolicy = hook.options?.retryPolicy ?? retryPolicy;\n const maxAttempts = hookRetryPolicy.shouldRetry(4) ? 5 : 1;\n internalUow.create(\"fragno_hooks\", {\n namespace,\n hookName: hook.hookName,\n payload: hook.payload,\n status: \"pending\",\n attempts: 0,\n maxAttempts,\n lastAttemptAt: null,\n nextRetryAt: null,\n error: null,\n nonce: uow.nonce,\n });\n }\n}\n\n/**\n * Process pending hook events after the transaction has committed.\n * This should be called in the onSuccess callback after executeMutations().\n */\nexport async function processHooks(config: HookProcessorConfig): Promise<void> {\n const { hooks, namespace, internalFragment, defaultRetryPolicy } = config;\n const retryPolicy = defaultRetryPolicy ?? new ExponentialBackoffRetryPolicy({ maxRetries: 5 });\n\n await internalFragment.inContext(async function () {\n return await this.uow(async ({ executeRetrieve, executeMutate }) => {\n const pendingEventsPromise =\n internalFragment.services.hookService.getPendingHookEvents(namespace);\n await executeRetrieve();\n\n const pendingEvents = await pendingEventsPromise;\n\n if (pendingEvents.length === 0) {\n return;\n }\n\n const processedEvents = await Promise.allSettled(\n pendingEvents.map(async (event) => {\n const hookFn = hooks[event.hookName];\n if (!hookFn) {\n return {\n eventId: event.id,\n status: \"failed\" as const,\n error: `Hook '${event.hookName}' not found in hooks map`,\n attempts: event.attempts,\n maxAttempts: event.maxAttempts,\n };\n }\n\n try {\n const hookContext: HookContext = { nonce: event.nonce };\n await hookFn.call(hookContext, event.payload);\n return {\n eventId: event.id,\n status: \"completed\" as const,\n };\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n return {\n eventId: event.id,\n status: \"failed\" as const,\n error: errorMessage,\n attempts: event.attempts,\n maxAttempts: event.maxAttempts,\n };\n }\n }),\n );\n\n for (const processedEvent of processedEvents) {\n if (processedEvent.status === \"rejected\") {\n continue;\n }\n\n const { eventId, status } = processedEvent.value;\n\n if (status === \"completed\") {\n internalFragment.services.hookService.markHookCompleted(eventId);\n } else if (status === \"failed\") {\n const { error, attempts } = processedEvent.value;\n internalFragment.services.hookService.markHookFailed(\n eventId,\n error,\n attempts,\n retryPolicy,\n );\n }\n }\n\n await executeMutate();\n });\n });\n}\n"],"mappings":";;;;;;;;AAuEA,SAAgB,qBAAqB,KAAkB,QAAmC;CACxF,MAAM,EAAE,WAAW,kBAAkB,uBAAuB;CAC5D,MAAM,cAAc,sBAAsB,IAAI,8BAA8B,EAAE,YAAY,GAAG,CAAC;CAE9F,MAAM,iBAAiB,IAAI,mBAAmB;AAE9C,KAAI,eAAe,WAAW,EAC5B;CAGF,MAAM,iBAAiB,iBAAiB,UAAU,KAAK;CACvD,MAAM,cAAc,IAAI,UAAU,eAAe;AAEjD,MAAK,MAAM,QAAQ,gBAAgB;EAEjC,MAAM,eADkB,KAAK,SAAS,eAAe,aACjB,YAAY,EAAE,GAAG,IAAI;AACzD,cAAY,OAAO,gBAAgB;GACjC;GACA,UAAU,KAAK;GACf,SAAS,KAAK;GACd,QAAQ;GACR,UAAU;GACV;GACA,eAAe;GACf,aAAa;GACb,OAAO;GACP,OAAO,IAAI;GACZ,CAAC;;;;;;;AAQN,eAAsB,aAAa,QAA4C;CAC7E,MAAM,EAAE,OAAO,WAAW,kBAAkB,uBAAuB;CACnE,MAAM,cAAc,sBAAsB,IAAI,8BAA8B,EAAE,YAAY,GAAG,CAAC;AAE9F,OAAM,iBAAiB,UAAU,iBAAkB;AACjD,SAAO,MAAM,KAAK,IAAI,OAAO,EAAE,iBAAiB,oBAAoB;GAClE,MAAM,uBACJ,iBAAiB,SAAS,YAAY,qBAAqB,UAAU;AACvE,SAAM,iBAAiB;GAEvB,MAAM,gBAAgB,MAAM;AAE5B,OAAI,cAAc,WAAW,EAC3B;GAGF,MAAM,kBAAkB,MAAM,QAAQ,WACpC,cAAc,IAAI,OAAO,UAAU;IACjC,MAAM,SAAS,MAAM,MAAM;AAC3B,QAAI,CAAC,OACH,QAAO;KACL,SAAS,MAAM;KACf,QAAQ;KACR,OAAO,SAAS,MAAM,SAAS;KAC/B,UAAU,MAAM;KAChB,aAAa,MAAM;KACpB;AAGH,QAAI;KACF,MAAMA,cAA2B,EAAE,OAAO,MAAM,OAAO;AACvD,WAAM,OAAO,KAAK,aAAa,MAAM,QAAQ;AAC7C,YAAO;MACL,SAAS,MAAM;MACf,QAAQ;MACT;aACM,OAAO;KACd,MAAM,eAAe,iBAAiB,QAAQ,MAAM,UAAU,OAAO,MAAM;AAC3E,YAAO;MACL,SAAS,MAAM;MACf,QAAQ;MACR,OAAO;MACP,UAAU,MAAM;MAChB,aAAa,MAAM;MACpB;;KAEH,CACH;AAED,QAAK,MAAM,kBAAkB,iBAAiB;AAC5C,QAAI,eAAe,WAAW,WAC5B;IAGF,MAAM,EAAE,SAAS,WAAW,eAAe;AAE3C,QAAI,WAAW,YACb,kBAAiB,SAAS,YAAY,kBAAkB,QAAQ;aACvD,WAAW,UAAU;KAC9B,MAAM,EAAE,OAAO,aAAa,eAAe;AAC3C,sBAAiB,SAAS,YAAY,eACpC,SACA,OACA,UACA,YACD;;;AAIL,SAAM,eAAe;IACrB;GACF"}
|
|
1
|
+
{"version":3,"file":"hooks.js","names":["hookContext: HookContext","txResults: TxResult<void>[]","currentPromise: Promise<number> | null"],"sources":["../../src/hooks/hooks.ts"],"sourcesContent":["import type {\n ExecuteTxOptions,\n HandlerTxBuilder,\n} from \"../query/unit-of-work/execute-unit-of-work\";\nimport type { RetryPolicy } from \"../query/unit-of-work/retry-policy\";\nimport { ExponentialBackoffRetryPolicy } from \"../query/unit-of-work/retry-policy\";\nimport type { IUnitOfWork } from \"../query/unit-of-work/unit-of-work\";\nimport type { InternalFragmentInstance } from \"../fragments/internal-fragment\";\nimport type { TxResult } from \"../query/unit-of-work/execute-unit-of-work\";\nimport type { FragnoId } from \"../schema/create\";\n\n/**\n * Context available in hook functions via `this`.\n * Contains the idempotency key for idempotency and database access.\n */\nexport interface HookContext {\n /**\n * Unique idempotency key for this transaction.\n * Use this for idempotency checks in your hook implementation.\n */\n idempotencyKey: string;\n /**\n * Create a handler transaction builder to run atomic operations.\n */\n handlerTx: HookHandlerTx;\n}\n\n/**\n * A hook function signature.\n * Hooks receive a typed payload and access context via `this`.\n */\nexport type HookFn<TPayload = unknown> = (payload: TPayload) => void | Promise<void>;\n\n/**\n * Map of hook names to hook functions.\n * Used for type-safe hook definitions and triggering.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport type HooksMap = Record<string, HookFn<any>>;\n\n/**\n * Extract the payload type from a hook function.\n */\nexport type HookPayload<T> = T extends HookFn<infer P> ? P : never;\n\n/**\n * Options for triggering a hook.\n */\nexport interface TriggerHookOptions {\n /**\n * Optional retry policy override for this specific hook trigger.\n * If not provided, uses the default retry policy.\n */\n retryPolicy?: RetryPolicy;\n /**\n * Absolute time for the first attempt. If in the future, the hook is\n * scheduled for that time; if in the past, it runs immediately.\n */\n processAt?: Date;\n}\n\n/**\n * Internal representation of a triggered hook.\n * Stored in the Unit of Work before execution.\n */\nexport interface TriggeredHook {\n hookName: string;\n payload: unknown;\n options?: TriggerHookOptions;\n}\n\nexport type HookScheduler = {\n schedule: () => Promise<number>;\n drain: () => Promise<void>;\n};\n\n/**\n * Configuration for hook processing.\n */\nexport interface HookProcessorConfig<THooks extends HooksMap = HooksMap> {\n hooks: THooks;\n namespace: string;\n internalFragment: InternalFragmentInstance;\n handlerTx: HookHandlerTx;\n /**\n * Internal hook scheduler used to coordinate processing/drain.\n */\n scheduler?: HookScheduler;\n defaultRetryPolicy?: RetryPolicy;\n /**\n * Re-queue hooks that have been in `processing` for at least this many minutes.\n * Use `false` to disable stuck-processing recovery entirely.\n * Values <= 0 are treated as `false`.\n *\n * Default: 10 minutes.\n */\n stuckProcessingTimeoutMinutes?: StuckHookProcessingTimeoutMinutes;\n /**\n * Called when stuck processing hooks are detected and re-queued.\n * Invoked after the hooks are moved back to `pending`.\n */\n onStuckProcessingHooks?: (info: StuckHookProcessingInfo) => void;\n}\n\nexport type StuckHookProcessingTimeoutMinutes = number | false;\n\nexport type StuckHookProcessingEvent = {\n id: FragnoId;\n hookName: string;\n attempts: number;\n maxAttempts: number;\n lastAttemptAt: Date | null;\n nextRetryAt: Date | null;\n};\n\nexport type StuckHookProcessingInfo = {\n namespace: string;\n timeoutMinutes: number;\n events: StuckHookProcessingEvent[];\n};\n\nexport type DurableHooksProcessingOptions = {\n /**\n * Re-queue hooks that have been in `processing` for at least this many minutes.\n * Use `false` to disable stuck-processing recovery entirely.\n * Values <= 0 are treated as `false`.\n *\n * Default: 10 minutes.\n */\n stuckProcessingTimeoutMinutes?: StuckHookProcessingTimeoutMinutes;\n /**\n * Called when stuck processing hooks are detected and re-queued.\n * Invoked after the hooks are moved back to `pending`.\n */\n onStuckProcessingHooks?: (info: StuckHookProcessingInfo) => void;\n};\n\nexport type HookHandlerTx = (\n execOptions?: Omit<ExecuteTxOptions, \"createUnitOfWork\">,\n) => HandlerTxBuilder<readonly [], [], [], unknown, unknown, false, false, false, false, HooksMap>;\n\nconst DEFAULT_STUCK_PROCESSING_TIMEOUT_MINUTES = 10;\n\nfunction resolveStuckProcessingTimeoutMinutes(\n value: StuckHookProcessingTimeoutMinutes | undefined,\n): number | false {\n if (value === false) {\n return false;\n }\n if (typeof value === \"number\") {\n return value > 0 ? value : false;\n }\n return DEFAULT_STUCK_PROCESSING_TIMEOUT_MINUTES;\n}\n\n/**\n * Add hook events as mutation operations to the UOW.\n * This should be called before executeMutations() so hook records are created\n * in the same transaction as the user's mutations.\n */\nexport function prepareHookMutations<THooks extends HooksMap>(\n uow: IUnitOfWork,\n config: HookProcessorConfig<THooks>,\n): void {\n const { namespace, internalFragment, defaultRetryPolicy } = config;\n const retryPolicy = defaultRetryPolicy ?? new ExponentialBackoffRetryPolicy({ maxRetries: 5 });\n\n const triggeredHooks = uow.getTriggeredHooks();\n\n if (triggeredHooks.length === 0) {\n return;\n }\n\n const internalSchema = internalFragment.$internal.deps.schema;\n const internalUow = uow.forSchema(internalSchema);\n\n for (const hook of triggeredHooks) {\n const hookRetryPolicy = hook.options?.retryPolicy ?? retryPolicy;\n const maxAttempts = hookRetryPolicy.shouldRetry(4) ? 5 : 1;\n const processAt = hook.options?.processAt ? new Date(hook.options.processAt) : null;\n const nextRetryAt = processAt ?? null;\n internalUow.create(\"fragno_hooks\", {\n namespace,\n hookName: hook.hookName,\n payload: hook.payload,\n status: \"pending\",\n attempts: 0,\n maxAttempts,\n lastAttemptAt: null,\n nextRetryAt,\n error: null,\n nonce: uow.idempotencyKey,\n });\n }\n}\n\n/**\n * Process pending hook events after the transaction has committed.\n * This should be called in the onSuccess callback after executeMutations().\n */\nexport async function processHooks<THooks extends HooksMap>(\n config: HookProcessorConfig<THooks>,\n): Promise<number> {\n const { hooks, namespace, internalFragment, defaultRetryPolicy } = config;\n const retryPolicy = defaultRetryPolicy ?? new ExponentialBackoffRetryPolicy({ maxRetries: 5 });\n const stuckProcessingTimeoutMinutes = resolveStuckProcessingTimeoutMinutes(\n config.stuckProcessingTimeoutMinutes,\n );\n const getDbNow = async () => {\n const services = internalFragment.services as { getDbNow?: () => Promise<Date> };\n if (services.getDbNow) {\n return services.getDbNow();\n }\n return new Date();\n };\n const dbNow = await getDbNow();\n\n if (stuckProcessingTimeoutMinutes !== false) {\n const staleBefore = new Date(dbNow.getTime() - stuckProcessingTimeoutMinutes * 60_000);\n const stuckEvents = await internalFragment.inContext(async function () {\n return await this.handlerTx()\n .withServiceCalls(\n () =>\n [\n internalFragment.services.hookService.requeueStuckProcessingHooks(\n namespace,\n staleBefore,\n ),\n ] as const,\n )\n .transform(({ serviceResult: [events] }) => events)\n .execute();\n });\n\n if (stuckEvents.length > 0) {\n try {\n config.onStuckProcessingHooks?.({\n namespace,\n timeoutMinutes: stuckProcessingTimeoutMinutes,\n events: stuckEvents,\n });\n } catch (error) {\n console.error(\"Error calling onStuckProcessingHooks\", error);\n }\n }\n }\n\n // Claim pending events in the same transaction to avoid double-processing.\n const pendingEvents = await internalFragment.inContext(async function () {\n return await this.handlerTx()\n .withServiceCalls(\n () => [internalFragment.services.hookService.claimPendingHookEvents(namespace)] as const,\n )\n .transform(({ serviceResult: [events] }) => events)\n .execute();\n });\n\n if (pendingEvents.length === 0) {\n return 0;\n }\n\n // Process events (async work outside transaction)\n const processedEvents = await Promise.allSettled(\n pendingEvents.map(async (event) => {\n const hookFn = hooks[event.hookName];\n if (!hookFn) {\n return {\n eventId: event.id,\n status: \"failed\" as const,\n error: `Hook '${event.hookName}' not found in hooks map`,\n attempts: event.attempts,\n maxAttempts: event.maxAttempts,\n };\n }\n\n try {\n const hookContext: HookContext = {\n idempotencyKey: event.idempotencyKey,\n handlerTx: config.handlerTx,\n };\n await hookFn.call(hookContext, event.payload);\n return {\n eventId: event.id,\n status: \"completed\" as const,\n };\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n return {\n eventId: event.id,\n status: \"failed\" as const,\n error: errorMessage,\n attempts: event.attempts,\n maxAttempts: event.maxAttempts,\n };\n }\n }),\n );\n\n // Mark events as completed/failed\n await internalFragment.inContext(async function () {\n await this.handlerTx()\n .withServiceCalls(() => {\n const txResults: TxResult<void>[] = [];\n for (const processedEvent of processedEvents) {\n if (processedEvent.status === \"rejected\") {\n continue;\n }\n\n const { eventId, status } = processedEvent.value;\n\n if (status === \"completed\") {\n txResults.push(internalFragment.services.hookService.markHookCompleted(eventId));\n } else if (status === \"failed\") {\n const { error, attempts } = processedEvent.value;\n txResults.push(\n internalFragment.services.hookService.markHookFailed(\n eventId,\n error,\n attempts,\n retryPolicy,\n dbNow,\n ),\n );\n }\n }\n return txResults;\n })\n .execute();\n });\n\n const processedCount = processedEvents.reduce(\n (count, result) => count + (result.status === \"fulfilled\" ? 1 : 0),\n 0,\n );\n\n return processedCount;\n}\n\nexport function createHookScheduler(config: HookProcessorConfig): HookScheduler {\n let processing = false;\n let queued = false;\n let currentPromise: Promise<number> | null = null;\n\n const schedule = async () => {\n if (processing) {\n queued = true;\n return currentPromise ?? Promise.resolve(0);\n }\n\n processing = true;\n currentPromise = (async () => {\n let lastCount = 0;\n try {\n do {\n queued = false;\n lastCount = await processHooks(config);\n } while (queued);\n return lastCount;\n } finally {\n processing = false;\n queued = false;\n }\n })();\n\n return currentPromise;\n };\n\n const drain = async () => {\n while (true) {\n const processed = await schedule();\n if (processed === 0) {\n return;\n }\n }\n };\n\n return { schedule, drain };\n}\n"],"mappings":"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"}
|
|
@@ -16,7 +16,7 @@ import "../schema/create.js";
|
|
|
16
16
|
*
|
|
17
17
|
* @example
|
|
18
18
|
* ```ts
|
|
19
|
-
* const mySchema = schema(s => s
|
|
19
|
+
* const mySchema = schema("my", s => s
|
|
20
20
|
* .addTable("users", t => t.addColumn("id", idColumn())) // version 1
|
|
21
21
|
* .addTable("posts", t => t.addColumn("id", idColumn())) // version 2
|
|
22
22
|
* );
|
|
@@ -92,16 +92,19 @@ function generateMigrationFromSchema(targetSchema, fromVersion, toVersion) {
|
|
|
92
92
|
columns: subOp.columns,
|
|
93
93
|
unique: subOp.unique
|
|
94
94
|
});
|
|
95
|
-
} else if (op.type === "add-reference")
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
95
|
+
} else if (op.type === "add-reference") {
|
|
96
|
+
if (!op.referenceName || op.referenceName.trim().length === 0) throw new Error(`referenceName is required for add-reference on ${op.tableName}`);
|
|
97
|
+
migrationOperations.push({
|
|
98
|
+
type: "add-foreign-key",
|
|
99
|
+
table: op.tableName,
|
|
100
|
+
value: {
|
|
101
|
+
name: op.referenceName,
|
|
102
|
+
columns: [op.config.from.column],
|
|
103
|
+
referencedTable: op.config.to.table,
|
|
104
|
+
referencedColumns: [op.config.to.column]
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
}
|
|
105
108
|
return migrationOperations;
|
|
106
109
|
}
|
|
107
110
|
|