@shivaedev/platform 0.6.0 → 0.7.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 (204) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +371 -112
  3. package/dist/errors/rejectedField.d.ts +7 -0
  4. package/dist/errors/rejectedField.d.ts.map +1 -0
  5. package/dist/errors/{rejected-field.js → rejectedField.js} +4 -2
  6. package/dist/errors/rejectedField.js.map +1 -0
  7. package/dist/errors/taxonomy.d.ts +10 -0
  8. package/dist/errors/taxonomy.d.ts.map +1 -1
  9. package/dist/errors/taxonomy.js +4 -0
  10. package/dist/errors/taxonomy.js.map +1 -1
  11. package/dist/rpc/identity.d.ts.map +1 -1
  12. package/dist/rpc/identity.js.map +1 -1
  13. package/dist/rpc/middleware.d.ts +1 -1
  14. package/dist/rpc/middleware.d.ts.map +1 -1
  15. package/dist/rpc/middleware.js +1 -1
  16. package/dist/rpc/middleware.js.map +1 -1
  17. package/dist/server/appHost.d.ts +61 -0
  18. package/dist/server/appHost.d.ts.map +1 -0
  19. package/dist/server/appHost.js +14 -0
  20. package/dist/server/appHost.js.map +1 -0
  21. package/dist/server/appHostHttp.d.ts +11 -0
  22. package/dist/server/appHostHttp.d.ts.map +1 -0
  23. package/dist/server/appHostHttp.js +51 -0
  24. package/dist/server/appHostHttp.js.map +1 -0
  25. package/dist/{better-auth → server/better-auth}/adapter.d.ts +1 -1
  26. package/dist/server/better-auth/adapter.d.ts.map +1 -0
  27. package/dist/{better-auth → server/better-auth}/adapter.js +26 -14
  28. package/dist/server/better-auth/adapter.js.map +1 -0
  29. package/dist/{better-auth → server/better-auth}/relation.d.ts +15 -15
  30. package/dist/server/better-auth/relation.d.ts.map +1 -0
  31. package/dist/{better-auth → server/better-auth}/relation.js +2 -2
  32. package/dist/server/better-auth/relation.js.map +1 -0
  33. package/dist/server/better-auth/rowAdapter.d.ts +8 -0
  34. package/dist/server/better-auth/rowAdapter.d.ts.map +1 -0
  35. package/dist/server/better-auth/rowAdapter.js +52 -0
  36. package/dist/server/better-auth/rowAdapter.js.map +1 -0
  37. package/dist/{better-auth → server/better-auth}/where.d.ts +1 -1
  38. package/dist/server/better-auth/where.d.ts.map +1 -0
  39. package/dist/{better-auth → server/better-auth}/where.js +22 -18
  40. package/dist/server/better-auth/where.js.map +1 -0
  41. package/dist/server/boundedRpcRequest.d.ts +3 -0
  42. package/dist/server/boundedRpcRequest.d.ts.map +1 -0
  43. package/dist/server/boundedRpcRequest.js +41 -0
  44. package/dist/server/boundedRpcRequest.js.map +1 -0
  45. package/dist/server/databaseLayer.d.ts +6 -0
  46. package/dist/server/databaseLayer.d.ts.map +1 -0
  47. package/dist/server/databaseLayer.js +29 -0
  48. package/dist/server/databaseLayer.js.map +1 -0
  49. package/dist/server/hostOptions.d.ts +28 -0
  50. package/dist/server/hostOptions.d.ts.map +1 -0
  51. package/dist/server/hostOptions.js +2 -0
  52. package/dist/server/hostOptions.js.map +1 -0
  53. package/dist/{node-http/subscription-signal.d.ts → server/node-http/subscriptionSignal.d.ts} +2 -2
  54. package/dist/server/node-http/subscriptionSignal.d.ts.map +1 -0
  55. package/dist/{node-http/subscription-signal.js → server/node-http/subscriptionSignal.js} +20 -13
  56. package/dist/server/node-http/subscriptionSignal.js.map +1 -0
  57. package/dist/server/rpc-server/adapters/betterAuthSessions.d.ts +4 -0
  58. package/dist/server/rpc-server/adapters/betterAuthSessions.d.ts.map +1 -0
  59. package/dist/server/rpc-server/adapters/betterAuthSessions.js +14 -0
  60. package/dist/server/rpc-server/adapters/betterAuthSessions.js.map +1 -0
  61. package/dist/server/rpc-server/boundary.d.ts +9 -0
  62. package/dist/server/rpc-server/boundary.d.ts.map +1 -0
  63. package/dist/server/rpc-server/boundary.js +18 -0
  64. package/dist/server/rpc-server/boundary.js.map +1 -0
  65. package/dist/server/rpc-server/local.d.ts +7 -0
  66. package/dist/server/rpc-server/local.d.ts.map +1 -0
  67. package/dist/server/rpc-server/local.js +70 -0
  68. package/dist/server/rpc-server/local.js.map +1 -0
  69. package/dist/{rpc-server → server/rpc-server}/origin.d.ts +3 -3
  70. package/dist/server/rpc-server/origin.d.ts.map +1 -0
  71. package/dist/server/rpc-server/origin.js +16 -0
  72. package/dist/server/rpc-server/origin.js.map +1 -0
  73. package/dist/server/rpc-server/redact.d.ts +3 -0
  74. package/dist/server/rpc-server/redact.d.ts.map +1 -0
  75. package/dist/{rpc-server → server/rpc-server}/redact.js +9 -7
  76. package/dist/server/rpc-server/redact.js.map +1 -0
  77. package/dist/server/rpc-server/redactCause.d.ts +6 -0
  78. package/dist/server/rpc-server/redactCause.d.ts.map +1 -0
  79. package/dist/server/rpc-server/redactCause.js +36 -0
  80. package/dist/server/rpc-server/redactCause.js.map +1 -0
  81. package/dist/server/rpc-server/sensitive.d.ts +5 -0
  82. package/dist/server/rpc-server/sensitive.d.ts.map +1 -0
  83. package/dist/{rpc-server → server/rpc-server}/sensitive.js +21 -17
  84. package/dist/server/rpc-server/sensitive.js.map +1 -0
  85. package/dist/server/rpc-server/session.d.ts +21 -0
  86. package/dist/server/rpc-server/session.d.ts.map +1 -0
  87. package/dist/server/rpc-server/session.js +37 -0
  88. package/dist/server/rpc-server/session.js.map +1 -0
  89. package/dist/{rpc-server → server/rpc-server}/tracing.d.ts +1 -1
  90. package/dist/server/rpc-server/tracing.d.ts.map +1 -0
  91. package/dist/server/rpc-server/tracing.js +42 -0
  92. package/dist/server/rpc-server/tracing.js.map +1 -0
  93. package/dist/server/rpc-server/transportHeaders.d.ts +4 -0
  94. package/dist/server/rpc-server/transportHeaders.d.ts.map +1 -0
  95. package/dist/server/rpc-server/transportHeaders.js +6 -0
  96. package/dist/server/rpc-server/transportHeaders.js.map +1 -0
  97. package/dist/server/rpc-server/trustedProxy.d.ts +6 -0
  98. package/dist/server/rpc-server/trustedProxy.d.ts.map +1 -0
  99. package/dist/server/rpc-server/trustedProxy.js +35 -0
  100. package/dist/server/rpc-server/trustedProxy.js.map +1 -0
  101. package/dist/server/runtime/makePlatform.d.ts +4 -0
  102. package/dist/server/runtime/makePlatform.d.ts.map +1 -0
  103. package/dist/{runtime/make.js → server/runtime/makePlatform.js} +11 -9
  104. package/dist/server/runtime/makePlatform.js.map +1 -0
  105. package/dist/server/runtime/types.d.ts +19 -0
  106. package/dist/server/runtime/types.d.ts.map +1 -0
  107. package/dist/{runtime → server/runtime}/types.js.map +1 -1
  108. package/dist/server/testing/makePlatformIt.d.ts +8 -0
  109. package/dist/server/testing/makePlatformIt.d.ts.map +1 -0
  110. package/dist/{testing/vitest.js → server/testing/makePlatformIt.js} +7 -3
  111. package/dist/server/testing/makePlatformIt.js.map +1 -0
  112. package/dist/server/testing/types.d.ts +28 -0
  113. package/dist/server/testing/types.d.ts.map +1 -0
  114. package/dist/{testing → server/testing}/types.js.map +1 -1
  115. package/docs/north-star.md +36 -0
  116. package/docs/roadmap.md +30 -0
  117. package/package.json +333 -32
  118. package/src/errors/{rejected-field.ts → rejectedField.ts} +3 -6
  119. package/src/errors/taxonomy.ts +2 -8
  120. package/src/rpc/identity.ts +0 -4
  121. package/src/rpc/middleware.ts +19 -10
  122. package/src/server/appHost.ts +130 -0
  123. package/src/server/appHostHttp.ts +101 -0
  124. package/src/{better-auth → server/better-auth}/adapter.ts +26 -30
  125. package/src/{better-auth → server/better-auth}/relation.ts +17 -19
  126. package/src/{better-auth/row-adapter.ts → server/better-auth/rowAdapter.ts} +19 -21
  127. package/src/{better-auth → server/better-auth}/where.ts +22 -32
  128. package/src/server/boundedRpcRequest.ts +47 -0
  129. package/src/server/databaseLayer.ts +36 -0
  130. package/src/server/hostOptions.ts +25 -0
  131. package/src/{node-http/subscription-signal.ts → server/node-http/subscriptionSignal.ts} +19 -22
  132. package/src/{rpc-server/adapters/better-auth-sessions.ts → server/rpc-server/adapters/betterAuthSessions.ts} +10 -11
  133. package/src/server/rpc-server/boundary.ts +26 -0
  134. package/src/server/rpc-server/local.ts +72 -0
  135. package/src/{rpc-server → server/rpc-server}/origin.ts +11 -14
  136. package/src/{rpc-server → server/rpc-server}/redact.ts +9 -11
  137. package/src/server/rpc-server/redactCause.ts +40 -0
  138. package/src/{rpc-server → server/rpc-server}/sensitive.ts +17 -22
  139. package/src/{rpc-server → server/rpc-server}/session.ts +23 -24
  140. package/src/{rpc-server → server/rpc-server}/tracing.ts +16 -19
  141. package/src/{rpc-server/transport.ts → server/rpc-server/transportHeaders.ts} +4 -4
  142. package/src/server/rpc-server/trustedProxy.ts +36 -0
  143. package/src/{runtime/make.ts → server/runtime/makePlatform.ts} +19 -27
  144. package/src/server/runtime/types.ts +24 -0
  145. package/src/{testing/vitest.ts → server/testing/makePlatformIt.ts} +27 -23
  146. package/src/server/testing/types.ts +38 -0
  147. package/dist/better-auth/adapter.d.ts.map +0 -1
  148. package/dist/better-auth/adapter.js.map +0 -1
  149. package/dist/better-auth/relation.d.ts.map +0 -1
  150. package/dist/better-auth/relation.js.map +0 -1
  151. package/dist/better-auth/row-adapter.d.ts +0 -8
  152. package/dist/better-auth/row-adapter.d.ts.map +0 -1
  153. package/dist/better-auth/row-adapter.js +0 -46
  154. package/dist/better-auth/row-adapter.js.map +0 -1
  155. package/dist/better-auth/where.d.ts.map +0 -1
  156. package/dist/better-auth/where.js.map +0 -1
  157. package/dist/errors/rejected-field.d.ts +0 -7
  158. package/dist/errors/rejected-field.d.ts.map +0 -1
  159. package/dist/errors/rejected-field.js.map +0 -1
  160. package/dist/node-http/subscription-signal.d.ts.map +0 -1
  161. package/dist/node-http/subscription-signal.js.map +0 -1
  162. package/dist/rpc-server/adapters/better-auth-sessions.d.ts +0 -4
  163. package/dist/rpc-server/adapters/better-auth-sessions.d.ts.map +0 -1
  164. package/dist/rpc-server/adapters/better-auth-sessions.js +0 -11
  165. package/dist/rpc-server/adapters/better-auth-sessions.js.map +0 -1
  166. package/dist/rpc-server/origin.d.ts.map +0 -1
  167. package/dist/rpc-server/origin.js +0 -12
  168. package/dist/rpc-server/origin.js.map +0 -1
  169. package/dist/rpc-server/redact-cause.d.ts +0 -6
  170. package/dist/rpc-server/redact-cause.d.ts.map +0 -1
  171. package/dist/rpc-server/redact-cause.js +0 -28
  172. package/dist/rpc-server/redact-cause.js.map +0 -1
  173. package/dist/rpc-server/redact.d.ts +0 -3
  174. package/dist/rpc-server/redact.d.ts.map +0 -1
  175. package/dist/rpc-server/redact.js.map +0 -1
  176. package/dist/rpc-server/sensitive.d.ts +0 -5
  177. package/dist/rpc-server/sensitive.d.ts.map +0 -1
  178. package/dist/rpc-server/sensitive.js.map +0 -1
  179. package/dist/rpc-server/session.d.ts +0 -21
  180. package/dist/rpc-server/session.d.ts.map +0 -1
  181. package/dist/rpc-server/session.js +0 -31
  182. package/dist/rpc-server/session.js.map +0 -1
  183. package/dist/rpc-server/tracing.d.ts.map +0 -1
  184. package/dist/rpc-server/tracing.js +0 -34
  185. package/dist/rpc-server/tracing.js.map +0 -1
  186. package/dist/rpc-server/transport.d.ts +0 -4
  187. package/dist/rpc-server/transport.d.ts.map +0 -1
  188. package/dist/rpc-server/transport.js +0 -4
  189. package/dist/rpc-server/transport.js.map +0 -1
  190. package/dist/runtime/make.d.ts +0 -4
  191. package/dist/runtime/make.d.ts.map +0 -1
  192. package/dist/runtime/make.js.map +0 -1
  193. package/dist/runtime/types.d.ts +0 -19
  194. package/dist/runtime/types.d.ts.map +0 -1
  195. package/dist/testing/types.d.ts +0 -28
  196. package/dist/testing/types.d.ts.map +0 -1
  197. package/dist/testing/vitest.d.ts +0 -8
  198. package/dist/testing/vitest.d.ts.map +0 -1
  199. package/dist/testing/vitest.js.map +0 -1
  200. package/src/rpc-server/redact-cause.ts +0 -41
  201. package/src/runtime/types.ts +0 -24
  202. package/src/testing/types.ts +0 -43
  203. /package/dist/{runtime → server/runtime}/types.js +0 -0
  204. /package/dist/{testing → server/testing}/types.js +0 -0
