@shivaedev/platform 0.15.0 → 0.16.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 +16 -0
- package/README.md +83 -49
- package/dist/rpc/identity.d.ts +1 -0
- package/dist/rpc/identity.d.ts.map +1 -1
- package/dist/rpc/identity.js.map +1 -1
- package/dist/rpc/requireAdmin.d.ts +5 -0
- package/dist/rpc/requireAdmin.d.ts.map +1 -0
- package/dist/rpc/requireAdmin.js +14 -0
- package/dist/rpc/requireAdmin.js.map +1 -0
- package/dist/server/MigrationOptions.d.ts +11 -0
- package/dist/server/MigrationOptions.d.ts.map +1 -0
- package/dist/server/MigrationOptions.js +2 -0
- package/dist/server/MigrationOptions.js.map +1 -0
- package/dist/server/appHost.d.ts +5 -10
- package/dist/server/appHost.d.ts.map +1 -1
- package/dist/server/appHost.js.map +1 -1
- package/dist/server/appHostHttp.d.ts +7 -7
- package/dist/server/appHostHttp.d.ts.map +1 -1
- package/dist/server/appHostHttp.js +8 -7
- package/dist/server/appHostHttp.js.map +1 -1
- package/dist/server/better-auth/adapter.d.ts +1 -1
- package/dist/server/better-auth/adapter.d.ts.map +1 -1
- package/dist/server/databaseLayer.d.ts.map +1 -1
- package/dist/server/databaseLayer.js +18 -4
- package/dist/server/databaseLayer.js.map +1 -1
- package/dist/server/hostOptions.d.ts +1 -1
- package/dist/server/hostOptions.d.ts.map +1 -1
- package/dist/server/otlpHttpClient.js +1 -1
- package/dist/server/otlpHttpClient.js.map +1 -1
- package/dist/server/otlpTracing.js +2 -2
- package/dist/server/otlpTracing.js.map +1 -1
- package/dist/server/redactOtlpUrl.js +1 -1
- package/dist/server/redactOtlpUrl.js.map +1 -1
- package/dist/server/rpc-server/adapters/betterAuthSessions.d.ts +1 -1
- package/dist/server/rpc-server/adapters/betterAuthSessions.d.ts.map +1 -1
- package/dist/server/rpc-server/adapters/betterAuthSessions.js +1 -1
- package/dist/server/rpc-server/adapters/betterAuthSessions.js.map +1 -1
- package/dist/server/rpc-server/session.d.ts +3 -2
- package/dist/server/rpc-server/session.d.ts.map +1 -1
- package/dist/server/rpc-server/session.js +9 -5
- package/dist/server/rpc-server/session.js.map +1 -1
- package/dist/server/safeOtlpSerialization.js +2 -2
- package/dist/server/safeOtlpSerialization.js.map +1 -1
- package/dist/server/testing/makePlatformIt.d.ts +1 -1
- package/dist/server/testing/makePlatformIt.d.ts.map +1 -1
- package/dist/server/testing/makePlatformIt.js +1 -1
- package/dist/server/testing/makePlatformIt.js.map +1 -1
- package/dist/server/testing/types.d.ts +3 -3
- package/dist/server/testing/types.d.ts.map +1 -1
- package/dist/server/tracedGroup.d.ts +4 -0
- package/dist/server/tracedGroup.d.ts.map +1 -0
- package/dist/server/tracedGroup.js +13 -0
- package/dist/server/tracedGroup.js.map +1 -0
- package/package.json +15 -343
- package/src/rpc/identity.ts +1 -0
- package/src/rpc/requireAdmin.ts +14 -0
- package/src/server/MigrationOptions.ts +11 -0
- package/src/server/appHost.ts +4 -9
- package/src/server/appHostHttp.ts +11 -10
- package/src/server/better-auth/adapter.ts +1 -1
- package/src/server/databaseLayer.ts +22 -6
- package/src/server/hostOptions.ts +1 -1
- package/src/server/otlpHttpClient.ts +1 -1
- package/src/server/otlpTracing.ts +2 -2
- package/src/server/redactOtlpUrl.ts +1 -1
- package/src/server/rpc-server/adapters/betterAuthSessions.ts +2 -2
- package/src/server/rpc-server/session.ts +9 -6
- package/src/server/safeOtlpSerialization.ts +2 -2
- package/src/server/testing/makePlatformIt.ts +4 -4
- package/src/server/testing/types.ts +3 -3
- package/src/server/tracedGroup.ts +14 -0
|
@@ -3,17 +3,18 @@ import { NodeHttpServer, NodeServices } from "@effect/platform-node";
|
|
|
3
3
|
import { ByteSize, Config, Effect, Layer, Option, Schema } from "effect";
|
|
4
4
|
import { HttpIncomingMessage, HttpRouter, HttpServerRequest, HttpStaticServer } from "effect/http";
|
|
5
5
|
import { type Rpc, RpcSerialization, RpcServer } from "effect/rpc";
|
|
6
|
-
import type { AppHostOptions, AppHttpHostOptions, AppLocalHostOptions } from "#appHost.ts";
|
|
7
|
-
import { boundedRpcRequest } from "#boundedRpcRequest.ts";
|
|
8
|
-
import type { databaseLayer } from "#databaseLayer.ts";
|
|
9
|
-
import type { HostOptions, HostPolicy } from "#hostOptions.ts";
|
|
10
|
-
import { otlpTracing } from "#otlpTracing.ts";
|
|
11
6
|
import type { Authenticated, MaybeAuthenticated, RequestTracing } from "#rpc/middleware.ts";
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import { TrustedProxy } from "#rpc-server/trustedProxy.ts";
|
|
7
|
+
import type { AppHostOptions, AppHttpHostOptions, AppLocalHostOptions } from "#server/appHost.ts";
|
|
8
|
+
import { boundedRpcRequest } from "#server/boundedRpcRequest.ts";
|
|
9
|
+
import type { databaseLayer } from "#server/databaseLayer.ts";
|
|
16
10
|
import { healthRoute } from "#server/healthRoute.ts";
|
|
11
|
+
import type { HostOptions, HostPolicy } from "#server/hostOptions.ts";
|
|
12
|
+
import { otlpTracing } from "#server/otlpTracing.ts";
|
|
13
|
+
import { localRpcGuard } from "#server/rpc-server/local.ts";
|
|
14
|
+
import { authenticatedLayer, maybeAuthenticatedLayer, type SessionShape } from "#server/rpc-server/session.ts";
|
|
15
|
+
import { requestTracingLayer } from "#server/rpc-server/tracing.ts";
|
|
16
|
+
import { TrustedProxy } from "#server/rpc-server/trustedProxy.ts";
|
|
17
|
+
import { tracedGroup } from "#server/tracedGroup.ts";
|
|
17
18
|
export function appHostHttp<
|
|
18
19
|
TRpcs extends Rpc.Any,
|
|
19
20
|
THandlers,
|
|
@@ -46,7 +47,7 @@ export function appHostHttp<
|
|
|
46
47
|
const routes = HttpRouter.use((router) =>
|
|
47
48
|
Effect.gen(function* () {
|
|
48
49
|
const wire = yield* RpcSerialization.RpcSerialization;
|
|
49
|
-
const handler = yield* RpcServer.toHttpEffect(options.group);
|
|
50
|
+
const handler = yield* RpcServer.toHttpEffect(tracedGroup(options.group));
|
|
50
51
|
const guarded = localRpcGuard(handler, trustedProxy);
|
|
51
52
|
yield* router.add("POST", options.rpcPath ?? "/rpc", (request) =>
|
|
52
53
|
guarded.pipe(
|
|
@@ -2,7 +2,7 @@ import type { BetterAuthOptions, DBAdapterInstance } from "better-auth";
|
|
|
2
2
|
import { createAdapterFactory } from "better-auth/adapters";
|
|
3
3
|
import { Effect } from "effect";
|
|
4
4
|
import type { AnyDatabase } from "@shivaedev/effect-prisma/databaseTypes.ts";
|
|
5
|
-
import type { PlatformRuntime } from "#runtime/types.ts";
|
|
5
|
+
import type { PlatformRuntime } from "#server/runtime/types.ts";
|
|
6
6
|
import { type DynamicRelation, refineRelation } from "./relation.ts";
|
|
7
7
|
import { makeRowAdapter, type RelationQuery } from "./rowAdapter.ts";
|
|
8
8
|
export interface EffectPrismaAdapterOptions {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as PgClient from "@effect/sql-pg/PgClient";
|
|
2
|
-
import { Config, Effect, Layer, type Redacted } from "effect";
|
|
2
|
+
import { Config, Effect, Layer, type Redacted, Schedule } from "effect";
|
|
3
3
|
import * as Reactivity from "effect/reactivity/Reactivity";
|
|
4
4
|
import { makeLiveServer } from "@shivaedev/effect-contract/makeLiveServer.ts";
|
|
5
5
|
import { assertPostgresMigrations } from "@shivaedev/effect-sql/assertPostgresMigrations.ts";
|
|
@@ -27,11 +27,7 @@ export function databaseLayer<R>(options: AppDatabaseOptions<R>, changesService:
|
|
|
27
27
|
});
|
|
28
28
|
}),
|
|
29
29
|
);
|
|
30
|
-
const prepared = Layer.effectDiscard(
|
|
31
|
-
options.migrations.mode === "check"
|
|
32
|
-
? assertPostgresMigrations(options.migrations)
|
|
33
|
-
: migratePostgres({ ...options.migrations, lockTimeout: options.migrations.lockTimeout ?? "5 seconds" }),
|
|
34
|
-
).pipe(Layer.provideMerge(pool));
|
|
30
|
+
const prepared = Layer.effectDiscard(prepareMigrations(options.migrations)).pipe(Layer.provideMerge(pool));
|
|
35
31
|
const live = Layer.effect(changesService, makeLiveServer).pipe(Layer.provideMerge(Reactivity.layer));
|
|
36
32
|
const base = Layer.merge(prepared, live);
|
|
37
33
|
const changes = options.config?.changes;
|
|
@@ -46,3 +42,23 @@ export function databaseLayer<R>(options: AppDatabaseOptions<R>, changesService:
|
|
|
46
42
|
}),
|
|
47
43
|
).pipe(Layer.provideMerge(base));
|
|
48
44
|
}
|
|
45
|
+
|
|
46
|
+
function prepareMigrations<R>(migrations: AppDatabaseOptions<R>["migrations"]) {
|
|
47
|
+
switch (migrations.mode) {
|
|
48
|
+
case "check":
|
|
49
|
+
return assertPostgresMigrations(migrations);
|
|
50
|
+
case "wait":
|
|
51
|
+
return waitForMigrations(migrations);
|
|
52
|
+
default:
|
|
53
|
+
return migratePostgres({ ...migrations, lockTimeout: migrations.lockTimeout ?? "5 seconds" });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function waitForMigrations<R>(migrations: AppDatabaseOptions<R>["migrations"]) {
|
|
58
|
+
return assertPostgresMigrations(migrations).pipe(
|
|
59
|
+
Effect.retry({
|
|
60
|
+
schedule: Schedule.max([Schedule.spaced(migrations.pollInterval ?? "1 second"), Schedule.during(migrations.waitTimeout ?? "5 minutes")]),
|
|
61
|
+
while: (error) => error._tag === "MigrationError" && error.kind === "BadState",
|
|
62
|
+
}),
|
|
63
|
+
);
|
|
64
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Layer } from "effect";
|
|
2
2
|
import type { HttpRouter, HttpStaticServer } from "effect/http";
|
|
3
3
|
import type { Rpc, RpcGroup } from "effect/rpc";
|
|
4
|
-
import type { SessionPolicy, SessionShape } from "#rpc-server/session.ts";
|
|
4
|
+
import type { SessionPolicy, SessionShape } from "#server/rpc-server/session.ts";
|
|
5
5
|
import type { AppDatabaseOptions } from "./appHost.ts";
|
|
6
6
|
|
|
7
7
|
export type HostOptions<TRpcs extends Rpc.Any, THandlers, THandlerError, THandlerRequirements, TServices, TServiceError, TServiceRequirements> = {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Effect, Layer } from "effect";
|
|
2
2
|
import { FetchHttpClient, HttpClient, HttpClientError, type HttpClientRequest } from "effect/http";
|
|
3
|
-
import { redactOtlpText } from "#safeOtlpSerialization.ts";
|
|
3
|
+
import { redactOtlpText } from "#server/safeOtlpSerialization.ts";
|
|
4
4
|
|
|
5
5
|
function timeoutError(request: HttpClientRequest.HttpClientRequest) {
|
|
6
6
|
return new HttpClientError.HttpClientError({
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Config, Effect, Layer, Schema } from "effect";
|
|
2
2
|
import { OtlpSerialization, OtlpTracer } from "effect/observability";
|
|
3
|
-
import { otlpHttpClient } from "#otlpHttpClient.ts";
|
|
4
|
-
import { safeOtlpSerialization } from "#safeOtlpSerialization.ts";
|
|
3
|
+
import { otlpHttpClient } from "#server/otlpHttpClient.ts";
|
|
4
|
+
import { safeOtlpSerialization } from "#server/safeOtlpSerialization.ts";
|
|
5
5
|
|
|
6
6
|
export const otlpTracing: Layer.Layer<never, Config.ConfigError> = Layer.unwrap(
|
|
7
7
|
Effect.gen(function* () {
|
|
@@ -2,8 +2,8 @@ import { Effect, Option } from "effect";
|
|
|
2
2
|
import type { Headers } from "effect/http";
|
|
3
3
|
import { AuthUnavailable } from "#errors/taxonomy.ts";
|
|
4
4
|
import type { ProviderAccountValue } from "#rpc/identity.ts";
|
|
5
|
-
import { redact } from "#rpc-server/redact.ts";
|
|
6
|
-
import type { SessionProvider, SessionShape } from "#rpc-server/session.ts";
|
|
5
|
+
import { redact } from "#server/rpc-server/redact.ts";
|
|
6
|
+
import type { SessionProvider, SessionShape } from "#server/rpc-server/session.ts";
|
|
7
7
|
export type GetSession<TSession extends SessionShape> = (headers: globalThis.Headers) => Promise<TSession | null>;
|
|
8
8
|
export type GetProviderAccounts = (headers: globalThis.Headers) => Promise<readonly ProviderAccountValue[]>;
|
|
9
9
|
export type SessionWithProviderAccounts<TSession extends SessionShape> = TSession & {
|
|
@@ -60,14 +60,14 @@ export function maybeAuthenticatedLayer<TSession extends SessionShape>(policy: S
|
|
|
60
60
|
export interface UserSessionShape extends SessionShape {
|
|
61
61
|
readonly user: SessionUserValue;
|
|
62
62
|
}
|
|
63
|
-
export function
|
|
64
|
-
return
|
|
63
|
+
export function withSessionUser<TSession extends UserSessionShape>(policy: SessionPolicy<TSession>, headers: Headers.Headers, operation: string) {
|
|
64
|
+
return <A, E, R>(effect: Effect.Effect<A, E, R>) =>
|
|
65
65
|
Effect.gen(function* () {
|
|
66
|
-
const session = yield* resolveSession(policy, headers,
|
|
66
|
+
const session = yield* resolveSession(policy, headers, operation);
|
|
67
67
|
if (Option.isNone(session)) {
|
|
68
68
|
return yield* new Unauthorized({ message: "Authentication required" });
|
|
69
69
|
}
|
|
70
|
-
const { name, email, providerAccounts } = session.value.user;
|
|
70
|
+
const { name, email, role, providerAccounts } = session.value.user;
|
|
71
71
|
const id = yield* identified(session.value.user.id);
|
|
72
72
|
return yield* effect.pipe(
|
|
73
73
|
Effect.provideService(Identity, { id }),
|
|
@@ -75,12 +75,15 @@ export function authenticatedUserLayer<TSession extends UserSessionShape>(policy
|
|
|
75
75
|
email,
|
|
76
76
|
id,
|
|
77
77
|
name,
|
|
78
|
+
...(role === undefined ? {} : { role }),
|
|
78
79
|
...(providerAccounts === undefined
|
|
79
80
|
? {}
|
|
80
81
|
: { providerAccounts: providerAccounts.map(({ accountId, providerId }) => ({ accountId, providerId })) }),
|
|
81
82
|
}),
|
|
82
83
|
Effect.annotateLogs({ userId: id }),
|
|
83
84
|
);
|
|
84
|
-
})
|
|
85
|
-
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
export function authenticatedUserLayer<TSession extends UserSessionShape>(policy: SessionPolicy<TSession>): Layer.Layer<AuthenticatedUser> {
|
|
88
|
+
return Layer.succeed(AuthenticatedUser, (effect, { headers, rpc }) => effect.pipe(withSessionUser(policy, headers, rpc._tag)));
|
|
86
89
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Effect, Layer } from "effect";
|
|
2
2
|
import type { OtlpResource } from "effect/observability";
|
|
3
3
|
import { OtlpSerialization } from "effect/observability";
|
|
4
|
-
import { redactOtlpUrl } from "#redactOtlpUrl.ts";
|
|
5
|
-
import { isSensitiveKey, REDACTED, redactText } from "#rpc-server/sensitive.ts";
|
|
4
|
+
import { redactOtlpUrl } from "#server/redactOtlpUrl.ts";
|
|
5
|
+
import { isSensitiveKey, REDACTED, redactText } from "#server/rpc-server/sensitive.ts";
|
|
6
6
|
|
|
7
7
|
function sensitive(key: string) {
|
|
8
8
|
return isSensitiveKey(key) || ["db.user", "db.user.name", "db.connection_string"].includes(key.toLowerCase());
|
|
@@ -2,10 +2,10 @@ import type { TestContext } from "@effect/vitest";
|
|
|
2
2
|
import { Effect, type Layer } from "effect";
|
|
3
3
|
import type { AnyDatabase } from "@shivaedev/effect-prisma/databaseTypes.ts";
|
|
4
4
|
import { withTestTransaction } from "@shivaedev/effect-prisma/testing/transaction.ts";
|
|
5
|
-
import type { EffectTRPCAdapter } from "@shivaedev/effect-trpc/adapter.ts";
|
|
6
|
-
import type { EffectCallerFactory } from "@shivaedev/effect-trpc/testing/caller.ts";
|
|
7
|
-
import type { CallerOptions, CallerResult } from "@shivaedev/effect-trpc/testing/types.ts";
|
|
8
|
-
import { makeTrpcHarnessIt } from "@shivaedev/effect-trpc/testing/vitest.ts";
|
|
5
|
+
import type { EffectTRPCAdapter } from "@shivaedev/effect-trpc/server/adapter.ts";
|
|
6
|
+
import type { EffectCallerFactory } from "@shivaedev/effect-trpc/server/testing/caller.ts";
|
|
7
|
+
import type { CallerOptions, CallerResult } from "@shivaedev/effect-trpc/server/testing/types.ts";
|
|
8
|
+
import { makeTrpcHarnessIt } from "@shivaedev/effect-trpc/server/testing/vitest.ts";
|
|
9
9
|
import type { Bivariant } from "@shivaedev/types/bivariant.ts";
|
|
10
10
|
import type { MakePlatformItOptions, PlatformHarness, PlatformIt } from "./types.ts";
|
|
11
11
|
|
|
@@ -2,9 +2,9 @@ import type { TestContext, Vitest } from "@effect/vitest";
|
|
|
2
2
|
import type { Effect, Layer } from "effect";
|
|
3
3
|
import type { AnyDatabase } from "@shivaedev/effect-prisma/databaseTypes.ts";
|
|
4
4
|
import type { DatabaseService } from "@shivaedev/effect-prisma/testing/types.ts";
|
|
5
|
-
import type { EffectTRPCAdapter } from "@shivaedev/effect-trpc/adapter.ts";
|
|
6
|
-
import type { EffectCallerFactory } from "@shivaedev/effect-trpc/testing/caller.ts";
|
|
7
|
-
import type { CallerOptions, CallerResult, TrpcHarnessTest, TrpcHarnessTester } from "@shivaedev/effect-trpc/testing/types.ts";
|
|
5
|
+
import type { EffectTRPCAdapter } from "@shivaedev/effect-trpc/server/adapter.ts";
|
|
6
|
+
import type { EffectCallerFactory } from "@shivaedev/effect-trpc/server/testing/caller.ts";
|
|
7
|
+
import type { CallerOptions, CallerResult, TrpcHarnessTest, TrpcHarnessTester } from "@shivaedev/effect-trpc/server/testing/types.ts";
|
|
8
8
|
export type PlatformHarness<TDatabase extends AnyDatabase, TOptions, TCaller, TExtension> = Readonly<{
|
|
9
9
|
db: DatabaseService<TDatabase>;
|
|
10
10
|
trpc: EffectCallerFactory<TOptions, TCaller>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Rpc, RpcGroup } from "effect/rpc";
|
|
2
|
+
import { RequestTracing } from "#rpc/middleware.ts";
|
|
3
|
+
|
|
4
|
+
export function tracedGroup<R extends Rpc.Any>(group: RpcGroup.RpcGroup<R>) {
|
|
5
|
+
const traced = group.middleware(RequestTracing);
|
|
6
|
+
for (const rpc of traced.requests.values()) {
|
|
7
|
+
const middlewares = new Set(rpc.middlewares);
|
|
8
|
+
middlewares.delete(RequestTracing);
|
|
9
|
+
middlewares.add(RequestTracing);
|
|
10
|
+
// Native middleware() clones each RPC but cannot move an existing middleware to the outermost position.
|
|
11
|
+
Object.assign(rpc, { middlewares });
|
|
12
|
+
}
|
|
13
|
+
return traced;
|
|
14
|
+
}
|