@beignet/provider-db-drizzle 0.0.36 → 0.0.38
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/CHANGELOG.md +17 -0
- package/README.md +130 -11
- package/dist/mysql/index.d.ts +27 -8
- package/dist/mysql/index.d.ts.map +1 -1
- package/dist/mysql/index.js +50 -52
- package/dist/mysql/index.js.map +1 -1
- package/dist/mysql/schema.d.ts +17 -0
- package/dist/mysql/schema.d.ts.map +1 -1
- package/dist/mysql/schema.js +1 -0
- package/dist/mysql/schema.js.map +1 -1
- package/dist/postgres/index.d.ts +31 -5
- package/dist/postgres/index.d.ts.map +1 -1
- package/dist/postgres/index.js +41 -51
- package/dist/postgres/index.js.map +1 -1
- package/dist/postgres/schema.d.ts +17 -0
- package/dist/postgres/schema.d.ts.map +1 -1
- package/dist/postgres/schema.js +1 -0
- package/dist/postgres/schema.js.map +1 -1
- package/dist/shared/idempotency-core.d.ts +2 -0
- package/dist/shared/idempotency-core.d.ts.map +1 -1
- package/dist/shared/idempotency-core.js +56 -8
- package/dist/shared/idempotency-core.js.map +1 -1
- package/dist/shared/outbox-core.d.ts.map +1 -1
- package/dist/shared/outbox-core.js +2 -0
- package/dist/shared/outbox-core.js.map +1 -1
- package/dist/shared/rows.d.ts +1 -0
- package/dist/shared/rows.d.ts.map +1 -1
- package/dist/shared/rows.js +13 -0
- package/dist/shared/rows.js.map +1 -1
- package/dist/sqlite/index.d.ts +20 -2
- package/dist/sqlite/index.d.ts.map +1 -1
- package/dist/sqlite/index.js +38 -50
- package/dist/sqlite/index.js.map +1 -1
- package/dist/sqlite/schema.d.ts +19 -0
- package/dist/sqlite/schema.d.ts.map +1 -1
- package/dist/sqlite/schema.js +1 -0
- package/dist/sqlite/schema.js.map +1 -1
- package/package.json +3 -2
- package/skills/database-provider/SKILL.md +29 -1
- package/src/mysql/index.ts +122 -45
- package/src/mysql/schema.ts +1 -0
- package/src/postgres/index.ts +119 -42
- package/src/postgres/schema.ts +1 -0
- package/src/shared/idempotency-core.ts +88 -7
- package/src/shared/outbox-core.ts +2 -0
- package/src/shared/rows.ts +13 -0
- package/src/sqlite/index.ts +103 -36
- package/src/sqlite/schema.ts +1 -0
package/src/mysql/index.ts
CHANGED
|
@@ -41,16 +41,17 @@ import {
|
|
|
41
41
|
type AnyProviderConfigSchema,
|
|
42
42
|
createProvider,
|
|
43
43
|
createProviderInstrumentation,
|
|
44
|
+
type ProviderInstrumentationTarget,
|
|
44
45
|
type ServiceProvider,
|
|
45
46
|
} from "@beignet/core/providers";
|
|
46
47
|
import { type ExtractTablesWithRelations, type SQL, sql } from "drizzle-orm";
|
|
47
48
|
import type {
|
|
48
49
|
MySqlDatabase,
|
|
49
50
|
MySqlQueryResultHKT,
|
|
50
|
-
MySqlTransaction,
|
|
51
51
|
MySqlTransactionConfig,
|
|
52
52
|
PreparedQueryHKTBase,
|
|
53
53
|
} from "drizzle-orm/mysql-core";
|
|
54
|
+
import { MySqlTransaction } from "drizzle-orm/mysql-core";
|
|
54
55
|
import { drizzle, type MySql2Database } from "drizzle-orm/mysql2";
|
|
55
56
|
import type { Pool, PoolOptions } from "mysql2/promise";
|
|
56
57
|
import * as mysql from "mysql2/promise";
|
|
@@ -192,6 +193,7 @@ export function createDrizzleMysqlUnitOfWork<
|
|
|
192
193
|
const events = createDomainEventRecorder();
|
|
193
194
|
|
|
194
195
|
const result = await options.db.transaction(async (tx) => {
|
|
196
|
+
markMysqlTransactionClient(tx as object);
|
|
195
197
|
const txPorts = options.createTransactionPorts(
|
|
196
198
|
tx as DrizzleMysqlTransaction<TSchema>,
|
|
197
199
|
events,
|
|
@@ -300,6 +302,12 @@ function isMysqlDeadlockError(error: unknown): boolean {
|
|
|
300
302
|
|
|
301
303
|
const MYSQL_DEADLOCK_RETRY_BASE_MS = 10;
|
|
302
304
|
const MYSQL_DEADLOCK_RETRY_MAX_MS = 250;
|
|
305
|
+
const mysqlTransactionClients = new WeakSet<object>();
|
|
306
|
+
|
|
307
|
+
function markMysqlTransactionClient<T extends object>(client: T): T {
|
|
308
|
+
mysqlTransactionClients.add(client);
|
|
309
|
+
return client;
|
|
310
|
+
}
|
|
303
311
|
|
|
304
312
|
function mysqlDeadlockRetryDelayMs(attempt: number): number {
|
|
305
313
|
const exponentialDelay = Math.min(
|
|
@@ -343,6 +351,8 @@ interface DrizzleMysqlSqlExecutor
|
|
|
343
351
|
function createMysqlExecutor<TSchema extends Record<string, unknown>>(
|
|
344
352
|
db: DrizzleMysqlDatabase<TSchema>,
|
|
345
353
|
table: SQL,
|
|
354
|
+
transactionScoped = db instanceof MySqlTransaction ||
|
|
355
|
+
mysqlTransactionClients.has(db as object),
|
|
346
356
|
): DrizzleMysqlSqlExecutor {
|
|
347
357
|
return {
|
|
348
358
|
table,
|
|
@@ -391,13 +401,20 @@ function createMysqlExecutor<TSchema extends Record<string, unknown>>(
|
|
|
391
401
|
},
|
|
392
402
|
|
|
393
403
|
withTransaction(fn) {
|
|
404
|
+
if (transactionScoped) return undefined;
|
|
394
405
|
if ("transaction" in db && typeof db.transaction === "function") {
|
|
395
406
|
// Outbox claims and idempotency reservations are small Beignet-owned
|
|
396
407
|
// transactions with no app callbacks. Retrying a deadlock victim is
|
|
397
408
|
// therefore safe and lets contenders classify against the winner.
|
|
398
409
|
return runMysqlInternalTransaction(() =>
|
|
399
410
|
db.transaction((tx) =>
|
|
400
|
-
fn(
|
|
411
|
+
fn(
|
|
412
|
+
createMysqlExecutor(
|
|
413
|
+
markMysqlTransactionClient(tx as DrizzleMysqlDatabase<TSchema>),
|
|
414
|
+
table,
|
|
415
|
+
true,
|
|
416
|
+
),
|
|
417
|
+
),
|
|
401
418
|
),
|
|
402
419
|
);
|
|
403
420
|
}
|
|
@@ -509,6 +526,7 @@ export function createDrizzleMysqlOutboxSetupStatements(
|
|
|
509
526
|
kind varchar(32) NOT NULL,
|
|
510
527
|
name varchar(255) NOT NULL,
|
|
511
528
|
payload_json longtext NOT NULL,
|
|
529
|
+
trace_context_json longtext,
|
|
512
530
|
status varchar(32) NOT NULL,
|
|
513
531
|
attempts int NOT NULL DEFAULT 0,
|
|
514
532
|
max_attempts int NOT NULL DEFAULT 3,
|
|
@@ -712,12 +730,22 @@ export type DrizzleMysqlProvider<
|
|
|
712
730
|
Record<PortName, DbPort<TSchema>>
|
|
713
731
|
>;
|
|
714
732
|
|
|
733
|
+
/** Drizzle MySQL provider backed by an app-supplied pool. */
|
|
734
|
+
export type InjectedDrizzleMysqlProvider<
|
|
735
|
+
TSchema extends Record<string, unknown>,
|
|
736
|
+
PortName extends string = "db",
|
|
737
|
+
> = ServiceProvider<
|
|
738
|
+
unknown,
|
|
739
|
+
AnyProviderConfigSchema<void>,
|
|
740
|
+
Record<PortName, DbPort<TSchema>>
|
|
741
|
+
>;
|
|
742
|
+
|
|
715
743
|
/**
|
|
716
744
|
* Options for creating a Drizzle MySQL provider.
|
|
717
745
|
*
|
|
718
746
|
* @template TSchema - The Drizzle schema type
|
|
719
747
|
*/
|
|
720
|
-
|
|
748
|
+
interface DrizzleMysqlProviderBaseOptions<
|
|
721
749
|
TSchema extends Record<string, unknown>,
|
|
722
750
|
PortName extends string = "db",
|
|
723
751
|
> {
|
|
@@ -735,12 +763,6 @@ export interface DrizzleMysqlProviderOptions<
|
|
|
735
763
|
*/
|
|
736
764
|
portName?: PortName;
|
|
737
765
|
|
|
738
|
-
/**
|
|
739
|
-
* Optional mysql2 pool options merged into the pool created from
|
|
740
|
-
* `MYSQL_DB_URL` (e.g. `connectionLimit`, `timezone`, TLS settings).
|
|
741
|
-
*/
|
|
742
|
-
pool?: Omit<PoolOptions, "uri">;
|
|
743
|
-
|
|
744
766
|
/**
|
|
745
767
|
* Drizzle relational-query mode. Use "planetscale" when connecting to
|
|
746
768
|
* PlanetScale; defaults to "default".
|
|
@@ -748,6 +770,26 @@ export interface DrizzleMysqlProviderOptions<
|
|
|
748
770
|
mode?: "default" | "planetscale";
|
|
749
771
|
}
|
|
750
772
|
|
|
773
|
+
export type DrizzleMysqlProviderOptions<
|
|
774
|
+
TSchema extends Record<string, unknown>,
|
|
775
|
+
PortName extends string = "db",
|
|
776
|
+
> = DrizzleMysqlProviderBaseOptions<TSchema, PortName> &
|
|
777
|
+
(
|
|
778
|
+
| {
|
|
779
|
+
/** Pool options for the provider-created `MYSQL_DB_URL` pool. */
|
|
780
|
+
pool?: Omit<PoolOptions, "uri">;
|
|
781
|
+
client?: never;
|
|
782
|
+
closeOnStop?: never;
|
|
783
|
+
}
|
|
784
|
+
| {
|
|
785
|
+
/** App-supplied mysql2 pool, caller-owned by default. */
|
|
786
|
+
client: Pool;
|
|
787
|
+
pool?: never;
|
|
788
|
+
/** Close the injected pool when the provider stops. @default false */
|
|
789
|
+
closeOnStop?: boolean;
|
|
790
|
+
}
|
|
791
|
+
);
|
|
792
|
+
|
|
751
793
|
/**
|
|
752
794
|
* Create a Drizzle MySQL provider factory.
|
|
753
795
|
*
|
|
@@ -769,14 +811,83 @@ export interface DrizzleMysqlProviderOptions<
|
|
|
769
811
|
* export const drizzleMysqlProvider = createDrizzleMysqlProvider({ schema });
|
|
770
812
|
* ```
|
|
771
813
|
*/
|
|
814
|
+
export function createDrizzleMysqlProvider<
|
|
815
|
+
TSchema extends Record<string, unknown>,
|
|
816
|
+
PortName extends string = "db",
|
|
817
|
+
>(
|
|
818
|
+
options: DrizzleMysqlProviderBaseOptions<TSchema, PortName> & {
|
|
819
|
+
client: Pool;
|
|
820
|
+
pool?: never;
|
|
821
|
+
closeOnStop?: boolean;
|
|
822
|
+
},
|
|
823
|
+
): InjectedDrizzleMysqlProvider<TSchema, PortName>;
|
|
824
|
+
export function createDrizzleMysqlProvider<
|
|
825
|
+
TSchema extends Record<string, unknown>,
|
|
826
|
+
PortName extends string = "db",
|
|
827
|
+
>(
|
|
828
|
+
options: DrizzleMysqlProviderBaseOptions<TSchema, PortName> & {
|
|
829
|
+
pool?: Omit<PoolOptions, "uri">;
|
|
830
|
+
client?: never;
|
|
831
|
+
closeOnStop?: never;
|
|
832
|
+
},
|
|
833
|
+
): DrizzleMysqlProvider<TSchema, PortName>;
|
|
772
834
|
export function createDrizzleMysqlProvider<
|
|
773
835
|
TSchema extends Record<string, unknown>,
|
|
774
836
|
PortName extends string = "db",
|
|
775
837
|
>(
|
|
776
838
|
options: DrizzleMysqlProviderOptions<TSchema, PortName>,
|
|
777
|
-
):
|
|
839
|
+
):
|
|
840
|
+
| DrizzleMysqlProvider<TSchema, PortName>
|
|
841
|
+
| InjectedDrizzleMysqlProvider<TSchema, PortName> {
|
|
778
842
|
const { schema, portName = "db", mode = "default" } = options;
|
|
779
843
|
|
|
844
|
+
const setupPool = (ports: unknown, pool: Pool, closeOnStop: boolean) => {
|
|
845
|
+
const instrumentation = createProviderInstrumentation(
|
|
846
|
+
ports as ProviderInstrumentationTarget,
|
|
847
|
+
{
|
|
848
|
+
providerName: "drizzle-mysql",
|
|
849
|
+
watcher: "db",
|
|
850
|
+
},
|
|
851
|
+
);
|
|
852
|
+
const logger = createDbQueryLogger(instrumentation, portName);
|
|
853
|
+
const db: MySql2Database<TSchema> = logger
|
|
854
|
+
? drizzle(pool, { schema, logger, mode })
|
|
855
|
+
: drizzle(pool, { schema, mode });
|
|
856
|
+
const dbPort: DbPort<TSchema> = {
|
|
857
|
+
drizzle: db,
|
|
858
|
+
pool,
|
|
859
|
+
checkHealth: () => checkMysqlHealth(pool),
|
|
860
|
+
};
|
|
861
|
+
|
|
862
|
+
return {
|
|
863
|
+
ports: { [portName]: dbPort } as Record<PortName, DbPort<TSchema>>,
|
|
864
|
+
async stop() {
|
|
865
|
+
if (!closeOnStop) return;
|
|
866
|
+
try {
|
|
867
|
+
await pool.end();
|
|
868
|
+
} catch (error) {
|
|
869
|
+
console.error(
|
|
870
|
+
"[drizzleMysqlProvider] Error closing database connection:",
|
|
871
|
+
error,
|
|
872
|
+
);
|
|
873
|
+
}
|
|
874
|
+
},
|
|
875
|
+
};
|
|
876
|
+
};
|
|
877
|
+
|
|
878
|
+
if (options.client) {
|
|
879
|
+
return createProvider<
|
|
880
|
+
unknown,
|
|
881
|
+
AnyProviderConfigSchema<void>,
|
|
882
|
+
Record<PortName, DbPort<TSchema>>
|
|
883
|
+
>({
|
|
884
|
+
name: "drizzle-mysql",
|
|
885
|
+
setup({ ports }) {
|
|
886
|
+
return setupPool(ports, options.client, options.closeOnStop ?? false);
|
|
887
|
+
},
|
|
888
|
+
});
|
|
889
|
+
}
|
|
890
|
+
|
|
780
891
|
return createProvider({
|
|
781
892
|
name: "drizzle-mysql",
|
|
782
893
|
|
|
@@ -798,41 +909,7 @@ export function createDrizzleMysqlProvider<
|
|
|
798
909
|
...options.pool,
|
|
799
910
|
});
|
|
800
911
|
|
|
801
|
-
|
|
802
|
-
providerName: "drizzle-mysql",
|
|
803
|
-
watcher: "db",
|
|
804
|
-
});
|
|
805
|
-
|
|
806
|
-
const logger = createDbQueryLogger(instrumentation, portName);
|
|
807
|
-
|
|
808
|
-
// 2. Create typed Drizzle instance (mode is required when a schema is
|
|
809
|
-
// provided to drizzle-orm/mysql2)
|
|
810
|
-
const db: MySql2Database<TSchema> = logger
|
|
811
|
-
? drizzle(pool, { schema, logger, mode })
|
|
812
|
-
: drizzle(pool, { schema, mode });
|
|
813
|
-
|
|
814
|
-
// 3. Build a typed DbPort
|
|
815
|
-
const dbPort: DbPort<TSchema> = {
|
|
816
|
-
drizzle: db,
|
|
817
|
-
pool,
|
|
818
|
-
checkHealth: () => checkMysqlHealth(pool),
|
|
819
|
-
};
|
|
820
|
-
|
|
821
|
-
return {
|
|
822
|
-
ports: { [portName]: dbPort } as Record<PortName, DbPort<TSchema>>,
|
|
823
|
-
async stop() {
|
|
824
|
-
// Gracefully close the database connection pool
|
|
825
|
-
try {
|
|
826
|
-
await dbPort.pool.end();
|
|
827
|
-
} catch (error) {
|
|
828
|
-
// Log error but don't throw - we're shutting down anyway
|
|
829
|
-
console.error(
|
|
830
|
-
"[drizzleMysqlProvider] Error closing database connection:",
|
|
831
|
-
error,
|
|
832
|
-
);
|
|
833
|
-
}
|
|
834
|
-
},
|
|
835
|
-
};
|
|
912
|
+
return setupPool(ports, pool, true);
|
|
836
913
|
},
|
|
837
914
|
});
|
|
838
915
|
}
|
package/src/mysql/schema.ts
CHANGED
|
@@ -49,6 +49,7 @@ export const outboxMessages = mysqlTable(
|
|
|
49
49
|
kind: varchar("kind", { length: 32 }).notNull(),
|
|
50
50
|
name: varchar("name", { length: 255 }).notNull(),
|
|
51
51
|
payloadJson: longtext("payload_json").notNull(),
|
|
52
|
+
traceContextJson: longtext("trace_context_json"),
|
|
52
53
|
status: varchar("status", { length: 32 }).notNull(),
|
|
53
54
|
attempts: int("attempts").notNull().default(0),
|
|
54
55
|
maxAttempts: int("max_attempts").notNull().default(3),
|
package/src/postgres/index.ts
CHANGED
|
@@ -41,6 +41,7 @@ import {
|
|
|
41
41
|
type AnyProviderConfigSchema,
|
|
42
42
|
createProvider,
|
|
43
43
|
createProviderInstrumentation,
|
|
44
|
+
type ProviderInstrumentationTarget,
|
|
44
45
|
type ServiceProvider,
|
|
45
46
|
} from "@beignet/core/providers";
|
|
46
47
|
import { type ExtractTablesWithRelations, type SQL, sql } from "drizzle-orm";
|
|
@@ -401,6 +402,7 @@ export function createDrizzlePostgresOutboxSetupStatements(
|
|
|
401
402
|
kind text NOT NULL,
|
|
402
403
|
name text NOT NULL,
|
|
403
404
|
payload_json text NOT NULL,
|
|
405
|
+
trace_context_json text,
|
|
404
406
|
status text NOT NULL,
|
|
405
407
|
attempts integer DEFAULT 0 NOT NULL,
|
|
406
408
|
max_attempts integer DEFAULT 3 NOT NULL,
|
|
@@ -605,12 +607,24 @@ export type DrizzlePostgresProvider<
|
|
|
605
607
|
Record<PortName, DbPort<TSchema>>
|
|
606
608
|
>;
|
|
607
609
|
|
|
610
|
+
/**
|
|
611
|
+
* Drizzle Postgres provider backed by an app-supplied pool.
|
|
612
|
+
*/
|
|
613
|
+
export type InjectedDrizzlePostgresProvider<
|
|
614
|
+
TSchema extends Record<string, unknown>,
|
|
615
|
+
PortName extends string = "db",
|
|
616
|
+
> = ServiceProvider<
|
|
617
|
+
unknown,
|
|
618
|
+
AnyProviderConfigSchema<void>,
|
|
619
|
+
Record<PortName, DbPort<TSchema>>
|
|
620
|
+
>;
|
|
621
|
+
|
|
608
622
|
/**
|
|
609
623
|
* Options for creating a Drizzle Postgres provider.
|
|
610
624
|
*
|
|
611
625
|
* @template TSchema - The Drizzle schema type
|
|
612
626
|
*/
|
|
613
|
-
|
|
627
|
+
interface DrizzlePostgresProviderBaseOptions<
|
|
614
628
|
TSchema extends Record<string, unknown>,
|
|
615
629
|
PortName extends string = "db",
|
|
616
630
|
> {
|
|
@@ -627,14 +641,34 @@ export interface DrizzlePostgresProviderOptions<
|
|
|
627
641
|
* with its own env prefix instead.
|
|
628
642
|
*/
|
|
629
643
|
portName?: PortName;
|
|
630
|
-
|
|
631
|
-
/**
|
|
632
|
-
* Optional node-postgres pool configuration merged into the pool created
|
|
633
|
-
* from `POSTGRES_DB_URL`, e.g. `max`, `ssl`, or timeouts.
|
|
634
|
-
*/
|
|
635
|
-
pool?: Omit<PoolConfig, "connectionString">;
|
|
636
644
|
}
|
|
637
645
|
|
|
646
|
+
export type DrizzlePostgresProviderOptions<
|
|
647
|
+
TSchema extends Record<string, unknown>,
|
|
648
|
+
PortName extends string = "db",
|
|
649
|
+
> = DrizzlePostgresProviderBaseOptions<TSchema, PortName> &
|
|
650
|
+
(
|
|
651
|
+
| {
|
|
652
|
+
/**
|
|
653
|
+
* Optional node-postgres pool configuration merged into the pool
|
|
654
|
+
* created from `POSTGRES_DB_URL`.
|
|
655
|
+
*/
|
|
656
|
+
pool?: Omit<PoolConfig, "connectionString">;
|
|
657
|
+
client?: never;
|
|
658
|
+
closeOnStop?: never;
|
|
659
|
+
}
|
|
660
|
+
| {
|
|
661
|
+
/**
|
|
662
|
+
* App-supplied node-postgres pool. Injected pools do not require
|
|
663
|
+
* provider configuration and remain caller-owned by default.
|
|
664
|
+
*/
|
|
665
|
+
client: Pool;
|
|
666
|
+
pool?: never;
|
|
667
|
+
/** Close the injected pool when the provider stops. @default false */
|
|
668
|
+
closeOnStop?: boolean;
|
|
669
|
+
}
|
|
670
|
+
);
|
|
671
|
+
|
|
638
672
|
/**
|
|
639
673
|
* Create a Drizzle Postgres provider factory.
|
|
640
674
|
*
|
|
@@ -658,14 +692,90 @@ export interface DrizzlePostgresProviderOptions<
|
|
|
658
692
|
* export const drizzlePostgresProvider = createDrizzlePostgresProvider({ schema });
|
|
659
693
|
* ```
|
|
660
694
|
*/
|
|
695
|
+
export function createDrizzlePostgresProvider<
|
|
696
|
+
TSchema extends Record<string, unknown>,
|
|
697
|
+
PortName extends string = "db",
|
|
698
|
+
>(
|
|
699
|
+
options: DrizzlePostgresProviderBaseOptions<TSchema, PortName> & {
|
|
700
|
+
client: Pool;
|
|
701
|
+
pool?: never;
|
|
702
|
+
closeOnStop?: boolean;
|
|
703
|
+
},
|
|
704
|
+
): InjectedDrizzlePostgresProvider<TSchema, PortName>;
|
|
705
|
+
export function createDrizzlePostgresProvider<
|
|
706
|
+
TSchema extends Record<string, unknown>,
|
|
707
|
+
PortName extends string = "db",
|
|
708
|
+
>(
|
|
709
|
+
options: DrizzlePostgresProviderBaseOptions<TSchema, PortName> & {
|
|
710
|
+
pool?: Omit<PoolConfig, "connectionString">;
|
|
711
|
+
client?: never;
|
|
712
|
+
closeOnStop?: never;
|
|
713
|
+
},
|
|
714
|
+
): DrizzlePostgresProvider<TSchema, PortName>;
|
|
661
715
|
export function createDrizzlePostgresProvider<
|
|
662
716
|
TSchema extends Record<string, unknown>,
|
|
663
717
|
PortName extends string = "db",
|
|
664
718
|
>(
|
|
665
719
|
options: DrizzlePostgresProviderOptions<TSchema, PortName>,
|
|
666
|
-
):
|
|
720
|
+
):
|
|
721
|
+
| DrizzlePostgresProvider<TSchema, PortName>
|
|
722
|
+
| InjectedDrizzlePostgresProvider<TSchema, PortName> {
|
|
723
|
+
return createDrizzlePostgresProviderImplementation(options);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
function createDrizzlePostgresProviderImplementation<
|
|
727
|
+
TSchema extends Record<string, unknown>,
|
|
728
|
+
PortName extends string = "db",
|
|
729
|
+
>(options: DrizzlePostgresProviderOptions<TSchema, PortName>) {
|
|
667
730
|
const { schema, portName = "db" } = options;
|
|
668
731
|
|
|
732
|
+
const setupPool = (ports: unknown, pool: Pool, closeOnStop: boolean) => {
|
|
733
|
+
const instrumentation = createProviderInstrumentation(
|
|
734
|
+
ports as ProviderInstrumentationTarget,
|
|
735
|
+
{
|
|
736
|
+
providerName: "drizzle-postgres",
|
|
737
|
+
watcher: "db",
|
|
738
|
+
},
|
|
739
|
+
);
|
|
740
|
+
const logger = createDbQueryLogger(instrumentation, portName);
|
|
741
|
+
const db: NodePgDatabase<TSchema> = logger
|
|
742
|
+
? drizzle(pool, { schema, logger })
|
|
743
|
+
: drizzle(pool, { schema });
|
|
744
|
+
const dbPort: DbPort<TSchema> = {
|
|
745
|
+
drizzle: db,
|
|
746
|
+
pool,
|
|
747
|
+
checkHealth: () => checkPostgresHealth(pool),
|
|
748
|
+
};
|
|
749
|
+
|
|
750
|
+
return {
|
|
751
|
+
ports: { [portName]: dbPort } as Record<PortName, DbPort<TSchema>>,
|
|
752
|
+
async stop() {
|
|
753
|
+
if (!closeOnStop) return;
|
|
754
|
+
try {
|
|
755
|
+
await pool.end();
|
|
756
|
+
} catch (error) {
|
|
757
|
+
console.error(
|
|
758
|
+
"[drizzlePostgresProvider] Error closing database connection:",
|
|
759
|
+
error,
|
|
760
|
+
);
|
|
761
|
+
}
|
|
762
|
+
},
|
|
763
|
+
};
|
|
764
|
+
};
|
|
765
|
+
|
|
766
|
+
if (options.client) {
|
|
767
|
+
return createProvider<
|
|
768
|
+
unknown,
|
|
769
|
+
AnyProviderConfigSchema<void>,
|
|
770
|
+
Record<PortName, DbPort<TSchema>>
|
|
771
|
+
>({
|
|
772
|
+
name: "drizzle-postgres",
|
|
773
|
+
setup({ ports }) {
|
|
774
|
+
return setupPool(ports, options.client, options.closeOnStop ?? false);
|
|
775
|
+
},
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
|
|
669
779
|
return createProvider({
|
|
670
780
|
name: "drizzle-postgres",
|
|
671
781
|
|
|
@@ -687,40 +797,7 @@ export function createDrizzlePostgresProvider<
|
|
|
687
797
|
...options.pool,
|
|
688
798
|
});
|
|
689
799
|
|
|
690
|
-
|
|
691
|
-
providerName: "drizzle-postgres",
|
|
692
|
-
watcher: "db",
|
|
693
|
-
});
|
|
694
|
-
|
|
695
|
-
const logger = createDbQueryLogger(instrumentation, portName);
|
|
696
|
-
|
|
697
|
-
// 2. Create typed Drizzle instance
|
|
698
|
-
const db: NodePgDatabase<TSchema> = logger
|
|
699
|
-
? drizzle(pool, { schema, logger })
|
|
700
|
-
: drizzle(pool, { schema });
|
|
701
|
-
|
|
702
|
-
// 3. Build a typed DbPort
|
|
703
|
-
const dbPort: DbPort<TSchema> = {
|
|
704
|
-
drizzle: db,
|
|
705
|
-
pool,
|
|
706
|
-
checkHealth: () => checkPostgresHealth(pool),
|
|
707
|
-
};
|
|
708
|
-
|
|
709
|
-
return {
|
|
710
|
-
ports: { [portName]: dbPort } as Record<PortName, DbPort<TSchema>>,
|
|
711
|
-
async stop() {
|
|
712
|
-
// Gracefully close the connection pool
|
|
713
|
-
try {
|
|
714
|
-
await dbPort.pool.end();
|
|
715
|
-
} catch (error) {
|
|
716
|
-
// Log error but don't throw - we're shutting down anyway
|
|
717
|
-
console.error(
|
|
718
|
-
"[drizzlePostgresProvider] Error closing database connection:",
|
|
719
|
-
error,
|
|
720
|
-
);
|
|
721
|
-
}
|
|
722
|
-
},
|
|
723
|
-
};
|
|
800
|
+
return setupPool(ports, pool, true);
|
|
724
801
|
},
|
|
725
802
|
});
|
|
726
803
|
}
|
package/src/postgres/schema.ts
CHANGED
|
@@ -43,6 +43,7 @@ export const outboxMessages = pgTable(
|
|
|
43
43
|
kind: text("kind").notNull(),
|
|
44
44
|
name: text("name").notNull(),
|
|
45
45
|
payloadJson: text("payload_json").notNull(),
|
|
46
|
+
traceContextJson: text("trace_context_json"),
|
|
46
47
|
status: text("status").notNull(),
|
|
47
48
|
attempts: integer("attempts").notNull().default(0),
|
|
48
49
|
maxAttempts: integer("max_attempts").notNull().default(3),
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
import {
|
|
15
15
|
createIdempotencyStorageKey,
|
|
16
|
+
DEFAULT_IDEMPOTENCY_RESERVATION_TTL_SEC,
|
|
16
17
|
type IdempotencyCompleteInput,
|
|
17
18
|
type IdempotencyFailInput,
|
|
18
19
|
type IdempotencyPort,
|
|
@@ -57,6 +58,8 @@ export interface IdempotencyPortCoreOptions {
|
|
|
57
58
|
* Clock used by tests and deterministic environments.
|
|
58
59
|
*/
|
|
59
60
|
now?: () => Date;
|
|
61
|
+
/** Token factory used for deterministic tests. */
|
|
62
|
+
createReservationToken?: () => string;
|
|
60
63
|
}
|
|
61
64
|
|
|
62
65
|
/**
|
|
@@ -84,7 +87,7 @@ export type IdempotencyMutationErrorConstructor = new (
|
|
|
84
87
|
export function formatIdempotencyMutationErrorMessage(
|
|
85
88
|
args: IdempotencyMutationErrorArgs,
|
|
86
89
|
): string {
|
|
87
|
-
return `Idempotency ${args.action} for key "${args.key}" in namespace "${args.namespace}" matched no in-progress reservation with this fingerprint. The reservation may be missing, expired, already completed or released, or
|
|
90
|
+
return `Idempotency ${args.action} for key "${args.key}" in namespace "${args.namespace}" matched no in-progress reservation with this fingerprint and reservation token. The reservation may be missing, expired, already completed or released, or owned by a different execution.`;
|
|
88
91
|
}
|
|
89
92
|
|
|
90
93
|
function nowFrom(options: IdempotencyPortCoreOptions): Date {
|
|
@@ -117,6 +120,7 @@ function validateReserveInput(input: IdempotencyReserveInput): void {
|
|
|
117
120
|
assertNonEmptyString("key", input.key);
|
|
118
121
|
assertNonEmptyString("fingerprint", input.fingerprint);
|
|
119
122
|
assertIdempotencyTtl(input.ttlSec);
|
|
123
|
+
assertIdempotencyTtl(input.reservationTtlSec);
|
|
120
124
|
}
|
|
121
125
|
|
|
122
126
|
function validateMutationInput(
|
|
@@ -125,6 +129,65 @@ function validateMutationInput(
|
|
|
125
129
|
assertNonEmptyString("namespace", input.namespace);
|
|
126
130
|
assertNonEmptyString("key", input.key);
|
|
127
131
|
assertNonEmptyString("fingerprint", input.fingerprint);
|
|
132
|
+
assertNonEmptyString("reservationToken", input.reservationToken);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
type StoredReservationState = {
|
|
136
|
+
reservationToken: string;
|
|
137
|
+
replayTtlSec: number | null;
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
function encodeReservationState(
|
|
141
|
+
reservationToken: string,
|
|
142
|
+
replayTtlSec: number | undefined,
|
|
143
|
+
): string {
|
|
144
|
+
return JSON.stringify({
|
|
145
|
+
reservationToken,
|
|
146
|
+
replayTtlSec: replayTtlSec ?? null,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function decodeReservationState(
|
|
151
|
+
value: string | null,
|
|
152
|
+
): StoredReservationState | undefined {
|
|
153
|
+
if (value === null) return undefined;
|
|
154
|
+
try {
|
|
155
|
+
const parsed = JSON.parse(value) as {
|
|
156
|
+
reservationToken?: unknown;
|
|
157
|
+
replayTtlSec?: unknown;
|
|
158
|
+
};
|
|
159
|
+
if (
|
|
160
|
+
typeof parsed.reservationToken !== "string" ||
|
|
161
|
+
(parsed.replayTtlSec !== null &&
|
|
162
|
+
(!Number.isInteger(parsed.replayTtlSec) ||
|
|
163
|
+
Number(parsed.replayTtlSec) <= 0))
|
|
164
|
+
) {
|
|
165
|
+
return undefined;
|
|
166
|
+
}
|
|
167
|
+
return {
|
|
168
|
+
reservationToken: parsed.reservationToken,
|
|
169
|
+
replayTtlSec:
|
|
170
|
+
parsed.replayTtlSec === null ? null : Number(parsed.replayTtlSec),
|
|
171
|
+
};
|
|
172
|
+
} catch {
|
|
173
|
+
return undefined;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function createRandomReservationToken(): string {
|
|
178
|
+
if (typeof globalThis.crypto?.randomUUID === "function") {
|
|
179
|
+
return globalThis.crypto.randomUUID();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const bytes = new Uint8Array(16);
|
|
183
|
+
globalThis.crypto?.getRandomValues?.(bytes);
|
|
184
|
+
if (bytes.some((value) => value !== 0)) {
|
|
185
|
+
return Array.from(bytes, (value) =>
|
|
186
|
+
value.toString(16).padStart(2, "0"),
|
|
187
|
+
).join("");
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
|
128
191
|
}
|
|
129
192
|
|
|
130
193
|
/**
|
|
@@ -172,7 +235,15 @@ export function createIdempotencyPortCore(
|
|
|
172
235
|
|
|
173
236
|
const now = nowFrom(options);
|
|
174
237
|
const nowIso = encodeTimestamp(now);
|
|
175
|
-
const expiresAt = resolveIdempotencyExpiresAt(
|
|
238
|
+
const expiresAt = resolveIdempotencyExpiresAt(
|
|
239
|
+
input.reservationTtlSec ?? DEFAULT_IDEMPOTENCY_RESERVATION_TTL_SEC,
|
|
240
|
+
now,
|
|
241
|
+
);
|
|
242
|
+
const reservationToken = encodeReservationState(
|
|
243
|
+
(options.createReservationToken ?? createRandomReservationToken)(),
|
|
244
|
+
input.ttlSec,
|
|
245
|
+
);
|
|
246
|
+
const reservationJson = reservationToken;
|
|
176
247
|
const storageKey = createIdempotencyStorageKey(input);
|
|
177
248
|
const scopeKey = normalizeIdempotencyScope(input.scope);
|
|
178
249
|
const reserved = (): IdempotencyReservation => ({
|
|
@@ -181,6 +252,7 @@ export function createIdempotencyPortCore(
|
|
|
181
252
|
key: input.key,
|
|
182
253
|
scopeKey,
|
|
183
254
|
fingerprint: input.fingerprint,
|
|
255
|
+
reservationToken,
|
|
184
256
|
reservedAt: now,
|
|
185
257
|
expiresAt,
|
|
186
258
|
});
|
|
@@ -204,7 +276,7 @@ export function createIdempotencyPortCore(
|
|
|
204
276
|
${scopeKey},
|
|
205
277
|
${input.fingerprint},
|
|
206
278
|
'in-progress',
|
|
207
|
-
|
|
279
|
+
${reservationJson},
|
|
208
280
|
${nowIso},
|
|
209
281
|
null,
|
|
210
282
|
${expiresAt ? encodeTimestamp(expiresAt) : null},
|
|
@@ -225,7 +297,7 @@ export function createIdempotencyPortCore(
|
|
|
225
297
|
scope_key = ${scopeKey},
|
|
226
298
|
fingerprint = ${input.fingerprint},
|
|
227
299
|
status = 'in-progress',
|
|
228
|
-
result_json =
|
|
300
|
+
result_json = ${reservationJson},
|
|
229
301
|
reserved_at = ${nowIso},
|
|
230
302
|
completed_at = null,
|
|
231
303
|
expires_at = ${expiresAt ? encodeTimestamp(expiresAt) : null},
|
|
@@ -265,8 +337,14 @@ export function createIdempotencyPortCore(
|
|
|
265
337
|
async complete(input) {
|
|
266
338
|
validateMutationInput(input);
|
|
267
339
|
|
|
268
|
-
const
|
|
340
|
+
const completedAt = nowFrom(options);
|
|
341
|
+
const completedAtIso = encodeTimestamp(completedAt);
|
|
269
342
|
const storageKey = createIdempotencyStorageKey(input);
|
|
343
|
+
const reservationState = decodeReservationState(input.reservationToken);
|
|
344
|
+
const expiresAt = resolveIdempotencyExpiresAt(
|
|
345
|
+
reservationState?.replayTtlSec ?? undefined,
|
|
346
|
+
completedAt,
|
|
347
|
+
);
|
|
270
348
|
const serializedResult = JSON.stringify(input.result);
|
|
271
349
|
const resultJson =
|
|
272
350
|
serializedResult === undefined ? null : serializedResult;
|
|
@@ -275,10 +353,12 @@ export function createIdempotencyPortCore(
|
|
|
275
353
|
set
|
|
276
354
|
status = 'completed',
|
|
277
355
|
result_json = ${resultJson},
|
|
278
|
-
completed_at = ${
|
|
279
|
-
|
|
356
|
+
completed_at = ${completedAtIso},
|
|
357
|
+
expires_at = ${expiresAt ? encodeTimestamp(expiresAt) : null},
|
|
358
|
+
updated_at = ${completedAtIso}
|
|
280
359
|
where storage_key = ${storageKey}
|
|
281
360
|
and fingerprint = ${input.fingerprint}
|
|
361
|
+
and result_json = ${input.reservationToken}
|
|
282
362
|
and status = 'in-progress'`);
|
|
283
363
|
|
|
284
364
|
assertIdempotencyMutationSucceeded("complete", input, rowsAffected);
|
|
@@ -291,6 +371,7 @@ export function createIdempotencyPortCore(
|
|
|
291
371
|
const rowsAffected = await executor.run(sql`delete from ${executor.table}
|
|
292
372
|
where storage_key = ${storageKey}
|
|
293
373
|
and fingerprint = ${input.fingerprint}
|
|
374
|
+
and result_json = ${input.reservationToken}
|
|
294
375
|
and status = 'in-progress'`);
|
|
295
376
|
|
|
296
377
|
assertIdempotencyMutationSucceeded("fail", input, rowsAffected);
|
|
@@ -362,6 +362,7 @@ export function createOutboxPortCore(
|
|
|
362
362
|
kind,
|
|
363
363
|
name,
|
|
364
364
|
payload_json,
|
|
365
|
+
trace_context_json,
|
|
365
366
|
status,
|
|
366
367
|
attempts,
|
|
367
368
|
max_attempts,
|
|
@@ -378,6 +379,7 @@ export function createOutboxPortCore(
|
|
|
378
379
|
${message.kind},
|
|
379
380
|
${message.name},
|
|
380
381
|
${JSON.stringify(message.payload)},
|
|
382
|
+
${message.trace ? JSON.stringify(message.trace) : null},
|
|
381
383
|
${message.status},
|
|
382
384
|
${message.attempts},
|
|
383
385
|
${message.maxAttempts},
|