@@ -0,0 +1,101 @@
1
+ import { createServer } from "node:http";
2
+ import { NodeHttpServer, NodeServices } from "@effect/platform-node";
3
+ import { ByteSize, Config, Effect, Layer, Option, Schema } from "effect";
4
+ import { HttpIncomingMessage, HttpRouter, HttpServerRequest, HttpStaticServer } from "effect/http";
5
+ import { type Rpc, RpcSerialization, RpcServer } from "effect/rpc";
6
+ import { localRpcGuard } from "#rpc-server/local.ts";
7
+ import { authenticatedLayer, maybeAuthenticatedLayer, type SessionShape } from "#rpc-server/session.ts";
8
+ import { requestTracingLayer } from "#rpc-server/tracing.ts";
9
+ import { TrustedProxy } from "#rpc-server/trustedProxy.ts";
10
+ import type { AppHostOptions, AppLocalHostOptions } from "./appHost.ts";
11
+ import { boundedRpcRequest } from "./boundedRpcRequest.ts";
12
+ import type { databaseLayer } from "./databaseLayer.ts";
13
+ import type { HostOptions, HostPolicy } from "./hostOptions.ts";
14
+ export function appHostHttp<
15
+ TRpcs extends Rpc.Any,
16
+ THandlers,
17
+ THandlerError,
18
+ THandlerRequirements,
19
+ TServices,
20
+ TServiceError,
21
+ TServiceRequirements,
22
+ TSession extends SessionShape,
23
+ >(
24
+ options: HostOptions<TRpcs, THandlers, THandlerError, THandlerRequirements, TServices, TServiceError, TServiceRequirements> & HostPolicy<TSession>,
25
+ ) {
26
+ return Layer.unwrap(
27
+ Effect.gen(function* () {
28
+ const maxBodySize = options.config?.maxBodySize ?? (yield* Config.Int("MAX_BODY_SIZE").pipe(Config.withDefault(1024 * 1024)));
29
+ const bodySize = yield* Schema.decodeUnknownEffect(Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)))(maxBodySize).pipe(
30
+ Effect.mapError((cause) => new Config.ConfigError(cause)),
31
+ );
32
+ const trustedProxy =
33
+ options.policy === "loopback"
34
+ ? undefined
35
+ : (options.config?.trustedProxy ?? Option.getOrUndefined(yield* Config.String("TRUSTED_PROXY").pipe(Config.option)));
36
+ if (trustedProxy !== undefined) {
37
+ yield* Schema.decodeUnknownEffect(TrustedProxy)(trustedProxy).pipe(Effect.mapError((cause) => new Config.ConfigError(cause)));
38
+ }
39
+ const middleware =
40
+ options.policy === "loopback"
41
+ ? requestTracingLayer()
42
+ : Layer.mergeAll(authenticatedLayer(options.sessions), maybeAuthenticatedLayer(options.sessions), requestTracingLayer());
43
+ const routes = HttpRouter.use((router) =>
44
+ Effect.gen(function* () {
45
+ const wire = yield* RpcSerialization.RpcSerialization;
46
+ const handler = yield* RpcServer.toHttpEffect(options.group);
47
+ const guarded = localRpcGuard(handler, trustedProxy);
48
+ yield* router.add("POST", options.rpcPath ?? "/rpc", (request) =>
49
+ guarded.pipe(
50
+ Effect.provideService(HttpIncomingMessage.MaxBodySize, ByteSize.bytes(bodySize)),
51
+ Effect.provideService(HttpServerRequest.HttpServerRequest, boundedRpcRequest(request)),
52
+ Effect.provideService(RpcSerialization.RpcSerialization, wire),
53
+ ),
54
+ );
55
+ }),
56
+ ).pipe(Layer.provide(Layer.merge(options.handlers, middleware)), Layer.provide(RpcSerialization.layerNdjson));
57
+ const assets =
58
+ options.assets === undefined
59
+ ? Layer.empty
60
+ : HttpStaticServer.layer(typeof options.assets === "string" ? { root: options.assets } : options.assets);
61
+ const application = options.services;
62
+ const server = Layer.unwrap(
63
+ Effect.gen(function* () {
64
+ const port = options.config?.port ?? (yield* Config.Int("PORT").pipe(Config.withDefault(3000)));
65
+ const host = options.config?.host ?? (yield* Config.String("HOST").pipe(Config.withDefault("127.0.0.1")));
66
+ return NodeHttpServer.layer(createServer, {
67
+ host,
68
+ port,
69
+ ...(options.config?.disablePreemptiveShutdown === undefined
70
+ ? {}
71
+ : { disablePreemptiveShutdown: options.config.disablePreemptiveShutdown }),
72
+ });
73
+ }),
74
+ );
75
+ return HttpRouter.serve(Layer.mergeAll(routes, assets, options.routes ?? Layer.empty), { disableLogger: true }).pipe(
76
+ Layer.provideMerge(application),
77
+ Layer.provideMerge(server),
78
+ Layer.provide(NodeServices.layer),
79
+ );
80
+ }),
81
+ );
82
+ }
83
+
84
+ export function migratedHost<
85
+ TRpcs extends Rpc.Any,
86
+ THandlers,
87
+ THandlerError,
88
+ THandlerRequirements,
89
+ TServices,
90
+ TServiceError,
91
+ TServiceRequirements,
92
+ TMigrations,
93
+ TSession extends SessionShape,
94
+ >(
95
+ options:
96
+ | AppHostOptions<TRpcs, THandlers, THandlerError, THandlerRequirements, TServices, TServiceError, TServiceRequirements, TMigrations, TSession>
97
+ | AppLocalHostOptions<TRpcs, THandlers, THandlerError, THandlerRequirements, TServices, TServiceError, TServiceRequirements, TMigrations>,
98
+ database: ReturnType<typeof databaseLayer<TMigrations>>,
99
+ ) {
100
+ return appHostHttp({ ...options, services: options.services.pipe(Layer.provideMerge(database)) });
101
+ }
@@ -4,38 +4,37 @@ import { Effect } from "effect";
4
4
  import type { AnyDatabase } from "@shivaedev/effect-prisma/databaseTypes.ts";
