@shivaedev/platform 0.3.7 → 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.
Files changed (124) hide show
  1. package/CHANGELOG.md +29 -1
  2. package/README.md +46 -0
  3. package/dist/better-auth/adapter.d.ts +2 -2
  4. package/dist/better-auth/adapter.d.ts.map +1 -1
  5. package/dist/better-auth/adapter.js +39 -95
  6. package/dist/better-auth/adapter.js.map +1 -1
  7. package/dist/better-auth/relation.d.ts +7 -27
  8. package/dist/better-auth/relation.d.ts.map +1 -1
  9. package/dist/better-auth/relation.js +2 -84
  10. package/dist/better-auth/relation.js.map +1 -1
  11. package/dist/better-auth/row-adapter.d.ts +8 -0
  12. package/dist/better-auth/row-adapter.d.ts.map +1 -0
  13. package/dist/better-auth/row-adapter.js +45 -0
  14. package/dist/better-auth/row-adapter.js.map +1 -0
  15. package/dist/better-auth/where.d.ts +22 -0
  16. package/dist/better-auth/where.d.ts.map +1 -0
  17. package/dist/better-auth/where.js +93 -0
  18. package/dist/better-auth/where.js.map +1 -0
  19. package/dist/better-auth.d.ts +1 -1
  20. package/dist/errors/rejected-field.d.ts +7 -0
  21. package/dist/errors/rejected-field.d.ts.map +1 -0
  22. package/dist/errors/rejected-field.js +5 -0
  23. package/dist/errors/rejected-field.js.map +1 -0
  24. package/dist/errors/taxonomy.d.ts +40 -0
  25. package/dist/errors/taxonomy.d.ts.map +1 -0
  26. package/dist/errors/taxonomy.js +18 -0
  27. package/dist/errors/taxonomy.js.map +1 -0
  28. package/dist/errors.d.ts +3 -0
  29. package/dist/errors.d.ts.map +1 -0
  30. package/dist/errors.js +3 -0
  31. package/dist/errors.js.map +1 -0
  32. package/dist/node-http/subscription-signal.d.ts.map +1 -1
  33. package/dist/node-http/subscription-signal.js +6 -1
  34. package/dist/node-http/subscription-signal.js.map +1 -1
  35. package/dist/node-http.d.ts +1 -1
  36. package/dist/rpc/identity.d.ts +15 -0
  37. package/dist/rpc/identity.d.ts.map +1 -0
  38. package/dist/rpc/identity.js +8 -0
  39. package/dist/rpc/identity.js.map +1 -0
  40. package/dist/rpc/middleware.d.ts +15 -0
  41. package/dist/rpc/middleware.d.ts.map +1 -0
  42. package/dist/rpc/middleware.js +12 -0
  43. package/dist/rpc/middleware.js.map +1 -0
  44. package/dist/rpc-server/adapters/better-auth-sessions.d.ts +4 -0
  45. package/dist/rpc-server/adapters/better-auth-sessions.d.ts.map +1 -0
  46. package/dist/rpc-server/adapters/better-auth-sessions.js +11 -0
  47. package/dist/rpc-server/adapters/better-auth-sessions.js.map +1 -0
  48. package/dist/rpc-server/origin.d.ts +15 -0
  49. package/dist/rpc-server/origin.d.ts.map +1 -0
  50. package/dist/rpc-server/origin.js +12 -0
  51. package/dist/rpc-server/origin.js.map +1 -0
  52. package/dist/rpc-server/redact-cause.d.ts +6 -0
  53. package/dist/rpc-server/redact-cause.d.ts.map +1 -0
  54. package/dist/rpc-server/redact-cause.js +28 -0
  55. package/dist/rpc-server/redact-cause.js.map +1 -0
  56. package/dist/rpc-server/redact.d.ts +3 -0
  57. package/dist/rpc-server/redact.d.ts.map +1 -0
  58. package/dist/rpc-server/redact.js +46 -0
  59. package/dist/rpc-server/redact.js.map +1 -0
  60. package/dist/rpc-server/sensitive.d.ts +5 -0
  61. package/dist/rpc-server/sensitive.d.ts.map +1 -0
  62. package/dist/rpc-server/sensitive.js +49 -0
  63. package/dist/rpc-server/sensitive.js.map +1 -0
  64. package/dist/rpc-server/session.d.ts +21 -0
  65. package/dist/rpc-server/session.d.ts.map +1 -0
  66. package/dist/rpc-server/session.js +28 -0
  67. package/dist/rpc-server/session.js.map +1 -0
  68. package/dist/rpc-server/tracing.d.ts +9 -0
  69. package/dist/rpc-server/tracing.d.ts.map +1 -0
  70. package/dist/rpc-server/tracing.js +32 -0
  71. package/dist/rpc-server/tracing.js.map +1 -0
  72. package/dist/rpc-server/transport.d.ts +4 -0
  73. package/dist/rpc-server/transport.d.ts.map +1 -0
  74. package/dist/rpc-server/transport.js +4 -0
  75. package/dist/rpc-server/transport.js.map +1 -0
  76. package/dist/rpc-server.d.ts +9 -0
  77. package/dist/rpc-server.d.ts.map +1 -0
  78. package/dist/rpc-server.js +9 -0
  79. package/dist/rpc-server.js.map +1 -0
  80. package/dist/rpc.d.ts +3 -0
  81. package/dist/rpc.d.ts.map +1 -0
  82. package/dist/rpc.js +3 -0
  83. package/dist/rpc.js.map +1 -0
  84. package/dist/runtime/make.d.ts +1 -1
  85. package/dist/runtime/make.d.ts.map +1 -1
  86. package/dist/runtime/make.js +10 -9
  87. package/dist/runtime/make.js.map +1 -1
  88. package/dist/runtime/types.d.ts.map +1 -1
  89. package/dist/runtime.d.ts +2 -2
  90. package/dist/testing/types.d.ts.map +1 -1
  91. package/dist/testing/vitest.d.ts +1 -1
  92. package/dist/testing/vitest.d.ts.map +1 -1
  93. package/dist/testing/vitest.js +7 -9
  94. package/dist/testing/vitest.js.map +1 -1
  95. package/dist/testing.d.ts +2 -2
  96. package/package.json +38 -6
  97. package/src/better-auth/adapter.ts +50 -190
  98. package/src/better-auth/relation.ts +21 -163
  99. package/src/better-auth/row-adapter.ts +78 -0
  100. package/src/better-auth/where.ts +118 -0
  101. package/src/better-auth.ts +1 -1
  102. package/src/errors/rejected-field.ts +13 -0
  103. package/src/errors/taxonomy.ts +18 -0
  104. package/src/errors.ts +2 -0
  105. package/src/node-http/subscription-signal.ts +10 -15
  106. package/src/node-http.ts +1 -1
  107. package/src/rpc/identity.ts +11 -0
  108. package/src/rpc/middleware.ts +15 -0
  109. package/src/rpc-server/adapters/better-auth-sessions.ts +22 -0
  110. package/src/rpc-server/origin.ts +29 -0
  111. package/src/rpc-server/redact-cause.ts +41 -0
  112. package/src/rpc-server/redact.ts +42 -0
  113. package/src/rpc-server/sensitive.ts +60 -0
  114. package/src/rpc-server/session.ts +54 -0
  115. package/src/rpc-server/tracing.ts +56 -0
  116. package/src/rpc-server/transport.ts +8 -0
  117. package/src/rpc-server.ts +15 -0
  118. package/src/rpc.ts +2 -0
  119. package/src/runtime/make.ts +20 -27
  120. package/src/runtime/types.ts +3 -12
  121. package/src/runtime.ts +2 -2
  122. package/src/testing/types.ts +7 -32
  123. package/src/testing/vitest.ts +59 -89
  124. package/src/testing.ts +2 -2
