ff-effect 0.0.7 → 0.0.10
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/dist/for/drizzle/index.cjs +129 -0
- package/dist/for/drizzle/index.cjs.map +1 -0
- package/dist/for/drizzle/index.d.cts +40 -0
- package/dist/for/drizzle/index.d.ts +40 -0
- package/dist/for/drizzle/index.js +85 -0
- package/dist/for/drizzle/index.js.map +1 -0
- package/package.json +14 -2
- package/src/for/drizzle/index.test.ts +209 -0
- package/src/for/drizzle/index.ts +114 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/for/drizzle/index.ts
|
|
31
|
+
var drizzle_exports = {};
|
|
32
|
+
__export(drizzle_exports, {
|
|
33
|
+
ChannelTypeId: () => ChannelTypeId2,
|
|
34
|
+
DrizzleError: () => DrizzleError,
|
|
35
|
+
EffectTypeId: () => EffectTypeId,
|
|
36
|
+
NodeInspectSymbol: () => NodeInspectSymbol2,
|
|
37
|
+
STMTypeId: () => STMTypeId2,
|
|
38
|
+
SinkTypeId: () => SinkTypeId2,
|
|
39
|
+
StreamTypeId: () => StreamTypeId2,
|
|
40
|
+
TagTypeId: () => TagTypeId2,
|
|
41
|
+
Unify: () => Unify,
|
|
42
|
+
createDatabase: () => createDatabase
|
|
43
|
+
});
|
|
44
|
+
module.exports = __toCommonJS(drizzle_exports);
|
|
45
|
+
var import_effect = require("effect");
|
|
46
|
+
var Channel = __toESM(require("effect/Channel"), 1);
|
|
47
|
+
var Context = __toESM(require("effect/Context"), 1);
|
|
48
|
+
var Inspectable = __toESM(require("effect/Inspectable"), 1);
|
|
49
|
+
var Sink = __toESM(require("effect/Sink"), 1);
|
|
50
|
+
var STM = __toESM(require("effect/STM"), 1);
|
|
51
|
+
var Stream = __toESM(require("effect/Stream"), 1);
|
|
52
|
+
var EUnify = __toESM(require("effect/Unify"), 1);
|
|
53
|
+
var TagTypeId2 = Context.TagTypeId;
|
|
54
|
+
var ChannelTypeId2 = Channel.ChannelTypeId;
|
|
55
|
+
var EffectTypeId = import_effect.Effect.EffectTypeId;
|
|
56
|
+
var NodeInspectSymbol2 = Inspectable.NodeInspectSymbol;
|
|
57
|
+
var STMTypeId2 = STM.STMTypeId;
|
|
58
|
+
var SinkTypeId2 = Sink.SinkTypeId;
|
|
59
|
+
var StreamTypeId2 = Stream.StreamTypeId;
|
|
60
|
+
var Unify = EUnify;
|
|
61
|
+
var DrizzleError = class extends import_effect.Data.TaggedError("ff-effect/DrizzleError") {
|
|
62
|
+
};
|
|
63
|
+
var WrappedTxError = class extends Error {
|
|
64
|
+
};
|
|
65
|
+
var defaultPrefix = "@ff-effect/Drizzle";
|
|
66
|
+
function createDatabase(createClient, opts) {
|
|
67
|
+
const tagId = opts?.tagId ?? defaultPrefix;
|
|
68
|
+
const Drizzle = Context.Tag(tagId)();
|
|
69
|
+
const drizzleTxTagId = `${tagId}.tx`;
|
|
70
|
+
const DrizzleTx = Context.Tag(drizzleTxTagId)();
|
|
71
|
+
const db = (fn) => import_effect.Effect.gen(function* () {
|
|
72
|
+
const client = yield* Drizzle;
|
|
73
|
+
return yield* import_effect.Effect.tryPromise({
|
|
74
|
+
try: () => fn(client),
|
|
75
|
+
catch: (cause) => new DrizzleError({ message: "Database operation failed", cause })
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
const tx = (fn) => import_effect.Effect.gen(function* () {
|
|
79
|
+
const client = yield* DrizzleTx;
|
|
80
|
+
return yield* import_effect.Effect.tryPromise({
|
|
81
|
+
try: () => fn(client),
|
|
82
|
+
catch: (cause) => new DrizzleError({ message: "Database operation failed", cause })
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
const withTransaction = (effect) => import_effect.Effect.gen(function* () {
|
|
86
|
+
const client = yield* Drizzle;
|
|
87
|
+
const runFork = yield* import_effect.FiberSet.makeRuntimePromise();
|
|
88
|
+
return yield* import_effect.Effect.tryPromise({
|
|
89
|
+
try: () => client.transaction(
|
|
90
|
+
(txClient) => runFork(
|
|
91
|
+
effect.pipe(
|
|
92
|
+
import_effect.Effect.provideService(Drizzle, txClient),
|
|
93
|
+
import_effect.Effect.provideService(DrizzleTx, txClient),
|
|
94
|
+
import_effect.Effect.mapError((e) => new WrappedTxError("", { cause: e }))
|
|
95
|
+
)
|
|
96
|
+
)
|
|
97
|
+
),
|
|
98
|
+
catch: (error) => {
|
|
99
|
+
if (error instanceof WrappedTxError) return error.cause;
|
|
100
|
+
return new DrizzleError({
|
|
101
|
+
message: "Transaction failed",
|
|
102
|
+
cause: error
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
}).pipe(import_effect.Effect.scoped);
|
|
107
|
+
return {
|
|
108
|
+
db,
|
|
109
|
+
tx,
|
|
110
|
+
Drizzle,
|
|
111
|
+
DrizzleTx,
|
|
112
|
+
withTransaction,
|
|
113
|
+
layer: import_effect.Layer.effect(Drizzle, createClient)
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
117
|
+
0 && (module.exports = {
|
|
118
|
+
ChannelTypeId,
|
|
119
|
+
DrizzleError,
|
|
120
|
+
EffectTypeId,
|
|
121
|
+
NodeInspectSymbol,
|
|
122
|
+
STMTypeId,
|
|
123
|
+
SinkTypeId,
|
|
124
|
+
StreamTypeId,
|
|
125
|
+
TagTypeId,
|
|
126
|
+
Unify,
|
|
127
|
+
createDatabase
|
|
128
|
+
});
|
|
129
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/for/drizzle/index.ts"],"sourcesContent":["import { Data, Effect, FiberSet, Layer } from 'effect';\nimport * as Channel from 'effect/Channel';\nimport * as Context from 'effect/Context';\nimport * as Inspectable from 'effect/Inspectable';\nimport * as Sink from 'effect/Sink';\nimport * as STM from 'effect/STM';\nimport * as Stream from 'effect/Stream';\nimport * as EUnify from 'effect/Unify';\n\n// TypeScript issue where the return type of createDatabase\n// contains internal Effect types (TagTypeId) that aren't exported\n// from this module,\n// so TypeScript can't \"name\" them in the declaration file.\nexport const TagTypeId = Context.TagTypeId;\nexport const ChannelTypeId = Channel.ChannelTypeId;\nexport const EffectTypeId = Effect.EffectTypeId;\nexport const NodeInspectSymbol = Inspectable.NodeInspectSymbol;\nexport const STMTypeId = STM.STMTypeId;\nexport const SinkTypeId = Sink.SinkTypeId;\nexport const StreamTypeId = Stream.StreamTypeId;\nexport const Unify = EUnify;\n\nexport class DrizzleError extends Data.TaggedError('ff-effect/DrizzleError')<{\n\tmessage: string;\n\tcause?: unknown;\n}> {}\n\ntype AnyDrizzleClient = {\n\ttransaction: (fn: (tx: any) => Promise<any>) => Promise<any>;\n};\n\ntype TxClient<TClient extends AnyDrizzleClient> = Parameters<\n\tParameters<TClient['transaction']>[0]\n>[0];\n\nclass WrappedTxError extends Error {}\n\nconst defaultPrefix = '@ff-effect/Drizzle' as const;\n\nexport function createDatabase<\n\tTClient extends AnyDrizzleClient,\n\tE,\n\tR,\n\tT extends string = typeof defaultPrefix,\n>(createClient: Effect.Effect<TClient, E, R>, opts?: { tagId?: T }) {\n\ttype Client = TClient | TxClient<TClient>;\n\ttype Tx = TxClient<TClient>;\n\n\tconst tagId = (opts?.tagId ?? defaultPrefix) as T;\n\n\ttype Drizzle = typeof tagId;\n\tconst Drizzle = Context.Tag(tagId)<Drizzle, Client>();\n\n\tconst drizzleTxTagId = `${tagId}.tx` as const;\n\ttype DrizzleTx = typeof drizzleTxTagId;\n\tconst DrizzleTx = Context.Tag(drizzleTxTagId)<DrizzleTx, Tx>();\n\n\tconst db = <T>(fn: (client: Client) => Promise<T>) =>\n\t\tEffect.gen(function* () {\n\t\t\tconst client = yield* Drizzle;\n\t\t\treturn yield* Effect.tryPromise({\n\t\t\t\ttry: () => fn(client),\n\t\t\t\tcatch: (cause) =>\n\t\t\t\t\tnew DrizzleError({ message: 'Database operation failed', cause }),\n\t\t\t});\n\t\t});\n\n\t/** Requires being inside withTransaction - enforces transaction at compile time */\n\tconst tx = <T>(fn: (client: Tx) => Promise<T>) =>\n\t\tEffect.gen(function* () {\n\t\t\tconst client = yield* DrizzleTx;\n\t\t\treturn yield* Effect.tryPromise({\n\t\t\t\ttry: () => fn(client),\n\t\t\t\tcatch: (cause) =>\n\t\t\t\t\tnew DrizzleError({ message: 'Database operation failed', cause }),\n\t\t\t});\n\t\t});\n\n\tconst withTransaction = <A, E, R>(effect: Effect.Effect<A, E, R>) =>\n\t\tEffect.gen(function* () {\n\t\t\tconst client = yield* Drizzle;\n\t\t\tconst runFork =\n\t\t\t\tyield* FiberSet.makeRuntimePromise<Exclude<R, DrizzleTx>>();\n\n\t\t\treturn yield* Effect.tryPromise<A, E | DrizzleError>({\n\t\t\t\ttry: () =>\n\t\t\t\t\t(client as TClient).transaction((txClient) =>\n\t\t\t\t\t\trunFork(\n\t\t\t\t\t\t\teffect.pipe(\n\t\t\t\t\t\t\t\tEffect.provideService(Drizzle, txClient as Client),\n\t\t\t\t\t\t\t\tEffect.provideService(DrizzleTx, txClient as Tx),\n\t\t\t\t\t\t\t\tEffect.mapError((e) => new WrappedTxError('', { cause: e })),\n\t\t\t\t\t\t\t) as Effect.Effect<A, WrappedTxError, Exclude<R, DrizzleTx>>,\n\t\t\t\t\t\t),\n\t\t\t\t\t),\n\t\t\t\tcatch: (error) => {\n\t\t\t\t\tif (error instanceof WrappedTxError) return error.cause as E;\n\t\t\t\t\treturn new DrizzleError({\n\t\t\t\t\t\tmessage: 'Transaction failed',\n\t\t\t\t\t\tcause: error,\n\t\t\t\t\t});\n\t\t\t\t},\n\t\t\t});\n\t\t}).pipe(Effect.scoped);\n\n\treturn {\n\t\tdb,\n\t\ttx,\n\t\tDrizzle,\n\t\tDrizzleTx,\n\t\twithTransaction,\n\t\tlayer: Layer.effect(Drizzle, createClient),\n\t};\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA,uBAAAA;AAAA,EAAA;AAAA;AAAA,2BAAAC;AAAA,EAAA,iBAAAC;AAAA,EAAA,kBAAAC;AAAA,EAAA,oBAAAC;AAAA,EAAA,iBAAAC;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,oBAA8C;AAC9C,cAAyB;AACzB,cAAyB;AACzB,kBAA6B;AAC7B,WAAsB;AACtB,UAAqB;AACrB,aAAwB;AACxB,aAAwB;AAMjB,IAAMA,aAAoB;AAC1B,IAAML,iBAAwB;AAC9B,IAAM,eAAe,qBAAO;AAC5B,IAAMC,qBAAgC;AACtC,IAAMC,aAAgB;AACtB,IAAMC,cAAkB;AACxB,IAAMC,gBAAsB;AAC5B,IAAM,QAAQ;AAEd,IAAM,eAAN,cAA2B,mBAAK,YAAY,wBAAwB,EAGxE;AAAC;AAUJ,IAAM,iBAAN,cAA6B,MAAM;AAAC;AAEpC,IAAM,gBAAgB;AAEf,SAAS,eAKd,cAA4C,MAAsB;AAInE,QAAM,QAAS,MAAM,SAAS;AAG9B,QAAM,UAAkB,YAAI,KAAK,EAAmB;AAEpD,QAAM,iBAAiB,GAAG,KAAK;AAE/B,QAAM,YAAoB,YAAI,cAAc,EAAiB;AAE7D,QAAM,KAAK,CAAI,OACd,qBAAO,IAAI,aAAa;AACvB,UAAM,SAAS,OAAO;AACtB,WAAO,OAAO,qBAAO,WAAW;AAAA,MAC/B,KAAK,MAAM,GAAG,MAAM;AAAA,MACpB,OAAO,CAAC,UACP,IAAI,aAAa,EAAE,SAAS,6BAA6B,MAAM,CAAC;AAAA,IAClE,CAAC;AAAA,EACF,CAAC;AAGF,QAAM,KAAK,CAAI,OACd,qBAAO,IAAI,aAAa;AACvB,UAAM,SAAS,OAAO;AACtB,WAAO,OAAO,qBAAO,WAAW;AAAA,MAC/B,KAAK,MAAM,GAAG,MAAM;AAAA,MACpB,OAAO,CAAC,UACP,IAAI,aAAa,EAAE,SAAS,6BAA6B,MAAM,CAAC;AAAA,IAClE,CAAC;AAAA,EACF,CAAC;AAEF,QAAM,kBAAkB,CAAU,WACjC,qBAAO,IAAI,aAAa;AACvB,UAAM,SAAS,OAAO;AACtB,UAAM,UACL,OAAO,uBAAS,mBAA0C;AAE3D,WAAO,OAAO,qBAAO,WAAgC;AAAA,MACpD,KAAK,MACH,OAAmB;AAAA,QAAY,CAAC,aAChC;AAAA,UACC,OAAO;AAAA,YACN,qBAAO,eAAe,SAAS,QAAkB;AAAA,YACjD,qBAAO,eAAe,WAAW,QAAc;AAAA,YAC/C,qBAAO,SAAS,CAAC,MAAM,IAAI,eAAe,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC;AAAA,UAC5D;AAAA,QACD;AAAA,MACD;AAAA,MACD,OAAO,CAAC,UAAU;AACjB,YAAI,iBAAiB,eAAgB,QAAO,MAAM;AAClD,eAAO,IAAI,aAAa;AAAA,UACvB,SAAS;AAAA,UACT,OAAO;AAAA,QACR,CAAC;AAAA,MACF;AAAA,IACD,CAAC;AAAA,EACF,CAAC,EAAE,KAAK,qBAAO,MAAM;AAEtB,SAAO;AAAA,IACN;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,OAAO,oBAAM,OAAO,SAAS,YAAY;AAAA,EAC1C;AACD;","names":["ChannelTypeId","NodeInspectSymbol","STMTypeId","SinkTypeId","StreamTypeId","TagTypeId"]}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import * as effect_Scope from 'effect/Scope';
|
|
2
|
+
import * as effect_Cause from 'effect/Cause';
|
|
3
|
+
import * as effect_Types from 'effect/Types';
|
|
4
|
+
import { Effect, Layer } from 'effect';
|
|
5
|
+
import * as Context from 'effect/Context';
|
|
6
|
+
import * as EUnify from 'effect/Unify';
|
|
7
|
+
|
|
8
|
+
declare const TagTypeId: symbol;
|
|
9
|
+
declare const ChannelTypeId: symbol;
|
|
10
|
+
declare const EffectTypeId: symbol;
|
|
11
|
+
declare const NodeInspectSymbol: symbol;
|
|
12
|
+
declare const STMTypeId: symbol;
|
|
13
|
+
declare const SinkTypeId: symbol;
|
|
14
|
+
declare const StreamTypeId: symbol;
|
|
15
|
+
declare const Unify: typeof EUnify;
|
|
16
|
+
declare const DrizzleError_base: new <A extends Record<string, any> = {}>(args: effect_Types.Equals<A, {}> extends true ? void : { readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }) => effect_Cause.YieldableError & {
|
|
17
|
+
readonly _tag: "ff-effect/DrizzleError";
|
|
18
|
+
} & Readonly<A>;
|
|
19
|
+
declare class DrizzleError extends DrizzleError_base<{
|
|
20
|
+
message: string;
|
|
21
|
+
cause?: unknown;
|
|
22
|
+
}> {
|
|
23
|
+
}
|
|
24
|
+
type AnyDrizzleClient = {
|
|
25
|
+
transaction: (fn: (tx: any) => Promise<any>) => Promise<any>;
|
|
26
|
+
};
|
|
27
|
+
type TxClient<TClient extends AnyDrizzleClient> = Parameters<Parameters<TClient['transaction']>[0]>[0];
|
|
28
|
+
declare const defaultPrefix: "@ff-effect/Drizzle";
|
|
29
|
+
declare function createDatabase<TClient extends AnyDrizzleClient, E, R, T extends string = typeof defaultPrefix>(createClient: Effect.Effect<TClient, E, R>, opts?: {
|
|
30
|
+
tagId?: T;
|
|
31
|
+
}): {
|
|
32
|
+
db: <T_1>(fn: (client: TClient | TxClient<TClient>) => Promise<T_1>) => Effect.Effect<T_1, DrizzleError, T>;
|
|
33
|
+
tx: <T_1>(fn: (client: TxClient<TClient>) => Promise<T_1>) => Effect.Effect<T_1, DrizzleError, `${T}.tx`>;
|
|
34
|
+
Drizzle: Context.TagClass<T, T, TClient | TxClient<TClient>>;
|
|
35
|
+
DrizzleTx: Context.TagClass<`${T}.tx`, `${T}.tx`, TxClient<TClient>>;
|
|
36
|
+
withTransaction: <A, E_1, R_1>(effect: Effect.Effect<A, E_1, R_1>) => Effect.Effect<A, DrizzleError | E_1, Exclude<T, effect_Scope.Scope> | Exclude<Exclude<R_1, `${T}.tx`>, effect_Scope.Scope>>;
|
|
37
|
+
layer: Layer.Layer<T, E, R>;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export { ChannelTypeId, DrizzleError, EffectTypeId, NodeInspectSymbol, STMTypeId, SinkTypeId, StreamTypeId, TagTypeId, Unify, createDatabase };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import * as effect_Scope from 'effect/Scope';
|
|
2
|
+
import * as effect_Cause from 'effect/Cause';
|
|
3
|
+
import * as effect_Types from 'effect/Types';
|
|
4
|
+
import { Effect, Layer } from 'effect';
|
|
5
|
+
import * as Context from 'effect/Context';
|
|
6
|
+
import * as EUnify from 'effect/Unify';
|
|
7
|
+
|
|
8
|
+
declare const TagTypeId: symbol;
|
|
9
|
+
declare const ChannelTypeId: symbol;
|
|
10
|
+
declare const EffectTypeId: symbol;
|
|
11
|
+
declare const NodeInspectSymbol: symbol;
|
|
12
|
+
declare const STMTypeId: symbol;
|
|
13
|
+
declare const SinkTypeId: symbol;
|
|
14
|
+
declare const StreamTypeId: symbol;
|
|
15
|
+
declare const Unify: typeof EUnify;
|
|
16
|
+
declare const DrizzleError_base: new <A extends Record<string, any> = {}>(args: effect_Types.Equals<A, {}> extends true ? void : { readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }) => effect_Cause.YieldableError & {
|
|
17
|
+
readonly _tag: "ff-effect/DrizzleError";
|
|
18
|
+
} & Readonly<A>;
|
|
19
|
+
declare class DrizzleError extends DrizzleError_base<{
|
|
20
|
+
message: string;
|
|
21
|
+
cause?: unknown;
|
|
22
|
+
}> {
|
|
23
|
+
}
|
|
24
|
+
type AnyDrizzleClient = {
|
|
25
|
+
transaction: (fn: (tx: any) => Promise<any>) => Promise<any>;
|
|
26
|
+
};
|
|
27
|
+
type TxClient<TClient extends AnyDrizzleClient> = Parameters<Parameters<TClient['transaction']>[0]>[0];
|
|
28
|
+
declare const defaultPrefix: "@ff-effect/Drizzle";
|
|
29
|
+
declare function createDatabase<TClient extends AnyDrizzleClient, E, R, T extends string = typeof defaultPrefix>(createClient: Effect.Effect<TClient, E, R>, opts?: {
|
|
30
|
+
tagId?: T;
|
|
31
|
+
}): {
|
|
32
|
+
db: <T_1>(fn: (client: TClient | TxClient<TClient>) => Promise<T_1>) => Effect.Effect<T_1, DrizzleError, T>;
|
|
33
|
+
tx: <T_1>(fn: (client: TxClient<TClient>) => Promise<T_1>) => Effect.Effect<T_1, DrizzleError, `${T}.tx`>;
|
|
34
|
+
Drizzle: Context.TagClass<T, T, TClient | TxClient<TClient>>;
|
|
35
|
+
DrizzleTx: Context.TagClass<`${T}.tx`, `${T}.tx`, TxClient<TClient>>;
|
|
36
|
+
withTransaction: <A, E_1, R_1>(effect: Effect.Effect<A, E_1, R_1>) => Effect.Effect<A, DrizzleError | E_1, Exclude<T, effect_Scope.Scope> | Exclude<Exclude<R_1, `${T}.tx`>, effect_Scope.Scope>>;
|
|
37
|
+
layer: Layer.Layer<T, E, R>;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export { ChannelTypeId, DrizzleError, EffectTypeId, NodeInspectSymbol, STMTypeId, SinkTypeId, StreamTypeId, TagTypeId, Unify, createDatabase };
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// src/for/drizzle/index.ts
|
|
2
|
+
import { Data, Effect, FiberSet, Layer } from "effect";
|
|
3
|
+
import * as Channel from "effect/Channel";
|
|
4
|
+
import * as Context from "effect/Context";
|
|
5
|
+
import * as Inspectable from "effect/Inspectable";
|
|
6
|
+
import * as Sink from "effect/Sink";
|
|
7
|
+
import * as STM from "effect/STM";
|
|
8
|
+
import * as Stream from "effect/Stream";
|
|
9
|
+
import * as EUnify from "effect/Unify";
|
|
10
|
+
var TagTypeId2 = Context.TagTypeId;
|
|
11
|
+
var ChannelTypeId2 = Channel.ChannelTypeId;
|
|
12
|
+
var EffectTypeId = Effect.EffectTypeId;
|
|
13
|
+
var NodeInspectSymbol2 = Inspectable.NodeInspectSymbol;
|
|
14
|
+
var STMTypeId2 = STM.STMTypeId;
|
|
15
|
+
var SinkTypeId2 = Sink.SinkTypeId;
|
|
16
|
+
var StreamTypeId2 = Stream.StreamTypeId;
|
|
17
|
+
var Unify = EUnify;
|
|
18
|
+
var DrizzleError = class extends Data.TaggedError("ff-effect/DrizzleError") {
|
|
19
|
+
};
|
|
20
|
+
var WrappedTxError = class extends Error {
|
|
21
|
+
};
|
|
22
|
+
var defaultPrefix = "@ff-effect/Drizzle";
|
|
23
|
+
function createDatabase(createClient, opts) {
|
|
24
|
+
const tagId = opts?.tagId ?? defaultPrefix;
|
|
25
|
+
const Drizzle = Context.Tag(tagId)();
|
|
26
|
+
const drizzleTxTagId = `${tagId}.tx`;
|
|
27
|
+
const DrizzleTx = Context.Tag(drizzleTxTagId)();
|
|
28
|
+
const db = (fn) => Effect.gen(function* () {
|
|
29
|
+
const client = yield* Drizzle;
|
|
30
|
+
return yield* Effect.tryPromise({
|
|
31
|
+
try: () => fn(client),
|
|
32
|
+
catch: (cause) => new DrizzleError({ message: "Database operation failed", cause })
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
const tx = (fn) => Effect.gen(function* () {
|
|
36
|
+
const client = yield* DrizzleTx;
|
|
37
|
+
return yield* Effect.tryPromise({
|
|
38
|
+
try: () => fn(client),
|
|
39
|
+
catch: (cause) => new DrizzleError({ message: "Database operation failed", cause })
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
const withTransaction = (effect) => Effect.gen(function* () {
|
|
43
|
+
const client = yield* Drizzle;
|
|
44
|
+
const runFork = yield* FiberSet.makeRuntimePromise();
|
|
45
|
+
return yield* Effect.tryPromise({
|
|
46
|
+
try: () => client.transaction(
|
|
47
|
+
(txClient) => runFork(
|
|
48
|
+
effect.pipe(
|
|
49
|
+
Effect.provideService(Drizzle, txClient),
|
|
50
|
+
Effect.provideService(DrizzleTx, txClient),
|
|
51
|
+
Effect.mapError((e) => new WrappedTxError("", { cause: e }))
|
|
52
|
+
)
|
|
53
|
+
)
|
|
54
|
+
),
|
|
55
|
+
catch: (error) => {
|
|
56
|
+
if (error instanceof WrappedTxError) return error.cause;
|
|
57
|
+
return new DrizzleError({
|
|
58
|
+
message: "Transaction failed",
|
|
59
|
+
cause: error
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
}).pipe(Effect.scoped);
|
|
64
|
+
return {
|
|
65
|
+
db,
|
|
66
|
+
tx,
|
|
67
|
+
Drizzle,
|
|
68
|
+
DrizzleTx,
|
|
69
|
+
withTransaction,
|
|
70
|
+
layer: Layer.effect(Drizzle, createClient)
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
export {
|
|
74
|
+
ChannelTypeId2 as ChannelTypeId,
|
|
75
|
+
DrizzleError,
|
|
76
|
+
EffectTypeId,
|
|
77
|
+
NodeInspectSymbol2 as NodeInspectSymbol,
|
|
78
|
+
STMTypeId2 as STMTypeId,
|
|
79
|
+
SinkTypeId2 as SinkTypeId,
|
|
80
|
+
StreamTypeId2 as StreamTypeId,
|
|
81
|
+
TagTypeId2 as TagTypeId,
|
|
82
|
+
Unify,
|
|
83
|
+
createDatabase
|
|
84
|
+
};
|
|
85
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/for/drizzle/index.ts"],"sourcesContent":["import { Data, Effect, FiberSet, Layer } from 'effect';\nimport * as Channel from 'effect/Channel';\nimport * as Context from 'effect/Context';\nimport * as Inspectable from 'effect/Inspectable';\nimport * as Sink from 'effect/Sink';\nimport * as STM from 'effect/STM';\nimport * as Stream from 'effect/Stream';\nimport * as EUnify from 'effect/Unify';\n\n// TypeScript issue where the return type of createDatabase\n// contains internal Effect types (TagTypeId) that aren't exported\n// from this module,\n// so TypeScript can't \"name\" them in the declaration file.\nexport const TagTypeId = Context.TagTypeId;\nexport const ChannelTypeId = Channel.ChannelTypeId;\nexport const EffectTypeId = Effect.EffectTypeId;\nexport const NodeInspectSymbol = Inspectable.NodeInspectSymbol;\nexport const STMTypeId = STM.STMTypeId;\nexport const SinkTypeId = Sink.SinkTypeId;\nexport const StreamTypeId = Stream.StreamTypeId;\nexport const Unify = EUnify;\n\nexport class DrizzleError extends Data.TaggedError('ff-effect/DrizzleError')<{\n\tmessage: string;\n\tcause?: unknown;\n}> {}\n\ntype AnyDrizzleClient = {\n\ttransaction: (fn: (tx: any) => Promise<any>) => Promise<any>;\n};\n\ntype TxClient<TClient extends AnyDrizzleClient> = Parameters<\n\tParameters<TClient['transaction']>[0]\n>[0];\n\nclass WrappedTxError extends Error {}\n\nconst defaultPrefix = '@ff-effect/Drizzle' as const;\n\nexport function createDatabase<\n\tTClient extends AnyDrizzleClient,\n\tE,\n\tR,\n\tT extends string = typeof defaultPrefix,\n>(createClient: Effect.Effect<TClient, E, R>, opts?: { tagId?: T }) {\n\ttype Client = TClient | TxClient<TClient>;\n\ttype Tx = TxClient<TClient>;\n\n\tconst tagId = (opts?.tagId ?? defaultPrefix) as T;\n\n\ttype Drizzle = typeof tagId;\n\tconst Drizzle = Context.Tag(tagId)<Drizzle, Client>();\n\n\tconst drizzleTxTagId = `${tagId}.tx` as const;\n\ttype DrizzleTx = typeof drizzleTxTagId;\n\tconst DrizzleTx = Context.Tag(drizzleTxTagId)<DrizzleTx, Tx>();\n\n\tconst db = <T>(fn: (client: Client) => Promise<T>) =>\n\t\tEffect.gen(function* () {\n\t\t\tconst client = yield* Drizzle;\n\t\t\treturn yield* Effect.tryPromise({\n\t\t\t\ttry: () => fn(client),\n\t\t\t\tcatch: (cause) =>\n\t\t\t\t\tnew DrizzleError({ message: 'Database operation failed', cause }),\n\t\t\t});\n\t\t});\n\n\t/** Requires being inside withTransaction - enforces transaction at compile time */\n\tconst tx = <T>(fn: (client: Tx) => Promise<T>) =>\n\t\tEffect.gen(function* () {\n\t\t\tconst client = yield* DrizzleTx;\n\t\t\treturn yield* Effect.tryPromise({\n\t\t\t\ttry: () => fn(client),\n\t\t\t\tcatch: (cause) =>\n\t\t\t\t\tnew DrizzleError({ message: 'Database operation failed', cause }),\n\t\t\t});\n\t\t});\n\n\tconst withTransaction = <A, E, R>(effect: Effect.Effect<A, E, R>) =>\n\t\tEffect.gen(function* () {\n\t\t\tconst client = yield* Drizzle;\n\t\t\tconst runFork =\n\t\t\t\tyield* FiberSet.makeRuntimePromise<Exclude<R, DrizzleTx>>();\n\n\t\t\treturn yield* Effect.tryPromise<A, E | DrizzleError>({\n\t\t\t\ttry: () =>\n\t\t\t\t\t(client as TClient).transaction((txClient) =>\n\t\t\t\t\t\trunFork(\n\t\t\t\t\t\t\teffect.pipe(\n\t\t\t\t\t\t\t\tEffect.provideService(Drizzle, txClient as Client),\n\t\t\t\t\t\t\t\tEffect.provideService(DrizzleTx, txClient as Tx),\n\t\t\t\t\t\t\t\tEffect.mapError((e) => new WrappedTxError('', { cause: e })),\n\t\t\t\t\t\t\t) as Effect.Effect<A, WrappedTxError, Exclude<R, DrizzleTx>>,\n\t\t\t\t\t\t),\n\t\t\t\t\t),\n\t\t\t\tcatch: (error) => {\n\t\t\t\t\tif (error instanceof WrappedTxError) return error.cause as E;\n\t\t\t\t\treturn new DrizzleError({\n\t\t\t\t\t\tmessage: 'Transaction failed',\n\t\t\t\t\t\tcause: error,\n\t\t\t\t\t});\n\t\t\t\t},\n\t\t\t});\n\t\t}).pipe(Effect.scoped);\n\n\treturn {\n\t\tdb,\n\t\ttx,\n\t\tDrizzle,\n\t\tDrizzleTx,\n\t\twithTransaction,\n\t\tlayer: Layer.effect(Drizzle, createClient),\n\t};\n}\n"],"mappings":";AAAA,SAAS,MAAM,QAAQ,UAAU,aAAa;AAC9C,YAAY,aAAa;AACzB,YAAY,aAAa;AACzB,YAAY,iBAAiB;AAC7B,YAAY,UAAU;AACtB,YAAY,SAAS;AACrB,YAAY,YAAY;AACxB,YAAY,YAAY;AAMjB,IAAMA,aAAoB;AAC1B,IAAMC,iBAAwB;AAC9B,IAAM,eAAe,OAAO;AAC5B,IAAMC,qBAAgC;AACtC,IAAMC,aAAgB;AACtB,IAAMC,cAAkB;AACxB,IAAMC,gBAAsB;AAC5B,IAAM,QAAQ;AAEd,IAAM,eAAN,cAA2B,KAAK,YAAY,wBAAwB,EAGxE;AAAC;AAUJ,IAAM,iBAAN,cAA6B,MAAM;AAAC;AAEpC,IAAM,gBAAgB;AAEf,SAAS,eAKd,cAA4C,MAAsB;AAInE,QAAM,QAAS,MAAM,SAAS;AAG9B,QAAM,UAAkB,YAAI,KAAK,EAAmB;AAEpD,QAAM,iBAAiB,GAAG,KAAK;AAE/B,QAAM,YAAoB,YAAI,cAAc,EAAiB;AAE7D,QAAM,KAAK,CAAI,OACd,OAAO,IAAI,aAAa;AACvB,UAAM,SAAS,OAAO;AACtB,WAAO,OAAO,OAAO,WAAW;AAAA,MAC/B,KAAK,MAAM,GAAG,MAAM;AAAA,MACpB,OAAO,CAAC,UACP,IAAI,aAAa,EAAE,SAAS,6BAA6B,MAAM,CAAC;AAAA,IAClE,CAAC;AAAA,EACF,CAAC;AAGF,QAAM,KAAK,CAAI,OACd,OAAO,IAAI,aAAa;AACvB,UAAM,SAAS,OAAO;AACtB,WAAO,OAAO,OAAO,WAAW;AAAA,MAC/B,KAAK,MAAM,GAAG,MAAM;AAAA,MACpB,OAAO,CAAC,UACP,IAAI,aAAa,EAAE,SAAS,6BAA6B,MAAM,CAAC;AAAA,IAClE,CAAC;AAAA,EACF,CAAC;AAEF,QAAM,kBAAkB,CAAU,WACjC,OAAO,IAAI,aAAa;AACvB,UAAM,SAAS,OAAO;AACtB,UAAM,UACL,OAAO,SAAS,mBAA0C;AAE3D,WAAO,OAAO,OAAO,WAAgC;AAAA,MACpD,KAAK,MACH,OAAmB;AAAA,QAAY,CAAC,aAChC;AAAA,UACC,OAAO;AAAA,YACN,OAAO,eAAe,SAAS,QAAkB;AAAA,YACjD,OAAO,eAAe,WAAW,QAAc;AAAA,YAC/C,OAAO,SAAS,CAAC,MAAM,IAAI,eAAe,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC;AAAA,UAC5D;AAAA,QACD;AAAA,MACD;AAAA,MACD,OAAO,CAAC,UAAU;AACjB,YAAI,iBAAiB,eAAgB,QAAO,MAAM;AAClD,eAAO,IAAI,aAAa;AAAA,UACvB,SAAS;AAAA,UACT,OAAO;AAAA,QACR,CAAC;AAAA,MACF;AAAA,IACD,CAAC;AAAA,EACF,CAAC,EAAE,KAAK,OAAO,MAAM;AAEtB,SAAO;AAAA,IACN;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,OAAO,MAAM,OAAO,SAAS,YAAY;AAAA,EAC1C;AACD;","names":["TagTypeId","ChannelTypeId","NodeInspectSymbol","STMTypeId","SinkTypeId","StreamTypeId"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ff-effect",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.10",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -20,23 +20,35 @@
|
|
|
20
20
|
],
|
|
21
21
|
"scripts": {
|
|
22
22
|
"build": "tsup",
|
|
23
|
-
"test": "vitest run",
|
|
23
|
+
"test": "bun -b vitest run",
|
|
24
24
|
"dev": "tsup --watch"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
|
+
"@effect/platform": "^0.94.2",
|
|
28
|
+
"@effect/platform-bun": "^0.87.1",
|
|
29
|
+
"@effect/platform-node": "^0.104.1",
|
|
27
30
|
"@effect/vitest": "^0.27.0",
|
|
31
|
+
"@electric-sql/pglite": "^0.3.15",
|
|
32
|
+
"@libsql/client": "^0.17.0",
|
|
28
33
|
"@orpc/contract": "^1.11.3",
|
|
29
34
|
"@orpc/server": "^1.11.3",
|
|
30
35
|
"@total-typescript/tsconfig": "^1.0.4",
|
|
31
36
|
"@types/bun": "^1.3.2",
|
|
37
|
+
"@typescript/native-preview": "^7.0.0-dev.20260122.4",
|
|
32
38
|
"tsup": "^8.5.0",
|
|
33
39
|
"typescript": "^5.9.3",
|
|
34
40
|
"valibot": "^1.1.0",
|
|
35
41
|
"vitest": "^4.0.10"
|
|
36
42
|
},
|
|
37
43
|
"peerDependencies": {
|
|
44
|
+
"drizzle-orm": "^0.44.7",
|
|
38
45
|
"effect": "^3.19.3"
|
|
39
46
|
},
|
|
47
|
+
"peerDependenciesMeta": {
|
|
48
|
+
"drizzle-orm": {
|
|
49
|
+
"optional": true
|
|
50
|
+
}
|
|
51
|
+
},
|
|
40
52
|
"publishConfig": {
|
|
41
53
|
"access": "public"
|
|
42
54
|
},
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import * as BunContext from '@effect/platform-bun/BunContext';
|
|
2
|
+
import { describe, expect, layer } from '@effect/vitest';
|
|
3
|
+
import { PGlite } from '@electric-sql/pglite';
|
|
4
|
+
import { pgTable, text } from 'drizzle-orm/pg-core';
|
|
5
|
+
import { drizzle } from 'drizzle-orm/pglite';
|
|
6
|
+
import { Effect, Layer } from 'effect';
|
|
7
|
+
import { expectTypeOf } from 'vitest';
|
|
8
|
+
import { createDatabase, DrizzleError } from './index.js';
|
|
9
|
+
|
|
10
|
+
const users = pgTable('users', {
|
|
11
|
+
id: text('id').primaryKey(),
|
|
12
|
+
name: text('name').notNull(),
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const schema = { users };
|
|
16
|
+
|
|
17
|
+
export class TestDb extends Effect.Service<TestDb>()('TestDb', {
|
|
18
|
+
accessors: true,
|
|
19
|
+
scoped: Effect.gen(function* () {
|
|
20
|
+
const db = new PGlite();
|
|
21
|
+
yield* Effect.promise(async () => {
|
|
22
|
+
await db.query('DROP TABLE IF EXISTS users');
|
|
23
|
+
await db.query(
|
|
24
|
+
'CREATE TABLE users (id TEXT PRIMARY KEY, name TEXT NOT NULL)',
|
|
25
|
+
);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const dump = yield* Effect.promise(() => db.dumpDataDir());
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
get: () =>
|
|
32
|
+
Effect.gen(function* () {
|
|
33
|
+
const db = new PGlite('memory://', { loadDataDir: dump });
|
|
34
|
+
return drizzle(db, { schema });
|
|
35
|
+
}),
|
|
36
|
+
};
|
|
37
|
+
}),
|
|
38
|
+
}) {}
|
|
39
|
+
|
|
40
|
+
layer(Layer.mergeAll(BunContext.layer, TestDb.Default))((it) => {
|
|
41
|
+
describe('db', () => {
|
|
42
|
+
it.scoped('wraps promises and returns correct data', () =>
|
|
43
|
+
Effect.gen(function* () {
|
|
44
|
+
const testDb = yield* TestDb.get();
|
|
45
|
+
yield* Effect.promise(() =>
|
|
46
|
+
testDb.insert(users).values({ id: '1', name: 'Alice' }),
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
const database = createDatabase(Effect.succeed(testDb));
|
|
50
|
+
|
|
51
|
+
const result = yield* database
|
|
52
|
+
.db((d) => d.select().from(users))
|
|
53
|
+
.pipe(Effect.provide(database.layer));
|
|
54
|
+
|
|
55
|
+
expect(result).toEqual([{ id: '1', name: 'Alice' }]);
|
|
56
|
+
}),
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
it.scoped('surfaces errors as DrizzleError', () =>
|
|
60
|
+
Effect.gen(function* () {
|
|
61
|
+
const testDb = yield* TestDb.get();
|
|
62
|
+
|
|
63
|
+
const database = createDatabase(Effect.succeed(testDb));
|
|
64
|
+
|
|
65
|
+
const result = yield* database
|
|
66
|
+
.db(() => Promise.reject(new Error('DB failure')))
|
|
67
|
+
.pipe(Effect.provide(database.layer), Effect.flip);
|
|
68
|
+
|
|
69
|
+
expect(result).toBeInstanceOf(DrizzleError);
|
|
70
|
+
expect(result.message).toBe('Database operation failed');
|
|
71
|
+
expect(result.cause).toBeInstanceOf(Error);
|
|
72
|
+
expect((result.cause as Error).message).toBe('DB failure');
|
|
73
|
+
}),
|
|
74
|
+
);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe('withTransaction', () => {
|
|
78
|
+
it.scoped('provides transaction context to nested db calls', () =>
|
|
79
|
+
Effect.gen(function* () {
|
|
80
|
+
const testDb = yield* TestDb.get();
|
|
81
|
+
|
|
82
|
+
const database = createDatabase(Effect.succeed(testDb));
|
|
83
|
+
|
|
84
|
+
yield* Effect.gen(function* () {
|
|
85
|
+
const ok = 'ok' as const;
|
|
86
|
+
const txResult = yield* database.withTransaction(
|
|
87
|
+
Effect.gen(function* () {
|
|
88
|
+
yield* database.db((d) =>
|
|
89
|
+
d.insert(users).values({ id: '1', name: 'Bob' }),
|
|
90
|
+
);
|
|
91
|
+
yield* database.db((d) =>
|
|
92
|
+
d.insert(users).values({ id: '2', name: 'Carol' }),
|
|
93
|
+
);
|
|
94
|
+
return ok;
|
|
95
|
+
}),
|
|
96
|
+
);
|
|
97
|
+
expectTypeOf(txResult).toEqualTypeOf<typeof ok>();
|
|
98
|
+
|
|
99
|
+
const result = yield* database.db((d) => d.select().from(users));
|
|
100
|
+
expect(result).toHaveLength(2);
|
|
101
|
+
expect(result).toEqual([
|
|
102
|
+
{ id: '1', name: 'Bob' },
|
|
103
|
+
{ id: '2', name: 'Carol' },
|
|
104
|
+
]);
|
|
105
|
+
}).pipe(Effect.provide(database.layer));
|
|
106
|
+
}),
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
it.scoped('rolls back on error', () =>
|
|
110
|
+
Effect.gen(function* () {
|
|
111
|
+
const testDb = yield* TestDb.get();
|
|
112
|
+
|
|
113
|
+
const database = createDatabase(Effect.succeed(testDb));
|
|
114
|
+
|
|
115
|
+
const result = yield* Effect.gen(function* () {
|
|
116
|
+
const txResult = yield* database
|
|
117
|
+
.withTransaction(
|
|
118
|
+
Effect.gen(function* () {
|
|
119
|
+
yield* database.db((d) =>
|
|
120
|
+
d.insert(users).values({ id: '1', name: 'Dave' }),
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
yield* database.tx((d) =>
|
|
124
|
+
d.insert(users).values({ id: '2', name: 'Bob' }),
|
|
125
|
+
);
|
|
126
|
+
return yield* Effect.fail(new Error('Intentional failure'));
|
|
127
|
+
}),
|
|
128
|
+
)
|
|
129
|
+
.pipe(Effect.either);
|
|
130
|
+
|
|
131
|
+
expect(txResult._tag).toBe('Left');
|
|
132
|
+
|
|
133
|
+
return yield* database.db((d) => d.select().from(users));
|
|
134
|
+
}).pipe(Effect.provide(database.layer));
|
|
135
|
+
|
|
136
|
+
expect(result).toEqual([]);
|
|
137
|
+
}),
|
|
138
|
+
);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
describe('multiple database', () => {
|
|
142
|
+
it.scoped(
|
|
143
|
+
'allows creating multiple database instances with different tagIds',
|
|
144
|
+
() =>
|
|
145
|
+
Effect.gen(function* () {
|
|
146
|
+
const testDb = yield* TestDb;
|
|
147
|
+
const database1 = createDatabase(testDb.get());
|
|
148
|
+
|
|
149
|
+
const database2Identifier = 'custom-db' as const;
|
|
150
|
+
const database2 = createDatabase(testDb.get(), {
|
|
151
|
+
tagId: database2Identifier,
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
const firstDbEffect = Effect.gen(function* () {
|
|
155
|
+
yield* database1.db((d) =>
|
|
156
|
+
d.insert(users).values({ id: '1', name: 'Eve' }),
|
|
157
|
+
);
|
|
158
|
+
});
|
|
159
|
+
expectTypeOf(firstDbEffect).toEqualTypeOf<
|
|
160
|
+
Effect.Effect<
|
|
161
|
+
void,
|
|
162
|
+
DrizzleError,
|
|
163
|
+
typeof database1.Drizzle.Identifier
|
|
164
|
+
>
|
|
165
|
+
>();
|
|
166
|
+
|
|
167
|
+
const secondDbEffect = Effect.gen(function* () {
|
|
168
|
+
yield* database2.db((d) =>
|
|
169
|
+
d.insert(users).values({ id: '2', name: 'Frank' }),
|
|
170
|
+
);
|
|
171
|
+
});
|
|
172
|
+
expectTypeOf(secondDbEffect).toEqualTypeOf<
|
|
173
|
+
Effect.Effect<void, DrizzleError, typeof database2Identifier>
|
|
174
|
+
>();
|
|
175
|
+
|
|
176
|
+
const main = Effect.gen(function* () {
|
|
177
|
+
yield* firstDbEffect;
|
|
178
|
+
yield* secondDbEffect;
|
|
179
|
+
|
|
180
|
+
yield* database1
|
|
181
|
+
.db((d) => d.select().from(users))
|
|
182
|
+
.pipe(
|
|
183
|
+
Effect.flatMap((result) =>
|
|
184
|
+
Effect.sync(() => {
|
|
185
|
+
expect(result).toEqual([{ id: '1', name: 'Eve' }]);
|
|
186
|
+
}),
|
|
187
|
+
),
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
yield* database2
|
|
191
|
+
.db((d) => d.select().from(users))
|
|
192
|
+
.pipe(
|
|
193
|
+
Effect.flatMap((result) =>
|
|
194
|
+
Effect.sync(() => {
|
|
195
|
+
expect(result).toEqual([{ id: '2', name: 'Frank' }]);
|
|
196
|
+
}),
|
|
197
|
+
),
|
|
198
|
+
);
|
|
199
|
+
}).pipe(
|
|
200
|
+
Effect.provide(database1.layer),
|
|
201
|
+
Effect.provide(database2.layer),
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
yield* main;
|
|
205
|
+
expectTypeOf(main).toEqualTypeOf<Effect.Effect<void, DrizzleError>>();
|
|
206
|
+
}),
|
|
207
|
+
);
|
|
208
|
+
});
|
|
209
|
+
});
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { Data, Effect, FiberSet, Layer } from 'effect';
|
|
2
|
+
import * as Channel from 'effect/Channel';
|
|
3
|
+
import * as Context from 'effect/Context';
|
|
4
|
+
import * as Inspectable from 'effect/Inspectable';
|
|
5
|
+
import * as Sink from 'effect/Sink';
|
|
6
|
+
import * as STM from 'effect/STM';
|
|
7
|
+
import * as Stream from 'effect/Stream';
|
|
8
|
+
import * as EUnify from 'effect/Unify';
|
|
9
|
+
|
|
10
|
+
// TypeScript issue where the return type of createDatabase
|
|
11
|
+
// contains internal Effect types (TagTypeId) that aren't exported
|
|
12
|
+
// from this module,
|
|
13
|
+
// so TypeScript can't "name" them in the declaration file.
|
|
14
|
+
export const TagTypeId = Context.TagTypeId;
|
|
15
|
+
export const ChannelTypeId = Channel.ChannelTypeId;
|
|
16
|
+
export const EffectTypeId = Effect.EffectTypeId;
|
|
17
|
+
export const NodeInspectSymbol = Inspectable.NodeInspectSymbol;
|
|
18
|
+
export const STMTypeId = STM.STMTypeId;
|
|
19
|
+
export const SinkTypeId = Sink.SinkTypeId;
|
|
20
|
+
export const StreamTypeId = Stream.StreamTypeId;
|
|
21
|
+
export const Unify = EUnify;
|
|
22
|
+
|
|
23
|
+
export class DrizzleError extends Data.TaggedError('ff-effect/DrizzleError')<{
|
|
24
|
+
message: string;
|
|
25
|
+
cause?: unknown;
|
|
26
|
+
}> {}
|
|
27
|
+
|
|
28
|
+
type AnyDrizzleClient = {
|
|
29
|
+
transaction: (fn: (tx: any) => Promise<any>) => Promise<any>;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
type TxClient<TClient extends AnyDrizzleClient> = Parameters<
|
|
33
|
+
Parameters<TClient['transaction']>[0]
|
|
34
|
+
>[0];
|
|
35
|
+
|
|
36
|
+
class WrappedTxError extends Error {}
|
|
37
|
+
|
|
38
|
+
const defaultPrefix = '@ff-effect/Drizzle' as const;
|
|
39
|
+
|
|
40
|
+
export function createDatabase<
|
|
41
|
+
TClient extends AnyDrizzleClient,
|
|
42
|
+
E,
|
|
43
|
+
R,
|
|
44
|
+
T extends string = typeof defaultPrefix,
|
|
45
|
+
>(createClient: Effect.Effect<TClient, E, R>, opts?: { tagId?: T }) {
|
|
46
|
+
type Client = TClient | TxClient<TClient>;
|
|
47
|
+
type Tx = TxClient<TClient>;
|
|
48
|
+
|
|
49
|
+
const tagId = (opts?.tagId ?? defaultPrefix) as T;
|
|
50
|
+
|
|
51
|
+
type Drizzle = typeof tagId;
|
|
52
|
+
const Drizzle = Context.Tag(tagId)<Drizzle, Client>();
|
|
53
|
+
|
|
54
|
+
const drizzleTxTagId = `${tagId}.tx` as const;
|
|
55
|
+
type DrizzleTx = typeof drizzleTxTagId;
|
|
56
|
+
const DrizzleTx = Context.Tag(drizzleTxTagId)<DrizzleTx, Tx>();
|
|
57
|
+
|
|
58
|
+
const db = <T>(fn: (client: Client) => Promise<T>) =>
|
|
59
|
+
Effect.gen(function* () {
|
|
60
|
+
const client = yield* Drizzle;
|
|
61
|
+
return yield* Effect.tryPromise({
|
|
62
|
+
try: () => fn(client),
|
|
63
|
+
catch: (cause) =>
|
|
64
|
+
new DrizzleError({ message: 'Database operation failed', cause }),
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
/** Requires being inside withTransaction - enforces transaction at compile time */
|
|
69
|
+
const tx = <T>(fn: (client: Tx) => Promise<T>) =>
|
|
70
|
+
Effect.gen(function* () {
|
|
71
|
+
const client = yield* DrizzleTx;
|
|
72
|
+
return yield* Effect.tryPromise({
|
|
73
|
+
try: () => fn(client),
|
|
74
|
+
catch: (cause) =>
|
|
75
|
+
new DrizzleError({ message: 'Database operation failed', cause }),
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const withTransaction = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
|
|
80
|
+
Effect.gen(function* () {
|
|
81
|
+
const client = yield* Drizzle;
|
|
82
|
+
const runFork =
|
|
83
|
+
yield* FiberSet.makeRuntimePromise<Exclude<R, DrizzleTx>>();
|
|
84
|
+
|
|
85
|
+
return yield* Effect.tryPromise<A, E | DrizzleError>({
|
|
86
|
+
try: () =>
|
|
87
|
+
(client as TClient).transaction((txClient) =>
|
|
88
|
+
runFork(
|
|
89
|
+
effect.pipe(
|
|
90
|
+
Effect.provideService(Drizzle, txClient as Client),
|
|
91
|
+
Effect.provideService(DrizzleTx, txClient as Tx),
|
|
92
|
+
Effect.mapError((e) => new WrappedTxError('', { cause: e })),
|
|
93
|
+
) as Effect.Effect<A, WrappedTxError, Exclude<R, DrizzleTx>>,
|
|
94
|
+
),
|
|
95
|
+
),
|
|
96
|
+
catch: (error) => {
|
|
97
|
+
if (error instanceof WrappedTxError) return error.cause as E;
|
|
98
|
+
return new DrizzleError({
|
|
99
|
+
message: 'Transaction failed',
|
|
100
|
+
cause: error,
|
|
101
|
+
});
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
}).pipe(Effect.scoped);
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
db,
|
|
108
|
+
tx,
|
|
109
|
+
Drizzle,
|
|
110
|
+
DrizzleTx,
|
|
111
|
+
withTransaction,
|
|
112
|
+
layer: Layer.effect(Drizzle, createClient),
|
|
113
|
+
};
|
|
114
|
+
}
|