5
5
  import type { PlatformRuntime } from "#runtime/types.ts";
6
6
  import { type DynamicRelation, refineRelation } from "./relation.ts";
7
- import { makeRowAdapter, type RelationQuery } from "./row-adapter.ts";
8
-
7
+ import { makeRowAdapter, type RelationQuery } from "./rowAdapter.ts";
9
8
  export interface EffectPrismaAdapterOptions {
10
9
  readonly debugLogs?: boolean;
11
10
  readonly modelName?: (model: string) => string;
12
11
  readonly usePlural?: boolean;
13
12
  }
14
-
15
13
  interface DynamicDatabase {
16
14
  readonly transaction: <A, E, R>(program: Effect.Effect<A, E, R>) => Effect.Effect<A, unknown, unknown>;
17
15
  }
18
-
19
- const isFunction = (value: unknown, key: string): boolean =>
20
- ((typeof value === "object" && value !== null) || typeof value === "function") && typeof Reflect.get(value, key) === "function";
21
-
22
- const isDynamicDatabase = (value: unknown): value is DynamicDatabase => isFunction(value, "transaction");
23
-
24
- const isDynamicRelation = (value: unknown): value is DynamicRelation<unknown> => isFunction(value, "where") && isFunction(value, "select");
25
-
26
- const modelRelation = (database: DynamicDatabase, model: string): DynamicRelation<unknown> => {
16
+ function isFunction(value: unknown, key: string): boolean {
17
+ return ((typeof value === "object" && value !== null) || typeof value === "function") && typeof Reflect.get(value, key) === "function";
18
+ }
19
+ function isDynamicDatabase(value: unknown): value is DynamicDatabase {
20
+ return isFunction(value, "transaction");
21
+ }
22
+ function isDynamicRelation(value: unknown): value is DynamicRelation<unknown> {
23
+ return isFunction(value, "where") && isFunction(value, "select");
24
+ }
25
+ function modelRelation(database: DynamicDatabase, model: string): DynamicRelation<unknown> {
27
26
  const relation: unknown = Reflect.get(database, model);
28
27
  if (!isDynamicRelation(relation)) {
29
28
  throw new TypeError(`Unknown database model: ${model}`);
30
29
  }
31
30
  return relation;
32
- };
33
-
34
- const defaultModelName = (model: string): string => (model.length === 0 ? model : `${model[0]?.toUpperCase()}${model.slice(1)}`);
35
-
36
- export function effectPrismaAdapter<Database extends AnyDatabase, Services, BuildError>(
37
- databaseTag: Database,
38
- runtime: PlatformRuntime<Services, BuildError>,
31
+ }
32
+ function defaultModelName(model: string): string {
33
+ return model.length === 0 ? model : `${model[0]?.toUpperCase()}${model.slice(1)}`;
34
+ }
35
+ export function effectPrismaAdapter<TDatabase extends AnyDatabase, TServices, TBuildError>(
36
+ databaseTag: TDatabase,
37
+ runtime: PlatformRuntime<TServices, TBuildError>,
39
38
  adapterOptions?: EffectPrismaAdapterOptions,
40
39
  ): DBAdapterInstance;
41
40
  export function effectPrismaAdapter(
@@ -48,21 +47,19 @@ export function effectPrismaAdapter(
48
47
  const databaseEffect = Effect.flatMap(databaseTag, (database) =>
49
48
  isDynamicDatabase(database) ? Effect.succeed(database) : Effect.die(new TypeError("The database service has no transaction")),
50
49
  );
51
-
52
- const run = <Value>(operation: (database: DynamicDatabase) => Effect.Effect<Value, unknown, unknown>): Promise<Value> =>
53
- runtime.runPromise(Effect.flatMap(databaseEffect, operation));
54
-
50
+ function run<TValue>(operation: (database: DynamicDatabase) => Effect.Effect<TValue, unknown, unknown>): Promise<TValue> {
51
+ return runtime.runPromise(Effect.flatMap(databaseEffect, operation));
52
+ }
55
53
  const query: RelationQuery = (model, refinement, use) =>
56
54
  run((database) => use(refineRelation(modelRelation(database, mapModelName(model)), refinement)));
57
-
58
- const inTransaction = <Value>(callback: () => Promise<Value>): Effect.Effect<Value, unknown, unknown> =>
59
- Effect.flatMap(Effect.context<unknown>(), (services) =>
55
+ function inTransaction<TValue>(callback: () => Promise<TValue>): Effect.Effect<TValue, unknown, unknown> {
56
+ return Effect.flatMap(Effect.context<unknown>(), (services) =>
60
57
  Effect.tryPromise({
61
58
  catch: (error) => error,
62
59
  try: () => runtime.runWithServices(services, callback),
63
60
  }),
64
61
  );
65
-
62
+ }
66
63
  let factory: ReturnType<typeof createAdapterFactory>;
67
64
  factory = createAdapterFactory({
68
65
  adapter: makeRowAdapter(query, adapterOptions.usePlural ?? false),
@@ -73,13 +70,12 @@ export function effectPrismaAdapter(
73
70
  supportsArrays: true,
74
71
  supportsBooleans: true,
75
72
  supportsDates: true,
76
- supportsJSON: true,
73
+ "supportsJSON": true,
77
74
  supportsNumericIds: true,
78
- supportsUUIDs: true,
75
+ "supportsUUIDs": true,
79
76
  transaction: (callback) => run((database) => database.transaction(inTransaction(() => callback(factory(authOptions))))),
80
77
  },
81
78
  });
82
-
83
79
  return factory(authOptions);
84
80
  };
85
81
  }
@@ -1,24 +1,22 @@
1
1
  import { all } from "@prisma-next/sql-orm-client";
2
2
  import type { Effect, Option } from "effect";
3
3
  import { type CleanedWhere, type DynamicField, type Expression, whereExpression } from "./where.ts";
4
-
5
- export interface DynamicRelation<Requirements> extends Effect.Effect<readonly Record<string, unknown>[], unknown, Requirements> {
6
- count: () => Effect.Effect<number, unknown, Requirements>;
7
- create: (data: Record<string, unknown>) => Effect.Effect<Record<string, unknown>, unknown, Requirements>;
8
- delete: () => Effect.Effect<Record<string, unknown> | null, unknown, Requirements>;
9
- deleteAll: () => Effect.Effect<readonly Record<string, unknown>[], unknown, Requirements>;
10
- first: () => Effect.Effect<Option.Option<Record<string, unknown>>, unknown, Requirements>;
11
- orderBy: (ordering: (fields: Record<string, DynamicField>) => unknown) => DynamicRelation<Requirements>;
12
- select: (...fields: readonly string[]) => DynamicRelation<Requirements>;
13
- skip: (count: number) => DynamicRelation<Requirements>;
14
- take: (count: number) => DynamicRelation<Requirements>;
15
- update: (data: Record<string, unknown>) => Effect.Effect<Record<string, unknown> | null, unknown, Requirements>;
16
- updateAll: (data: Record<string, unknown>) => Effect.Effect<readonly Record<string, unknown>[], unknown, Requirements>;
17
- where: (predicate: (fields: Record<string, DynamicField>) => Expression) => DynamicRelation<Requirements>;
4
+ export interface DynamicRelation<TRequirements> extends Effect.Effect<readonly Record<string, unknown>[], unknown, TRequirements> {
5
+ count: () => Effect.Effect<number, unknown, TRequirements>;
6
+ create: (data: Record<string, unknown>) => Effect.Effect<Record<string, unknown>, unknown, TRequirements>;
7
+ delete: () => Effect.Effect<Record<string, unknown> | null, unknown, TRequirements>;
8
+ deleteAll: () => Effect.Effect<readonly Record<string, unknown>[], unknown, TRequirements>;
9
+ first: () => Effect.Effect<Option.Option<Record<string, unknown>>, unknown, TRequirements>;
10
+ orderBy: (ordering: (fields: Record<string, DynamicField>) => unknown) => DynamicRelation<TRequirements>;
11
+ select: (...fields: readonly string[]) => DynamicRelation<TRequirements>;
12
+ skip: (count: number) => DynamicRelation<TRequirements>;
13
+ take: (count: number) => DynamicRelation<TRequirements>;
14
+ update: (data: Record<string, unknown>) => Effect.Effect<Record<string, unknown> | null, unknown, TRequirements>;
15
+ updateAll: (data: Record<string, unknown>) => Effect.Effect<readonly Record<string, unknown>[], unknown, TRequirements>;
16
+ where: (predicate: (fields: Record<string, DynamicField>) => Expression) => DynamicRelation<TRequirements>;
18
17
  }
19
-
20
- export const refineRelation = <Requirements>(
21
- relation: DynamicRelation<Requirements>,
18
+ export function refineRelation<TRequirements>(
19
+ relation: DynamicRelation<TRequirements>,
22
20
  options: {
23
21
  readonly where?: readonly CleanedWhere[] | undefined;
24
22
  readonly select?: readonly string[] | undefined;
@@ -31,7 +29,7 @@ export const refineRelation = <Requirements>(
31
29
  }
32
30
  | undefined;
33
31
  },
34
- ): DynamicRelation<Requirements> => {
32
+ ): DynamicRelation<TRequirements> {
35
33
  let refined = relation;
36
34
  if (options.where !== undefined) {
37
35
  refined = refined.where((fields) => {
@@ -58,4 +56,4 @@ export const refineRelation = <Requirements>(
58
56
  refined = refined.select(...options.select);
59
57
  }
60
58
  return refined;
61
- };
59
+ }
@@ -9,43 +9,40 @@ type FindManyInput = Parameters<CustomAdapter["findMany"]>[0];
9
9
  type Row = Record<string, unknown>;
10
10
  type RowMethod = "create" | "findMany" | "findOne" | "update";
11
11
  type Refinement = Parameters<typeof refineRelation>[1];
12
-
13
12
  interface RowMethods {
14
13
  readonly create: (input: { readonly data: Row; readonly model: string; readonly select?: string[] | undefined }) => Promise<Row>;
15
14
  readonly findMany: (input: FindManyInput) => Promise<readonly Row[]>;
16
15
  readonly findOne: (input: FindOneInput) => Promise<Row | null>;
17
16
  readonly update: (input: { readonly model: string; readonly update: Row; readonly where: CleanedWhere[] }) => Promise<Row | null>;
18
17
  }
19
-
20
- export type RelationQuery = <Value>(
18
+ export type RelationQuery = <TValue>(
21
19
  model: string,
22
20
  refinement: Refinement,
23
- use: (relation: DynamicRelation<unknown>) => Effect.Effect<Value, unknown, unknown>,
24
- ) => Promise<Value>;
25
-
21
+ use: (relation: DynamicRelation<unknown>) => Effect.Effect<TValue, unknown, unknown>,
22
+ ) => Promise<TValue>;
26
23
  // Better Auth lets each call declare its row type; the rows are whatever its generated schema stored.
27
24
  function rowTyped(adapter: Omit<CustomAdapter, RowMethod> & RowMethods): CustomAdapter;
28
25
  function rowTyped(adapter: unknown): unknown {
29
26
  return adapter;
30
27
  }
31
-
32
- const rejectJoin = (join: unknown): void => {
28
+ function rejectJoin(join: unknown): void {
33
29
  if (join !== undefined) {
34
30
  throw new TypeError("The Effect Prisma Better Auth adapter does not support experimental native joins");
35
31
  }
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
-
32
+ }
33
+ function namesId(condition: CleanedWhere): boolean {
34
+ return condition.connector !== "OR" && condition.field === "id" && condition.operator === "eq";
35
+ }
36
+ export function makeRowAdapter(query: RelationQuery, usePlural: boolean): AdapterFactoryCustomizeAdapterCreator {
37
+ return ({ debugLog, getFieldAttributes }) => {
38
+ function isUnique(model: string, condition: CleanedWhere): boolean {
39
+ return (
40
+ condition.connector !== "OR"
41
+ && condition.operator === "eq"
42
+ && condition.mode !== "insensitive"
43
+ && (condition.field === "id" || getFieldAttributes({ field: condition.field, model }).unique === true)
44
+ );
45
+ }
49
46
  return rowTyped({
50
47
  count: ({ model, where }) => query(model, { where }, (relation) => relation.count()),
51
48
  create: ({ data, model, select }) => {
@@ -78,3 +75,4 @@ export const makeRowAdapter =
78
75
  updateMany: ({ model, update, where }) => query(model, { where }, (relation) => Effect.map(relation.updateAll(update), (rows) => rows.length)),
79
76
  });
80
77
  };
78
+ }
@@ -1,9 +1,7 @@
1
1
  import { all, and, not, or } from "@prisma-next/sql-orm-client";
2
2
  import type { Where } from "better-auth";
3
-
4
3
  export type CleanedWhere = Required<Where>;
5
4
  export type Expression = Parameters<typeof and>[number];
6
-
7
5
  export interface DynamicField {
8
6
  asc: () => unknown;
9
7
  desc: () => unknown;
@@ -20,53 +18,49 @@ export interface DynamicField {
20
18
  neq: (value: unknown) => Expression;
21
19
  notIn: (value: readonly unknown[]) => Expression;
22
20
  }
23
-
24
- const escapeLike = (value: string): string => value.replaceAll("\\", "\\\\").replaceAll("%", "\\%").replaceAll("_", "\\_");
25
-
26
- const nonNullValues = (value: unknown): readonly unknown[] => (Array.isArray(value) ? value : [value]).filter((item) => item !== null);
27
-
28
- const insensitiveStrings = (field: DynamicField, where: CleanedWhere, values: readonly unknown[]) => {
21
+ function escapeLike(value: string): string {
22
+ return value.replaceAll("\\", "\\\\").replaceAll("%", "\\%").replaceAll("_", "\\_");
23
+ }
24
+ function nonNullValues(value: unknown): readonly unknown[] {
25
+ return (Array.isArray(value) ? value : [value]).filter((item) => item !== null);
26
+ }
27
+ function insensitiveStrings(field: DynamicField, where: CleanedWhere, values: readonly unknown[]) {
29
28
  const ilike = field.ilike;
30
29
  if (where.mode !== "insensitive" || ilike === undefined || !values.every((item) => typeof item === "string")) {
31
30
  return undefined;
32
31
  }
33
32
  return values.map((item) => ilike(escapeLike(String(item))));
34
- };
35
-
36
- const membership = (field: DynamicField, where: CleanedWhere): Expression => {
33
+ }
34
+ function membership(field: DynamicField, where: CleanedWhere): Expression {
37
35
  const values = nonNullValues(where.value);
38
36
  if (values.length === 0) {
39
37
  return and(field.isNull(), field.isNotNull());
40
38
  }
41
39
  const matches = insensitiveStrings(field, where, values);
42
40
  return matches === undefined ? field.in(values) : or(...matches);
43
- };
44
-
45
- const exclusion = (field: DynamicField, where: CleanedWhere): Expression | undefined => {
41
+ }
42
+ function exclusion(field: DynamicField, where: CleanedWhere): Expression | undefined {
46
43
  const values = nonNullValues(where.value);
47
44
  if (values.length === 0) {
48
45
  return undefined;
49
46
  }
50
47
  const matches = insensitiveStrings(field, where, values);
51
48
  return matches === undefined ? field.notIn(values) : and(...matches.map((match) => not(match)));
52
- };
53
-
54
- const likePattern = (operator: CleanedWhere["operator"], escaped: string): string => {
49
+ }
50
+ function likePattern(operator: CleanedWhere["operator"], escaped: string): string {
55
51
  if (operator === "contains") {
56
52
  return `%${escaped}%`;
57
53
  }
58
54
  return operator === "starts_with" ? `${escaped}%` : `%${escaped}`;
59
- };
60
-
61
- const pattern = (field: DynamicField, where: CleanedWhere): Expression => {
55
+ }
56
+ function pattern(field: DynamicField, where: CleanedWhere): Expression {
62
57
  if (typeof where.value !== "string") {
63
58
  throw new TypeError(`${where.operator} requires a string value`);
64
59
  }
65
60
  const text = likePattern(where.operator, escapeLike(where.value));
66
61
  return where.mode === "insensitive" && field.ilike !== undefined ? field.ilike(text) : field.like(text);
67
- };
68
-
69
- const insensitiveEquality = (field: DynamicField, where: CleanedWhere): Expression | undefined => {
62
+ }
63
+ function insensitiveEquality(field: DynamicField, where: CleanedWhere): Expression | undefined {
70
64
  const value = where.value;
71
65
  const operator = where.operator ?? "eq";
72
66
  if (where.mode !== "insensitive" || typeof value !== "string" || field.ilike === undefined) {
@@ -79,9 +73,8 @@ const insensitiveEquality = (field: DynamicField, where: CleanedWhere): Expressi
79
73
  return not(field.ilike(escapeLike(value)));
80
74
  }
81
75
  return undefined;
82
- };
83
-
84
- const comparison = (field: DynamicField, where: CleanedWhere): Expression | undefined => {
76
+ }
77
+ function comparison(field: DynamicField, where: CleanedWhere): Expression | undefined {
85
78
  const value = where.value;
86
79
  switch (where.operator ?? "eq") {
87
80
  case "eq":
@@ -105,12 +98,10 @@ const comparison = (field: DynamicField, where: CleanedWhere): Expression | unde
105
98
  case "ends_with":
106
99
  return pattern(field, where);
107
100
  }
108
- };
109
-
110
- export const whereExpression = (fields: Record<string, DynamicField>, where: readonly CleanedWhere[]): Expression | undefined => {
101
+ }
102
+ export function whereExpression(fields: Record<string, DynamicField>, where: readonly CleanedWhere[]): Expression | undefined {
111
103
  const conjunctions: Expression[] = [];
112
104
  const disjunctions: Expression[] = [];
113
-
114
105
  for (const condition of where) {
115
106
  const field = fields[condition.field];
116
107
  if (field === undefined) {
@@ -122,7 +113,6 @@ export const whereExpression = (fields: Record<string, DynamicField>, where: rea
122
113
  }
123
114
  (condition.connector === "OR" ? disjunctions : conjunctions).push(expression);
124
115
  }
125
-
126
116
  if (disjunctions.length > 0) {
127
117
  conjunctions.push(or(...disjunctions));
128
118
  }
@@ -130,4 +120,4 @@ export const whereExpression = (fields: Record<string, DynamicField>, where: rea
130
120
  return all();
131
121
  }
132
122
  return conjunctions.length === 1 ? conjunctions[0] : and(...conjunctions);
133
- };
123
+ }
@@ -0,0 +1,47 @@
1
+ import { NodeHttpServerRequest, NodeStream } from "@effect/platform-node";
2
+ import { Effect, Stream } from "effect";
3
+ import { HttpIncomingMessage, HttpServerError, type HttpServerRequest } from "effect/http";
4
+
5
+ export function boundedRpcRequest(request: HttpServerRequest.HttpServerRequest): HttpServerRequest.HttpServerRequest {
6
+ const incoming = NodeHttpServerRequest.toIncomingMessage(request);
7
+ function error(cause: unknown) {
8
+ return new HttpServerError.HttpServerError({ reason: new HttpServerError.RequestParseError({ cause, request }) });
9
+ }
10
+ const text = Effect.gen(function* () {
11
+ const limit = yield* HttpIncomingMessage.MaxBodySize;
12
+ const chunks: Uint8Array[] = [];
13
+ let size = 0n;
14
+ yield* NodeStream.fromReadable<Uint8Array, HttpServerError.HttpServerError>({
15
+ closeOnDone: false,
16
+ evaluate: () => incoming,
17
+ onError: error,
18
+ }).pipe(
19
+ Stream.runForEach((chunk) => {
20
+ size += BigInt(chunk.byteLength);
21
+ if (limit !== undefined && size > limit) {
22
+ return Effect.fail(error(new Error("RPC body exceeds MAX_BODY_SIZE")));
23
+ }
24
+ chunks.push(chunk);
25
+ return Effect.void;
26
+ }),
27
+ );
28
+ return Buffer.concat(chunks).toString("utf8");
29
+ }).pipe(
30
+ Effect.onError(() => Effect.sync(() => incoming.resume())),
31
+ Effect.cached,
32
+ Effect.runSync,
33
+ );
34
+ const arrayBuffer = Effect.map(text, (body) => new TextEncoder().encode(body).buffer);
35
+ // The native text reader destroys the HTTP socket on overflow, preventing a 4xx response.
36
+ return new Proxy(request, {
37
+ get: (target, key, receiver) => {
38
+ if (key === "text") {
39
+ return text;
40
+ }
41
+ if (key === "arrayBuffer") {
42
+ return arrayBuffer;
43
+ }
44
+ return Reflect.get(target, key, receiver);
45
+ },
46
+ });
47
+ }
@@ -0,0 +1,36 @@
1
+ import * as PgClient from "@effect/sql-pg/PgClient";
2
+ import { Config, Effect, Layer } from "effect";
3
+ import * as Reactivity from "effect/reactivity/Reactivity";
4
+ import { makeLiveServer } from "@shivaedev/effect-contract/makeLiveServer.ts";
5
+ import { migratePostgres } from "@shivaedev/effect-sql/migrations.ts";
6
+ import { postgresChanges } from "@shivaedev/effect-sql/postgresChanges.ts";
7
+ import type { AppChanges, AppDatabaseOptions } from "./appHost.ts";
8
+
9
+ export function databaseLayer<R>(options: AppDatabaseOptions<R>, changesService: typeof AppChanges) {
10
+ const pool = Layer.unwrap(
11
+ Effect.gen(function* () {
12
+ const url = options.config?.databaseUrl ?? (yield* Config.Redacted("DATABASE_URL"));
13
+ return PgClient.layer({
14
+ url,
15
+ ...(options.config?.maxConnections === undefined ? {} : { maxConnections: options.config.maxConnections }),
16
+ ...(options.config?.applicationName === undefined ? {} : { applicationName: options.config.applicationName }),
17
+ });
18
+ }),
19
+ );
20
+ const migrated = Layer.effectDiscard(migratePostgres({ ...options.migrations, lockTimeout: options.migrations.lockTimeout ?? "5 seconds" })).pipe(
21
+ Layer.provideMerge(pool),
22
+ );
23
+ const live = Layer.effect(changesService, makeLiveServer).pipe(Layer.provideMerge(Reactivity.layer));
24
+ const base = Layer.merge(migrated, live);
25
+ const changes = options.config?.changes;
26
+ if (changes === undefined) {
27
+ return base;
28
+ }
29
+ return Layer.unwrap(
30
+ Effect.gen(function* () {
31
+ const client = yield* PgClient.PgClient;
32
+ const hints = yield* changesService;
33
+ return postgresChanges({ ...changes, client, resync: hints.resync });
34
+ }),
35
+ ).pipe(Layer.provideMerge(base));
36
+ }
@@ -0,0 +1,25 @@
1
+ import type { Layer } from "effect";
2
+ import type { HttpRouter, HttpStaticServer } from "effect/http";
3
+ import type { Rpc, RpcGroup } from "effect/rpc";
4
+ import type { SessionPolicy, SessionShape } from "#rpc-server/session.ts";
5
+ import type { AppDatabaseOptions } from "./appHost.ts";
6
+
7
+ export interface HostOptions<TRpcs extends Rpc.Any, THandlers, THandlerError, THandlerRequirements, TServices, TServiceError, TServiceRequirements> {
8
+ readonly assets?: string | Parameters<typeof HttpStaticServer.layer>[0];
9
+ readonly config?: AppDatabaseOptions["config"] & {
10
+ readonly port?: number;
11
+ readonly host?: string;
12
+ readonly trustedProxy?: string;
13
+ readonly maxBodySize?: number;
14
+ readonly disablePreemptiveShutdown?: boolean;
15
+ };
16
+ readonly group: RpcGroup.RpcGroup<TRpcs>;
17
+ readonly handlers: Layer.Layer<THandlers, THandlerError, THandlerRequirements>;
18
+ readonly routes?: Layer.Layer<never, never, HttpRouter.HttpRouter | HttpRouter.Request<"Error", unknown>>;
19
+ readonly rpcPath?: HttpRouter.PathInput;
20
+ readonly services: Layer.Layer<TServices, TServiceError, TServiceRequirements>;
21
+ }
22
+
23
+ export type HostPolicy<TSession extends SessionShape> =
24
+ | { readonly policy?: undefined; readonly sessions: SessionPolicy<TSession> }
25
+ | { readonly policy: "loopback"; readonly sessions?: never };
@@ -1,28 +1,25 @@
1
1
  import type { IncomingMessage } from "node:http";
2
-
3
2
  export interface NodeSubscriptionSignalOptions {
4
3
  readonly nodeRequest?: IncomingMessage | undefined;
5
4
  readonly request?: Request | undefined;
6
5
  readonly signals?: Iterable<AbortSignal | undefined>;
7
6
  }
8
-
9
7
  export interface NodeSubscriptionSignal {
10
8
  readonly dispose: () => void;
11
9
  readonly signal: AbortSignal;
12
10
  }
13
-
14
- const property = (value: unknown, key: string): unknown => (typeof value === "object" && value !== null ? Reflect.get(value, key) : undefined);
15
-
16
- const isIncomingMessage = (value: unknown): value is IncomingMessage =>
17
- typeof property(value, "socket") === "object" && typeof property(value, "once") === "function";
18
-
19
- const nodeRequestFrom = (request: Request | undefined): IncomingMessage | undefined => {
11
+ function property(value: unknown, key: string): unknown {
12
+ return typeof value === "object" && value !== null ? Reflect.get(value, key) : undefined;
13
+ }
14
+ function isIncomingMessage(value: unknown): value is IncomingMessage {
15
+ return typeof property(value, "socket") === "object" && typeof property(value, "once") === "function";
16
+ }
17
+ function nodeRequestFrom(request: Request | undefined): IncomingMessage | undefined {
20
18
  const req = property(property(property(request, "runtime"), "node"), "req");
21
19
  return isIncomingMessage(req) ? req : undefined;
22
- };
23
-
20
+ }
24
21
  // Under Bun's node:http compatibility layer a Web request signal may miss an abandoned socket, so node close events cover long-lived responses.
25
- export const nodeSubscriptionSignal = (options: NodeSubscriptionSignalOptions): NodeSubscriptionSignal => {
22
+ export function nodeSubscriptionSignal(options: NodeSubscriptionSignalOptions): NodeSubscriptionSignal {
26
23
  const controller = new AbortController();
27
24
  const signals: AbortSignal[] = [controller.signal];
28
25
  if (options.request !== undefined) {
@@ -33,25 +30,25 @@ export const nodeSubscriptionSignal = (options: NodeSubscriptionSignalOptions):
33
30
  signals.push(signal);
34
31
  }
35
32
  }
36
-
37
33
  const nodeRequest = options.nodeRequest ?? nodeRequestFrom(options.request);
38
34
  if (nodeRequest === undefined) {
39
- return { dispose: () => {}, signal: AbortSignal.any(signals) };
35
+ return { dispose: (): void => undefined, signal: AbortSignal.any(signals) };
40
36
  }
41
-
42
- const socket = nodeRequest.socket;
43
- const onRequestClose = () => {
44
- if (!nodeRequest.complete) {
37
+ const request = nodeRequest;
38
+ const socket = request.socket;
39
+ function onRequestClose() {
40
+ if (!request.complete) {
45
41
  controller.abort();
46
42
  }
47
- };
48
- const onSocketClose = () => controller.abort();
43
+ }
44
+ function onSocketClose() {
45
+ return controller.abort();
46
+ }
49
47
  nodeRequest.once("close", onRequestClose);
50
48
  socket.once("close", onSocketClose);
51
49
  if (nodeRequest.destroyed || socket.destroyed) {
52
50
  controller.abort();
53
51
  }
54
-
55
52
  let disposed = false;
56
53
  return {
57
54
  dispose: () => {
@@ -64,4 +61,4 @@ export const nodeSubscriptionSignal = (options: NodeSubscriptionSignalOptions):
64
61
  },
65
62
  signal: AbortSignal.any(signals),
66
63
  };
67
- };
64
+ }