@@ -1,18 +1,10 @@
1
- import type { AnyDatabase, DatabaseServiceOf } from "@shivaedev/effect-prisma";
2
- import type { BetterAuthOptions, DBAdapterInstance, Where } from "better-auth";
3
- import { type CustomAdapter, createAdapterFactory } from "better-auth/adapters";
4
- import { Effect, Option } from "effect";
5
- import type { PlatformRuntime } from "../runtime/types.js";
6
- import { type DynamicRelation, refineRelation } from "./relation.js";
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<Requirements> {
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 modelRelation = <Requirements>(
32
- database: DynamicDatabase<Requirements>,
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 defaultModelName = (model: string): string =>
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 rejectJoin = (join: unknown): void => {
42
- if (join !== undefined) {
43
- throw new TypeError(
44
- "The Effect Prisma Better Auth adapter does not support experimental native joins",
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
- export const effectPrismaAdapter =
50
- <Database extends AnyDatabase, Services, BuildError>(
51
- databaseTag: Database,
52
- runtime: PlatformRuntime<Services, BuildError>,
53
- adapterOptions: EffectPrismaAdapterOptions = {},
54
- ): DBAdapterInstance =>
55
- (authOptions: BetterAuthOptions) => {
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 as Effect.Effect<
58
- DatabaseServiceOf<Database>,
59
- never,
60
- Services
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 run = <Value>(
64
- operation: (
65
- database: DynamicDatabase<Services>,
66
- ) => Effect.Effect<Value, unknown, Services>,
67
- ): Promise<Value> =>
68
- runtime.runPromise(
69
- Effect.flatMap(databaseEffect, (database) =>
70
- operation(database as DynamicDatabase<Services>),
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: ({ debugLog, getFieldAttributes }) => ({
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, and, not, or } from "@shivaedev/effect-prisma";
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
- type CleanedWhere = Required<Where>;
6
- type Expression = Parameters<typeof and>[number];
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
- data: Record<string, unknown>,
43
- ): Effect.Effect<Record<string, unknown>, unknown, Requirements>;
44
- update(
45
- data: Record<string, unknown>,
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
- readonly direction: "asc" | "desc";
173
- readonly field: string;
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
+ };
@@ -1,4 +1,4 @@
1
1
  export {
2
2
  type EffectPrismaAdapterOptions,
3
3
  effectPrismaAdapter,
4
- } from "./better-auth/adapter.js";
4
+ } from "./better-auth/adapter.ts";
@@ -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 }));