@shivaedev/platform 0.3.8 → 0.4.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/CHANGELOG.md +18 -1
- package/README.md +41 -2
- package/dist/better-auth/adapter.d.ts +2 -2
- package/dist/better-auth/adapter.d.ts.map +1 -1
- package/dist/better-auth/adapter.js +39 -95
- package/dist/better-auth/adapter.js.map +1 -1
- package/dist/better-auth/relation.d.ts +7 -27
- package/dist/better-auth/relation.d.ts.map +1 -1
- package/dist/better-auth/relation.js +2 -84
- package/dist/better-auth/relation.js.map +1 -1
- package/dist/better-auth/row-adapter.d.ts +8 -0
- package/dist/better-auth/row-adapter.d.ts.map +1 -0
- package/dist/better-auth/row-adapter.js +45 -0
- package/dist/better-auth/row-adapter.js.map +1 -0
- package/dist/better-auth/where.d.ts +22 -0
- package/dist/better-auth/where.d.ts.map +1 -0
- package/dist/better-auth/where.js +93 -0
- package/dist/better-auth/where.js.map +1 -0
- package/dist/better-auth.d.ts +1 -1
- package/dist/errors/rejected-field.d.ts +7 -0
- package/dist/errors/rejected-field.d.ts.map +1 -0
- package/dist/errors/rejected-field.js +5 -0
- package/dist/errors/rejected-field.js.map +1 -0
- package/dist/errors/taxonomy.d.ts +40 -0
- package/dist/errors/taxonomy.d.ts.map +1 -0
- package/dist/errors/taxonomy.js +18 -0
- package/dist/errors/taxonomy.js.map +1 -0
- package/dist/errors.d.ts +3 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +3 -0
- package/dist/errors.js.map +1 -0
- package/dist/node-http/subscription-signal.d.ts.map +1 -1
- package/dist/node-http/subscription-signal.js +6 -1
- package/dist/node-http/subscription-signal.js.map +1 -1
- package/dist/node-http.d.ts +1 -1
- package/dist/rpc/identity.d.ts +15 -0
- package/dist/rpc/identity.d.ts.map +1 -0
- package/dist/rpc/identity.js +8 -0
- package/dist/rpc/identity.js.map +1 -0
- package/dist/rpc/middleware.d.ts +15 -0
- package/dist/rpc/middleware.d.ts.map +1 -0
- package/dist/rpc/middleware.js +12 -0
- package/dist/rpc/middleware.js.map +1 -0
- package/dist/rpc-server/adapters/better-auth-sessions.d.ts +4 -0
- package/dist/rpc-server/adapters/better-auth-sessions.d.ts.map +1 -0
- package/dist/rpc-server/adapters/better-auth-sessions.js +11 -0
- package/dist/rpc-server/adapters/better-auth-sessions.js.map +1 -0
- package/dist/rpc-server/origin.d.ts +15 -0
- package/dist/rpc-server/origin.d.ts.map +1 -0
- package/dist/rpc-server/origin.js +12 -0
- package/dist/rpc-server/origin.js.map +1 -0
- package/dist/rpc-server/redact-cause.d.ts +6 -0
- package/dist/rpc-server/redact-cause.d.ts.map +1 -0
- package/dist/rpc-server/redact-cause.js +28 -0
- package/dist/rpc-server/redact-cause.js.map +1 -0
- package/dist/rpc-server/redact.d.ts +3 -0
- package/dist/rpc-server/redact.d.ts.map +1 -0
- package/dist/rpc-server/redact.js +46 -0
- package/dist/rpc-server/redact.js.map +1 -0
- package/dist/rpc-server/sensitive.d.ts +5 -0
- package/dist/rpc-server/sensitive.d.ts.map +1 -0
- package/dist/rpc-server/sensitive.js +49 -0
- package/dist/rpc-server/sensitive.js.map +1 -0
- package/dist/rpc-server/session.d.ts +21 -0
- package/dist/rpc-server/session.d.ts.map +1 -0
- package/dist/rpc-server/session.js +28 -0
- package/dist/rpc-server/session.js.map +1 -0
- package/dist/rpc-server/tracing.d.ts +9 -0
- package/dist/rpc-server/tracing.d.ts.map +1 -0
- package/dist/rpc-server/tracing.js +32 -0
- package/dist/rpc-server/tracing.js.map +1 -0
- package/dist/rpc-server/transport.d.ts +4 -0
- package/dist/rpc-server/transport.d.ts.map +1 -0
- package/dist/rpc-server/transport.js +4 -0
- package/dist/rpc-server/transport.js.map +1 -0
- package/dist/rpc-server.d.ts +9 -0
- package/dist/rpc-server.d.ts.map +1 -0
- package/dist/rpc-server.js +9 -0
- package/dist/rpc-server.js.map +1 -0
- package/dist/rpc.d.ts +3 -0
- package/dist/rpc.d.ts.map +1 -0
- package/dist/rpc.js +3 -0
- package/dist/rpc.js.map +1 -0
- package/dist/runtime/make.d.ts +1 -1
- package/dist/runtime/make.d.ts.map +1 -1
- package/dist/runtime/make.js +10 -9
- package/dist/runtime/make.js.map +1 -1
- package/dist/runtime/types.d.ts.map +1 -1
- package/dist/runtime.d.ts +2 -2
- package/dist/testing/types.d.ts.map +1 -1
- package/dist/testing/vitest.d.ts +1 -1
- package/dist/testing/vitest.d.ts.map +1 -1
- package/dist/testing/vitest.js +7 -9
- package/dist/testing/vitest.js.map +1 -1
- package/dist/testing.d.ts +2 -2
- package/package.json +25 -5
- package/src/better-auth/adapter.ts +50 -190
- package/src/better-auth/relation.ts +21 -163
- package/src/better-auth/row-adapter.ts +78 -0
- package/src/better-auth/where.ts +118 -0
- package/src/better-auth.ts +1 -1
- package/src/errors/rejected-field.ts +13 -0
- package/src/errors/taxonomy.ts +18 -0
- package/src/errors.ts +2 -0
- package/src/node-http/subscription-signal.ts +10 -15
- package/src/node-http.ts +1 -1
- package/src/rpc/identity.ts +11 -0
- package/src/rpc/middleware.ts +15 -0
- package/src/rpc-server/adapters/better-auth-sessions.ts +22 -0
- package/src/rpc-server/origin.ts +29 -0
- package/src/rpc-server/redact-cause.ts +41 -0
- package/src/rpc-server/redact.ts +42 -0
- package/src/rpc-server/sensitive.ts +60 -0
- package/src/rpc-server/session.ts +54 -0
- package/src/rpc-server/tracing.ts +56 -0
- package/src/rpc-server/transport.ts +8 -0
- package/src/rpc-server.ts +15 -0
- package/src/rpc.ts +2 -0
- package/src/runtime/make.ts +20 -27
- package/src/runtime/types.ts +3 -12
- package/src/runtime.ts +2 -2
- package/src/testing/types.ts +7 -32
- package/src/testing/vitest.ts +59 -89
- package/src/testing.ts +2 -2
|
@@ -1,18 +1,10 @@
|
|
|
1
|
-
import type { AnyDatabase
|
|
2
|
-
import type { BetterAuthOptions, DBAdapterInstance
|
|
3
|
-
import {
|
|
4
|
-
import { Effect
|
|
5
|
-
import type { PlatformRuntime } from "../runtime/types.
|
|
6
|
-
import { type DynamicRelation, refineRelation } from "./relation.
|
|
7
|
-
|
|
8
|
-
type CleanedWhere = Required<Where>;
|
|
9
|
-
type FindOneInput = Parameters<CustomAdapter["findOne"]>[0];
|
|
10
|
-
type FindManyInput = Parameters<CustomAdapter["findMany"]>[0];
|
|
11
|
-
type UpdateInput<Value> = {
|
|
12
|
-
readonly model: string;
|
|
13
|
-
readonly update: Value;
|
|
14
|
-
readonly where: CleanedWhere[];
|
|
15
|
-
};
|
|
1
|
+
import type { AnyDatabase } from "@shivaedev/effect-prisma";
|
|
2
|
+
import type { BetterAuthOptions, DBAdapterInstance } from "better-auth";
|
|
3
|
+
import { createAdapterFactory } from "better-auth/adapters";
|
|
4
|
+
import { Effect } from "effect";
|
|
5
|
+
import type { PlatformRuntime } from "../runtime/types.ts";
|
|
6
|
+
import { type DynamicRelation, refineRelation } from "./relation.ts";
|
|
7
|
+
import { makeRowAdapter, type RelationQuery } from "./row-adapter.ts";
|
|
16
8
|
|
|
17
9
|
export interface EffectPrismaAdapterOptions {
|
|
18
10
|
readonly debugLogs?: boolean;
|
|
@@ -21,54 +13,55 @@ export interface EffectPrismaAdapterOptions {
|
|
|
21
13
|
readonly usePlural?: boolean;
|
|
22
14
|
}
|
|
23
15
|
|
|
24
|
-
interface DynamicDatabase
|
|
25
|
-
readonly transaction: <A, E, R>(
|
|
26
|
-
program: Effect.Effect<A, E, R>,
|
|
27
|
-
) => Effect.Effect<A, E | unknown, R | Requirements>;
|
|
28
|
-
readonly [model: string]: DynamicRelation<Requirements> | unknown;
|
|
16
|
+
interface DynamicDatabase {
|
|
17
|
+
readonly transaction: <A, E, R>(program: Effect.Effect<A, E, R>) => Effect.Effect<A, unknown, unknown>;
|
|
29
18
|
}
|
|
30
19
|
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
model: string,
|
|
34
|
-
mapModelName: (model: string) => string,
|
|
35
|
-
): DynamicRelation<Requirements> =>
|
|
36
|
-
Reflect.get(database, mapModelName(model)) as DynamicRelation<Requirements>;
|
|
20
|
+
const isFunction = (value: unknown, key: string): boolean =>
|
|
21
|
+
((typeof value === "object" && value !== null) || typeof value === "function") && typeof Reflect.get(value, key) === "function";
|
|
37
22
|
|
|
38
|
-
const
|
|
39
|
-
model.length === 0 ? model : `${model[0]?.toUpperCase()}${model.slice(1)}`;
|
|
23
|
+
const isDynamicDatabase = (value: unknown): value is DynamicDatabase => isFunction(value, "transaction");
|
|
40
24
|
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
25
|
+
const isDynamicRelation = (value: unknown): value is DynamicRelation<unknown> => isFunction(value, "where") && isFunction(value, "select");
|
|
26
|
+
|
|
27
|
+
const modelRelation = (database: DynamicDatabase, model: string): DynamicRelation<unknown> => {
|
|
28
|
+
const relation: unknown = Reflect.get(database, model);
|
|
29
|
+
if (!isDynamicRelation(relation)) {
|
|
30
|
+
throw new TypeError(`Unknown database model: ${model}`);
|
|
46
31
|
}
|
|
32
|
+
return relation;
|
|
47
33
|
};
|
|
48
34
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
35
|
+
const defaultModelName = (model: string): string => (model.length === 0 ? model : `${model[0]?.toUpperCase()}${model.slice(1)}`);
|
|
36
|
+
|
|
37
|
+
export function effectPrismaAdapter<Database extends AnyDatabase, Services, BuildError>(
|
|
38
|
+
databaseTag: Database,
|
|
39
|
+
runtime: PlatformRuntime<Services, BuildError>,
|
|
40
|
+
adapterOptions?: EffectPrismaAdapterOptions,
|
|
41
|
+
): DBAdapterInstance;
|
|
42
|
+
export function effectPrismaAdapter(
|
|
43
|
+
databaseTag: AnyDatabase,
|
|
44
|
+
runtime: PlatformRuntime<unknown, unknown>,
|
|
45
|
+
adapterOptions: EffectPrismaAdapterOptions = {},
|
|
46
|
+
): DBAdapterInstance {
|
|
47
|
+
return (authOptions: BetterAuthOptions) => {
|
|
56
48
|
const mapModelName = adapterOptions.modelName ?? defaultModelName;
|
|
57
|
-
const databaseEffect = databaseTag
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
49
|
+
const databaseEffect = Effect.flatMap(databaseTag, (database) =>
|
|
50
|
+
isDynamicDatabase(database) ? Effect.succeed(database) : Effect.die(new TypeError("The database service has no transaction")),
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
const run = <Value>(operation: (database: DynamicDatabase) => Effect.Effect<Value, unknown, unknown>): Promise<Value> =>
|
|
54
|
+
runtime.runPromise(Effect.flatMap(databaseEffect, operation));
|
|
62
55
|
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
),
|
|
56
|
+
const query: RelationQuery = (model, refinement, use) =>
|
|
57
|
+
run((database) => use(refineRelation(modelRelation(database, mapModelName(model)), refinement)));
|
|
58
|
+
|
|
59
|
+
const inTransaction = <Value>(callback: () => Promise<Value>): Effect.Effect<Value, unknown, unknown> =>
|
|
60
|
+
Effect.flatMap(Effect.context<unknown>(), (services) =>
|
|
61
|
+
Effect.tryPromise({
|
|
62
|
+
try: () => runtime.runWithServices(services, callback),
|
|
63
|
+
catch: (error) => error,
|
|
64
|
+
}),
|
|
72
65
|
);
|
|
73
66
|
|
|
74
67
|
let factory: ReturnType<typeof createAdapterFactory>;
|
|
@@ -83,144 +76,11 @@ export const effectPrismaAdapter =
|
|
|
83
76
|
supportsJSON: true,
|
|
84
77
|
supportsNumericIds: true,
|
|
85
78
|
supportsUUIDs: true,
|
|
86
|
-
transaction: (callback) =>
|
|
87
|
-
run((database) =>
|
|
88
|
-
database.transaction(
|
|
89
|
-
Effect.flatMap(Effect.context<Services>(), (services) =>
|
|
90
|
-
Effect.tryPromise({
|
|
91
|
-
try: () =>
|
|
92
|
-
runtime.runWithServices(services, () =>
|
|
93
|
-
callback(factory(authOptions)),
|
|
94
|
-
),
|
|
95
|
-
catch: (error) => error,
|
|
96
|
-
}),
|
|
97
|
-
),
|
|
98
|
-
),
|
|
99
|
-
),
|
|
79
|
+
transaction: (callback) => run((database) => database.transaction(inTransaction(() => callback(factory(authOptions))))),
|
|
100
80
|
},
|
|
101
|
-
adapter: (
|
|
102
|
-
create: async ({ data, model, select }) => {
|
|
103
|
-
debugLog("create", { model });
|
|
104
|
-
return (await run((database) =>
|
|
105
|
-
refineRelation(modelRelation(database, model, mapModelName), {
|
|
106
|
-
select,
|
|
107
|
-
}).create(data),
|
|
108
|
-
)) as typeof data;
|
|
109
|
-
},
|
|
110
|
-
findOne: async <Value>({
|
|
111
|
-
join,
|
|
112
|
-
model,
|
|
113
|
-
select,
|
|
114
|
-
where,
|
|
115
|
-
}: FindOneInput) => {
|
|
116
|
-
rejectJoin(join);
|
|
117
|
-
return (await run((database) =>
|
|
118
|
-
Effect.map(
|
|
119
|
-
refineRelation(modelRelation(database, model, mapModelName), {
|
|
120
|
-
select,
|
|
121
|
-
where,
|
|
122
|
-
}).first(),
|
|
123
|
-
Option.getOrNull,
|
|
124
|
-
),
|
|
125
|
-
)) as Value | null;
|
|
126
|
-
},
|
|
127
|
-
findMany: async <Value>({
|
|
128
|
-
join,
|
|
129
|
-
limit,
|
|
130
|
-
model,
|
|
131
|
-
offset,
|
|
132
|
-
select,
|
|
133
|
-
sortBy,
|
|
134
|
-
where,
|
|
135
|
-
}: FindManyInput) => {
|
|
136
|
-
rejectJoin(join);
|
|
137
|
-
return (await run(
|
|
138
|
-
(database) =>
|
|
139
|
-
refineRelation(modelRelation(database, model, mapModelName), {
|
|
140
|
-
limit,
|
|
141
|
-
offset,
|
|
142
|
-
select,
|
|
143
|
-
sortBy,
|
|
144
|
-
where,
|
|
145
|
-
}) as unknown as Effect.Effect<
|
|
146
|
-
ReadonlyArray<Record<string, unknown>>,
|
|
147
|
-
unknown,
|
|
148
|
-
Services
|
|
149
|
-
>,
|
|
150
|
-
)) as Value[];
|
|
151
|
-
},
|
|
152
|
-
update: async <Value>({ model, update, where }: UpdateInput<Value>) => {
|
|
153
|
-
if (where.length === 0) return null;
|
|
154
|
-
const hasUniqueCondition = where.some(
|
|
155
|
-
(condition) =>
|
|
156
|
-
condition.connector !== "OR" &&
|
|
157
|
-
condition.operator === "eq" &&
|
|
158
|
-
condition.mode !== "insensitive" &&
|
|
159
|
-
(condition.field === "id" ||
|
|
160
|
-
getFieldAttributes({
|
|
161
|
-
field: condition.field,
|
|
162
|
-
model,
|
|
163
|
-
}).unique === true),
|
|
164
|
-
);
|
|
165
|
-
return (await run((database) => {
|
|
166
|
-
const relation = refineRelation(
|
|
167
|
-
modelRelation(database, model, mapModelName),
|
|
168
|
-
{ where },
|
|
169
|
-
);
|
|
170
|
-
return hasUniqueCondition
|
|
171
|
-
? relation.update(update as Record<string, unknown>)
|
|
172
|
-
: Effect.map(
|
|
173
|
-
relation.updateAll(update as Record<string, unknown>),
|
|
174
|
-
(rows) => rows[0] ?? null,
|
|
175
|
-
);
|
|
176
|
-
})) as Value | null;
|
|
177
|
-
},
|
|
178
|
-
updateMany: ({ model, update, where }) =>
|
|
179
|
-
run((database) =>
|
|
180
|
-
Effect.map(
|
|
181
|
-
refineRelation(modelRelation(database, model, mapModelName), {
|
|
182
|
-
where,
|
|
183
|
-
}).updateAll(update),
|
|
184
|
-
(rows) => rows.length,
|
|
185
|
-
),
|
|
186
|
-
),
|
|
187
|
-
delete: ({ model, where }) => {
|
|
188
|
-
const hasId = where.some(
|
|
189
|
-
(condition) =>
|
|
190
|
-
condition.connector !== "OR" &&
|
|
191
|
-
condition.field === "id" &&
|
|
192
|
-
condition.operator === "eq",
|
|
193
|
-
);
|
|
194
|
-
return run((database) => {
|
|
195
|
-
const relation = refineRelation(
|
|
196
|
-
modelRelation(database, model, mapModelName),
|
|
197
|
-
{ where },
|
|
198
|
-
);
|
|
199
|
-
return hasId
|
|
200
|
-
? Effect.asVoid(relation.delete())
|
|
201
|
-
: Effect.asVoid(relation.deleteAll());
|
|
202
|
-
});
|
|
203
|
-
},
|
|
204
|
-
deleteMany: ({ model, where }) =>
|
|
205
|
-
run((database) =>
|
|
206
|
-
Effect.map(
|
|
207
|
-
refineRelation(modelRelation(database, model, mapModelName), {
|
|
208
|
-
where,
|
|
209
|
-
}).deleteAll(),
|
|
210
|
-
(rows) => rows.length,
|
|
211
|
-
),
|
|
212
|
-
),
|
|
213
|
-
count: ({ model, where }) =>
|
|
214
|
-
run((database) =>
|
|
215
|
-
refineRelation(modelRelation(database, model, mapModelName), {
|
|
216
|
-
where: where as CleanedWhere[] | undefined,
|
|
217
|
-
}).count(),
|
|
218
|
-
),
|
|
219
|
-
options: {
|
|
220
|
-
usePlural: adapterOptions.usePlural ?? false,
|
|
221
|
-
},
|
|
222
|
-
}),
|
|
81
|
+
adapter: makeRowAdapter(query, adapterOptions.usePlural ?? false),
|
|
223
82
|
});
|
|
224
83
|
|
|
225
84
|
return factory(authOptions);
|
|
226
85
|
};
|
|
86
|
+
}
|
|
@@ -1,177 +1,35 @@
|
|
|
1
|
-
import { all
|
|
2
|
-
import type { Where } from "better-auth";
|
|
1
|
+
import { all } from "@shivaedev/effect-prisma";
|
|
3
2
|
import type { Effect, Option } from "effect";
|
|
3
|
+
import { type CleanedWhere, type DynamicField, type Expression, whereExpression } from "./where.ts";
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
interface DynamicField {
|
|
9
|
-
eq(value: unknown): Expression;
|
|
10
|
-
neq(value: unknown): Expression;
|
|
11
|
-
gt(value: unknown): Expression;
|
|
12
|
-
gte(value: unknown): Expression;
|
|
13
|
-
lt(value: unknown): Expression;
|
|
14
|
-
lte(value: unknown): Expression;
|
|
15
|
-
in(value: ReadonlyArray<unknown>): Expression;
|
|
16
|
-
notIn(value: ReadonlyArray<unknown>): Expression;
|
|
17
|
-
like(value: string): Expression;
|
|
18
|
-
ilike?(value: string): Expression;
|
|
19
|
-
isNull(): Expression;
|
|
20
|
-
isNotNull(): Expression;
|
|
21
|
-
asc(): unknown;
|
|
22
|
-
desc(): unknown;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export interface DynamicRelation<Requirements> {
|
|
26
|
-
where(
|
|
27
|
-
predicate: (fields: Record<string, DynamicField>) => Expression,
|
|
28
|
-
): DynamicRelation<Requirements>;
|
|
29
|
-
orderBy(
|
|
30
|
-
ordering: (fields: Record<string, DynamicField>) => unknown,
|
|
31
|
-
): DynamicRelation<Requirements>;
|
|
5
|
+
export interface DynamicRelation<Requirements> extends Effect.Effect<ReadonlyArray<Record<string, unknown>>, unknown, Requirements> {
|
|
6
|
+
where(predicate: (fields: Record<string, DynamicField>) => Expression): DynamicRelation<Requirements>;
|
|
7
|
+
orderBy(ordering: (fields: Record<string, DynamicField>) => unknown): DynamicRelation<Requirements>;
|
|
32
8
|
take(count: number): DynamicRelation<Requirements>;
|
|
33
9
|
skip(count: number): DynamicRelation<Requirements>;
|
|
34
10
|
select(...fields: ReadonlyArray<string>): DynamicRelation<Requirements>;
|
|
35
|
-
first(): Effect.Effect<
|
|
36
|
-
Option.Option<Record<string, unknown>>,
|
|
37
|
-
unknown,
|
|
38
|
-
Requirements
|
|
39
|
-
>;
|
|
11
|
+
first(): Effect.Effect<Option.Option<Record<string, unknown>>, unknown, Requirements>;
|
|
40
12
|
count(): Effect.Effect<number, unknown, Requirements>;
|
|
41
|
-
create(
|
|
42
|
-
|
|
43
|
-
): Effect.Effect<Record<string, unknown
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
): Effect.Effect<Record<string, unknown> | null, unknown, Requirements>;
|
|
47
|
-
updateAll(
|
|
48
|
-
data: Record<string, unknown>,
|
|
49
|
-
): Effect.Effect<
|
|
50
|
-
ReadonlyArray<Record<string, unknown>>,
|
|
51
|
-
unknown,
|
|
52
|
-
Requirements
|
|
53
|
-
>;
|
|
54
|
-
delete(): Effect.Effect<
|
|
55
|
-
Record<string, unknown> | null,
|
|
56
|
-
unknown,
|
|
57
|
-
Requirements
|
|
58
|
-
>;
|
|
59
|
-
deleteAll(): Effect.Effect<
|
|
60
|
-
ReadonlyArray<Record<string, unknown>>,
|
|
61
|
-
unknown,
|
|
62
|
-
Requirements
|
|
63
|
-
>;
|
|
13
|
+
create(data: Record<string, unknown>): Effect.Effect<Record<string, unknown>, unknown, Requirements>;
|
|
14
|
+
update(data: Record<string, unknown>): Effect.Effect<Record<string, unknown> | null, unknown, Requirements>;
|
|
15
|
+
updateAll(data: Record<string, unknown>): Effect.Effect<ReadonlyArray<Record<string, unknown>>, unknown, Requirements>;
|
|
16
|
+
delete(): Effect.Effect<Record<string, unknown> | null, unknown, Requirements>;
|
|
17
|
+
deleteAll(): Effect.Effect<ReadonlyArray<Record<string, unknown>>, unknown, Requirements>;
|
|
64
18
|
}
|
|
65
19
|
|
|
66
|
-
const escapeLike = (value: string): string =>
|
|
67
|
-
value.replaceAll("\\", "\\\\").replaceAll("%", "\\%").replaceAll("_", "\\_");
|
|
68
|
-
|
|
69
|
-
const comparison = (
|
|
70
|
-
field: DynamicField,
|
|
71
|
-
where: CleanedWhere,
|
|
72
|
-
): Expression | undefined => {
|
|
73
|
-
const value = where.value;
|
|
74
|
-
if (
|
|
75
|
-
where.mode === "insensitive" &&
|
|
76
|
-
typeof value === "string" &&
|
|
77
|
-
field.ilike !== undefined
|
|
78
|
-
) {
|
|
79
|
-
if (where.operator === "eq") return field.ilike(escapeLike(value));
|
|
80
|
-
if (where.operator === "ne") return not(field.ilike(escapeLike(value)));
|
|
81
|
-
}
|
|
82
|
-
switch (where.operator) {
|
|
83
|
-
case "eq":
|
|
84
|
-
return value === null ? field.isNull() : field.eq(value);
|
|
85
|
-
case "ne":
|
|
86
|
-
return value === null ? field.isNotNull() : field.neq(value);
|
|
87
|
-
case "lt":
|
|
88
|
-
return field.lt(value);
|
|
89
|
-
case "lte":
|
|
90
|
-
return field.lte(value);
|
|
91
|
-
case "gt":
|
|
92
|
-
return field.gt(value);
|
|
93
|
-
case "gte":
|
|
94
|
-
return field.gte(value);
|
|
95
|
-
case "in": {
|
|
96
|
-
const values = (Array.isArray(value) ? value : [value]).filter(
|
|
97
|
-
(item) => item !== null,
|
|
98
|
-
);
|
|
99
|
-
const ilike = field.ilike;
|
|
100
|
-
return values.length === 0
|
|
101
|
-
? and(field.isNull(), field.isNotNull())
|
|
102
|
-
: where.mode === "insensitive" &&
|
|
103
|
-
ilike !== undefined &&
|
|
104
|
-
values.every((item) => typeof item === "string")
|
|
105
|
-
? or(...values.map((item) => ilike(escapeLike(String(item)))))
|
|
106
|
-
: field.in(values);
|
|
107
|
-
}
|
|
108
|
-
case "not_in": {
|
|
109
|
-
const values = (Array.isArray(value) ? value : [value]).filter(
|
|
110
|
-
(item) => item !== null,
|
|
111
|
-
);
|
|
112
|
-
if (values.length === 0) return undefined;
|
|
113
|
-
const ilike = field.ilike;
|
|
114
|
-
return where.mode === "insensitive" &&
|
|
115
|
-
ilike !== undefined &&
|
|
116
|
-
values.every((item) => typeof item === "string")
|
|
117
|
-
? and(...values.map((item) => not(ilike(escapeLike(String(item))))))
|
|
118
|
-
: field.notIn(values);
|
|
119
|
-
}
|
|
120
|
-
case "contains":
|
|
121
|
-
case "starts_with":
|
|
122
|
-
case "ends_with": {
|
|
123
|
-
if (typeof value !== "string") {
|
|
124
|
-
throw new TypeError(`${where.operator} requires a string value`);
|
|
125
|
-
}
|
|
126
|
-
const escaped = escapeLike(value);
|
|
127
|
-
const pattern =
|
|
128
|
-
where.operator === "contains"
|
|
129
|
-
? `%${escaped}%`
|
|
130
|
-
: where.operator === "starts_with"
|
|
131
|
-
? `${escaped}%`
|
|
132
|
-
: `%${escaped}`;
|
|
133
|
-
return where.mode === "insensitive" && field.ilike !== undefined
|
|
134
|
-
? field.ilike(pattern)
|
|
135
|
-
: field.like(pattern);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
};
|
|
139
|
-
|
|
140
|
-
const whereExpression = (
|
|
141
|
-
fields: Record<string, DynamicField>,
|
|
142
|
-
where: ReadonlyArray<CleanedWhere>,
|
|
143
|
-
): Expression | undefined => {
|
|
144
|
-
const conjunctions: Expression[] = [];
|
|
145
|
-
const disjunctions: Expression[] = [];
|
|
146
|
-
|
|
147
|
-
for (const condition of where) {
|
|
148
|
-
const field = fields[condition.field];
|
|
149
|
-
if (field === undefined) {
|
|
150
|
-
throw new TypeError(`Unknown database field: ${condition.field}`);
|
|
151
|
-
}
|
|
152
|
-
const expression = comparison(field, condition);
|
|
153
|
-
if (expression === undefined) continue;
|
|
154
|
-
(condition.connector === "OR" ? disjunctions : conjunctions).push(
|
|
155
|
-
expression,
|
|
156
|
-
);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
if (disjunctions.length > 0) conjunctions.push(or(...disjunctions));
|
|
160
|
-
if (conjunctions.length === 0) return all();
|
|
161
|
-
return conjunctions.length === 1 ? conjunctions[0] : and(...conjunctions);
|
|
162
|
-
};
|
|
163
|
-
|
|
164
20
|
export const refineRelation = <Requirements>(
|
|
165
21
|
relation: DynamicRelation<Requirements>,
|
|
166
22
|
options: {
|
|
167
|
-
readonly where?: ReadonlyArray<CleanedWhere
|
|
168
|
-
readonly select?: ReadonlyArray<string
|
|
169
|
-
readonly limit?: number;
|
|
170
|
-
readonly offset?: number;
|
|
171
|
-
readonly sortBy?:
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
23
|
+
readonly where?: ReadonlyArray<CleanedWhere> | undefined;
|
|
24
|
+
readonly select?: ReadonlyArray<string> | undefined;
|
|
25
|
+
readonly limit?: number | undefined;
|
|
26
|
+
readonly offset?: number | undefined;
|
|
27
|
+
readonly sortBy?:
|
|
28
|
+
| {
|
|
29
|
+
readonly direction: "asc" | "desc";
|
|
30
|
+
readonly field: string;
|
|
31
|
+
}
|
|
32
|
+
| undefined;
|
|
175
33
|
},
|
|
176
34
|
): DynamicRelation<Requirements> => {
|
|
177
35
|
let refined = relation;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { Where } from "better-auth";
|
|
2
|
+
import type { AdapterFactoryCustomizeAdapterCreator, CustomAdapter } from "better-auth/adapters";
|
|
3
|
+
import { Effect, Option } from "effect";
|
|
4
|
+
import type { DynamicRelation, refineRelation } from "./relation.ts";
|
|
5
|
+
|
|
6
|
+
type CleanedWhere = Required<Where>;
|
|
7
|
+
type FindOneInput = Parameters<CustomAdapter["findOne"]>[0];
|
|
8
|
+
type FindManyInput = Parameters<CustomAdapter["findMany"]>[0];
|
|
9
|
+
type Row = Record<string, unknown>;
|
|
10
|
+
type RowMethod = "create" | "findMany" | "findOne" | "update";
|
|
11
|
+
type Refinement = Parameters<typeof refineRelation>[1];
|
|
12
|
+
|
|
13
|
+
interface RowMethods {
|
|
14
|
+
readonly create: (input: { readonly data: Row; readonly model: string; readonly select?: string[] | undefined }) => Promise<Row>;
|
|
15
|
+
readonly findOne: (input: FindOneInput) => Promise<Row | null>;
|
|
16
|
+
readonly findMany: (input: FindManyInput) => Promise<ReadonlyArray<Row>>;
|
|
17
|
+
readonly update: (input: { readonly model: string; readonly update: Row; readonly where: CleanedWhere[] }) => Promise<Row | null>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type RelationQuery = <Value>(
|
|
21
|
+
model: string,
|
|
22
|
+
refinement: Refinement,
|
|
23
|
+
use: (relation: DynamicRelation<unknown>) => Effect.Effect<Value, unknown, unknown>,
|
|
24
|
+
) => Promise<Value>;
|
|
25
|
+
|
|
26
|
+
// Better Auth lets each call declare its row type; the rows are whatever its generated schema stored.
|
|
27
|
+
function rowTyped(adapter: Omit<CustomAdapter, RowMethod> & RowMethods): CustomAdapter;
|
|
28
|
+
function rowTyped(adapter: unknown): unknown {
|
|
29
|
+
return adapter;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const rejectJoin = (join: unknown): void => {
|
|
33
|
+
if (join !== undefined) {
|
|
34
|
+
throw new TypeError("The Effect Prisma Better Auth adapter does not support experimental native joins");
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const namesId = (condition: CleanedWhere): boolean => condition.connector !== "OR" && condition.field === "id" && condition.operator === "eq";
|
|
39
|
+
|
|
40
|
+
export const makeRowAdapter =
|
|
41
|
+
(query: RelationQuery, usePlural: boolean): AdapterFactoryCustomizeAdapterCreator =>
|
|
42
|
+
({ debugLog, getFieldAttributes }) => {
|
|
43
|
+
const isUnique = (model: string, condition: CleanedWhere): boolean =>
|
|
44
|
+
condition.connector !== "OR" &&
|
|
45
|
+
condition.operator === "eq" &&
|
|
46
|
+
condition.mode !== "insensitive" &&
|
|
47
|
+
(condition.field === "id" || getFieldAttributes({ field: condition.field, model }).unique === true);
|
|
48
|
+
|
|
49
|
+
return rowTyped({
|
|
50
|
+
create: ({ data, model, select }) => {
|
|
51
|
+
debugLog("create", { model });
|
|
52
|
+
return query(model, { select }, (relation) => relation.create(data));
|
|
53
|
+
},
|
|
54
|
+
findOne: ({ join, model, select, where }) => {
|
|
55
|
+
rejectJoin(join);
|
|
56
|
+
return query(model, { select, where }, (relation) => Effect.map(relation.first(), Option.getOrNull));
|
|
57
|
+
},
|
|
58
|
+
findMany: ({ join, limit, model, offset, select, sortBy, where }) => {
|
|
59
|
+
rejectJoin(join);
|
|
60
|
+
return query(model, { limit, offset, select, sortBy, where }, (relation) => relation);
|
|
61
|
+
},
|
|
62
|
+
update: ({ model, update, where }) => {
|
|
63
|
+
if (where.length === 0) return Promise.resolve(null);
|
|
64
|
+
const unique = where.some((condition) => isUnique(model, condition));
|
|
65
|
+
return query(model, { where }, (relation) =>
|
|
66
|
+
unique ? relation.update(update) : Effect.map(relation.updateAll(update), (rows) => rows[0] ?? null),
|
|
67
|
+
);
|
|
68
|
+
},
|
|
69
|
+
updateMany: ({ model, update, where }) => query(model, { where }, (relation) => Effect.map(relation.updateAll(update), (rows) => rows.length)),
|
|
70
|
+
delete: ({ model, where }) => {
|
|
71
|
+
const byId = where.some(namesId);
|
|
72
|
+
return query(model, { where }, (relation) => (byId ? Effect.asVoid(relation.delete()) : Effect.asVoid(relation.deleteAll())));
|
|
73
|
+
},
|
|
74
|
+
deleteMany: ({ model, where }) => query(model, { where }, (relation) => Effect.map(relation.deleteAll(), (rows) => rows.length)),
|
|
75
|
+
count: ({ model, where }) => query(model, { where }, (relation) => relation.count()),
|
|
76
|
+
options: { usePlural },
|
|
77
|
+
});
|
|
78
|
+
};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { all, and, not, or } from "@shivaedev/effect-prisma";
|
|
2
|
+
import type { Where } from "better-auth";
|
|
3
|
+
|
|
4
|
+
export type CleanedWhere = Required<Where>;
|
|
5
|
+
export type Expression = Parameters<typeof and>[number];
|
|
6
|
+
|
|
7
|
+
export interface DynamicField {
|
|
8
|
+
eq(value: unknown): Expression;
|
|
9
|
+
neq(value: unknown): Expression;
|
|
10
|
+
gt(value: unknown): Expression;
|
|
11
|
+
gte(value: unknown): Expression;
|
|
12
|
+
lt(value: unknown): Expression;
|
|
13
|
+
lte(value: unknown): Expression;
|
|
14
|
+
in(value: ReadonlyArray<unknown>): Expression;
|
|
15
|
+
notIn(value: ReadonlyArray<unknown>): Expression;
|
|
16
|
+
like(value: string): Expression;
|
|
17
|
+
ilike?(value: string): Expression;
|
|
18
|
+
isNull(): Expression;
|
|
19
|
+
isNotNull(): Expression;
|
|
20
|
+
asc(): unknown;
|
|
21
|
+
desc(): unknown;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const escapeLike = (value: string): string => value.replaceAll("\\", "\\\\").replaceAll("%", "\\%").replaceAll("_", "\\_");
|
|
25
|
+
|
|
26
|
+
const nonNullValues = (value: unknown): ReadonlyArray<unknown> => (Array.isArray(value) ? value : [value]).filter((item) => item !== null);
|
|
27
|
+
|
|
28
|
+
const insensitiveStrings = (field: DynamicField, where: CleanedWhere, values: ReadonlyArray<unknown>) => {
|
|
29
|
+
const ilike = field.ilike;
|
|
30
|
+
if (where.mode !== "insensitive" || ilike === undefined || !values.every((item) => typeof item === "string")) {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
return values.map((item) => ilike(escapeLike(String(item))));
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const membership = (field: DynamicField, where: CleanedWhere): Expression => {
|
|
37
|
+
const values = nonNullValues(where.value);
|
|
38
|
+
if (values.length === 0) {
|
|
39
|
+
return and(field.isNull(), field.isNotNull());
|
|
40
|
+
}
|
|
41
|
+
const matches = insensitiveStrings(field, where, values);
|
|
42
|
+
return matches === undefined ? field.in(values) : or(...matches);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const exclusion = (field: DynamicField, where: CleanedWhere): Expression | undefined => {
|
|
46
|
+
const values = nonNullValues(where.value);
|
|
47
|
+
if (values.length === 0) {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
const matches = insensitiveStrings(field, where, values);
|
|
51
|
+
return matches === undefined ? field.notIn(values) : and(...matches.map((match) => not(match)));
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const likePattern = (operator: CleanedWhere["operator"], escaped: string): string => {
|
|
55
|
+
if (operator === "contains") return `%${escaped}%`;
|
|
56
|
+
return operator === "starts_with" ? `${escaped}%` : `%${escaped}`;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const pattern = (field: DynamicField, where: CleanedWhere): Expression => {
|
|
60
|
+
if (typeof where.value !== "string") {
|
|
61
|
+
throw new TypeError(`${where.operator} requires a string value`);
|
|
62
|
+
}
|
|
63
|
+
const text = likePattern(where.operator, escapeLike(where.value));
|
|
64
|
+
return where.mode === "insensitive" && field.ilike !== undefined ? field.ilike(text) : field.like(text);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const insensitiveEquality = (field: DynamicField, where: CleanedWhere): Expression | undefined => {
|
|
68
|
+
const value = where.value;
|
|
69
|
+
if (where.mode !== "insensitive" || typeof value !== "string" || field.ilike === undefined) return undefined;
|
|
70
|
+
if (where.operator === "eq") return field.ilike(escapeLike(value));
|
|
71
|
+
if (where.operator === "ne") return not(field.ilike(escapeLike(value)));
|
|
72
|
+
return undefined;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const comparison = (field: DynamicField, where: CleanedWhere): Expression | undefined => {
|
|
76
|
+
const value = where.value;
|
|
77
|
+
switch (where.operator) {
|
|
78
|
+
case "eq":
|
|
79
|
+
return value === null ? field.isNull() : field.eq(value);
|
|
80
|
+
case "ne":
|
|
81
|
+
return value === null ? field.isNotNull() : field.neq(value);
|
|
82
|
+
case "lt":
|
|
83
|
+
return field.lt(value);
|
|
84
|
+
case "lte":
|
|
85
|
+
return field.lte(value);
|
|
86
|
+
case "gt":
|
|
87
|
+
return field.gt(value);
|
|
88
|
+
case "gte":
|
|
89
|
+
return field.gte(value);
|
|
90
|
+
case "in":
|
|
91
|
+
return membership(field, where);
|
|
92
|
+
case "not_in":
|
|
93
|
+
return exclusion(field, where);
|
|
94
|
+
case "contains":
|
|
95
|
+
case "starts_with":
|
|
96
|
+
case "ends_with":
|
|
97
|
+
return pattern(field, where);
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
export const whereExpression = (fields: Record<string, DynamicField>, where: ReadonlyArray<CleanedWhere>): Expression | undefined => {
|
|
102
|
+
const conjunctions: Expression[] = [];
|
|
103
|
+
const disjunctions: Expression[] = [];
|
|
104
|
+
|
|
105
|
+
for (const condition of where) {
|
|
106
|
+
const field = fields[condition.field];
|
|
107
|
+
if (field === undefined) {
|
|
108
|
+
throw new TypeError(`Unknown database field: ${condition.field}`);
|
|
109
|
+
}
|
|
110
|
+
const expression = insensitiveEquality(field, condition) ?? comparison(field, condition);
|
|
111
|
+
if (expression === undefined) continue;
|
|
112
|
+
(condition.connector === "OR" ? disjunctions : conjunctions).push(expression);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (disjunctions.length > 0) conjunctions.push(or(...disjunctions));
|
|
116
|
+
if (conjunctions.length === 0) return all();
|
|
117
|
+
return conjunctions.length === 1 ? conjunctions[0] : and(...conjunctions);
|
|
118
|
+
};
|
package/src/better-auth.ts
CHANGED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Option, Schema } from "effect";
|
|
2
|
+
|
|
3
|
+
export interface RejectedField {
|
|
4
|
+
readonly field: string;
|
|
5
|
+
readonly message: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const Rejected = Schema.Struct({ _tag: Schema.String, field: Schema.String, message: Schema.String });
|
|
9
|
+
|
|
10
|
+
const decode = Schema.decodeUnknownOption(Rejected);
|
|
11
|
+
|
|
12
|
+
export const rejectedField = (error: unknown): Option.Option<RejectedField> =>
|
|
13
|
+
Option.map(decode(error), ({ field, message }) => ({ field, message }));
|