@shivaedev/server 0.4.0 → 0.6.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/README.md +5 -3
- package/dist/AppHostFactory.d.ts +3 -3
- package/dist/AppHostFactory.d.ts.map +1 -1
- package/dist/AppHostInput.d.ts +2 -2
- package/dist/AppHostInput.d.ts.map +1 -1
- package/dist/MigrationOptions.d.ts +2 -2
- package/dist/MigrationOptions.d.ts.map +1 -1
- package/dist/TrustedProxyRequired.d.ts +8 -0
- package/dist/TrustedProxyRequired.d.ts.map +1 -0
- package/dist/TrustedProxyRequired.js +5 -0
- package/dist/TrustedProxyRequired.js.map +1 -0
- package/dist/appHost.d.ts +8 -8
- package/dist/appHost.d.ts.map +1 -1
- package/dist/appHost.js.map +1 -1
- package/dist/appHostHttp.d.ts +29 -29
- package/dist/appHostHttp.d.ts.map +1 -1
- package/dist/appHostHttp.js.map +1 -1
- package/dist/databaseLayer.d.ts +6 -6
- package/dist/databaseLayer.d.ts.map +1 -1
- package/dist/databaseLayer.js +15 -33
- package/dist/databaseLayer.js.map +1 -1
- package/dist/hostRpcRoutes.js +1 -4
- package/dist/hostRpcRoutes.js.map +1 -1
- package/dist/hostTransport.d.ts +2 -1
- package/dist/hostTransport.d.ts.map +1 -1
- package/dist/hostTransport.js +2 -1
- package/dist/hostTransport.js.map +1 -1
- package/dist/node-http/subscriptionSignal.js +1 -1
- package/dist/node-http/subscriptionSignal.js.map +1 -1
- package/package.json +20 -20
- package/src/AppHostFactory.ts +6 -3
- package/src/AppHostInput.ts +6 -3
- package/src/MigrationOptions.ts +2 -2
- package/src/TrustedProxyRequired.ts +6 -0
- package/src/appHost.ts +10 -6
- package/src/appHostHttp.ts +5 -3
- package/src/databaseLayer.ts +27 -43
- package/src/hostRpcRoutes.ts +5 -5
- package/src/hostTransport.ts +2 -3
- package/src/node-http/subscriptionSignal.ts +1 -1
- package/dist/databaseAddress.d.ts +0 -4
- package/dist/databaseAddress.d.ts.map +0 -1
- package/dist/databaseAddress.js +0 -25
- package/dist/databaseAddress.js.map +0 -1
- package/src/databaseAddress.ts +0 -25
package/src/databaseLayer.ts
CHANGED
|
@@ -1,57 +1,29 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { createConnection } from "node:net";
|
|
3
2
|
import * as PgClient from "@effect/sql-pg/PgClient";
|
|
4
|
-
import { Config, Duration, Effect, Fiber, Layer, Redacted, Schedule } from "effect";
|
|
3
|
+
import { Config, Context, Duration, Effect, Fiber, Layer, Option, Redacted, Schedule } from "effect";
|
|
5
4
|
import * as Reactivity from "effect/reactivity/Reactivity";
|
|
5
|
+
import { Migrator } from "effect/sql";
|
|
6
6
|
import { makeLiveServer } from "@shivaedev/contract/makeLiveServer.ts";
|
|
7
7
|
import { assertPostgresMigrations, postgresMigrationError } from "@shivaedev/sql/assertPostgresMigrations.ts";
|
|
8
|
-
import type { SqlNameTransforms } from "@shivaedev/sql/clientOptions.ts";
|
|
9
8
|
import { coordinateWorkerRelease } from "@shivaedev/sql/coordinateWorkerRelease.ts";
|
|
10
9
|
import { migratePostgres } from "@shivaedev/sql/migrations.ts";
|
|
11
10
|
import { type PendingPostgresMigrations, pendingPostgresMigrations } from "@shivaedev/sql/pendingPostgresMigrations.ts";
|
|
12
11
|
import { type PostgresChangesOptions, postgresChanges } from "@shivaedev/sql/postgresChanges.ts";
|
|
12
|
+
import { type PostgresOptions, postgresLayer } from "@shivaedev/sql/postgresLayer.ts";
|
|
13
13
|
import { WorkerRelease } from "@shivaedev/sql/WorkerRelease.ts";
|
|
14
14
|
import type { AppChanges, AppDatabaseOptions } from "#appHost.ts";
|
|
15
|
-
import { databaseAddress } from "./databaseAddress.ts";
|
|
16
15
|
|
|
17
|
-
export interface DatabaseConfig extends
|
|
18
|
-
readonly
|
|
19
|
-
readonly changes?: Partial<Omit<PostgresChangesOptions, "client" | "resync">>;
|
|
16
|
+
export interface DatabaseConfig extends PostgresOptions {
|
|
17
|
+
readonly changes?: Partial<Omit<PostgresChangesOptions, "client" | "listenerClient" | "connectionSetting" | "resync">>;
|
|
20
18
|
readonly databaseUrl?: Redacted.Redacted<string>;
|
|
21
|
-
readonly
|
|
19
|
+
readonly listenDatabaseUrl?: Redacted.Redacted<string>;
|
|
22
20
|
}
|
|
23
21
|
|
|
24
|
-
export function databaseLayer<R>(options: AppDatabaseOptions<R>, changesService: typeof AppChanges) {
|
|
22
|
+
export function databaseLayer<R, E>(options: AppDatabaseOptions<R, E>, changesService: typeof AppChanges) {
|
|
25
23
|
const pool = Layer.unwrap(
|
|
26
|
-
Effect.
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const disconnected = new Set<Promise<void>>();
|
|
30
|
-
// Native pool shutdown sends Terminate without waiting for its sockets to close.
|
|
31
|
-
yield* Effect.addFinalizer(() => Effect.promise(() => Promise.all([...disconnected])));
|
|
32
|
-
return PgClient.layer({
|
|
33
|
-
stream: () => {
|
|
34
|
-
const target = new URL(Redacted.value(url));
|
|
35
|
-
const host = address.host ?? target.searchParams.getAll("host").at(-1) ?? (decodeURIComponent(target.hostname) || "localhost");
|
|
36
|
-
const port = address.port ?? 5432;
|
|
37
|
-
const socket = createConnection(host.startsWith("/") ? { path: `${host}/.s.PGSQL.${port}` } : { host, noDelay: true, port });
|
|
38
|
-
const closed = new Promise<void>((resolve) =>
|
|
39
|
-
socket.once("close", () => {
|
|
40
|
-
disconnected.delete(closed);
|
|
41
|
-
resolve();
|
|
42
|
-
}),
|
|
43
|
-
);
|
|
44
|
-
disconnected.add(closed);
|
|
45
|
-
return socket;
|
|
46
|
-
},
|
|
47
|
-
transformQueryNames: options.config?.transformQueryNames,
|
|
48
|
-
transformResultNames: options.config?.transformResultNames,
|
|
49
|
-
url,
|
|
50
|
-
...address,
|
|
51
|
-
...(options.config?.maxConnections === undefined ? {} : { maxConnections: options.config.maxConnections }),
|
|
52
|
-
...(options.config?.applicationName === undefined ? {} : { applicationName: options.config.applicationName }),
|
|
53
|
-
});
|
|
54
|
-
}),
|
|
24
|
+
Effect.map(options.config?.databaseUrl === undefined ? Config.Redacted("DATABASE_URL") : Effect.succeed(options.config.databaseUrl), (url) =>
|
|
25
|
+
postgresLayer(url, options.config),
|
|
26
|
+
),
|
|
55
27
|
);
|
|
56
28
|
const prepared = Layer.effectDiscard(prepareMigrations(options.migrations)).pipe(Layer.provideMerge(pool));
|
|
57
29
|
const live = Layer.effect(changesService, makeLiveServer).pipe(Layer.provideMerge(Reactivity.layer));
|
|
@@ -69,12 +41,24 @@ export function databaseLayer<R>(options: AppDatabaseOptions<R>, changesService:
|
|
|
69
41
|
const url = options.config?.databaseUrl ?? (yield* Config.Redacted("DATABASE_URL"));
|
|
70
42
|
const name = yield* Config.String("APP_NAME").pipe(Config.withDefault(new URL(Redacted.value(url)).pathname));
|
|
71
43
|
const channel = changes?.channel ?? `changes_${createHash("sha256").update(name).digest("hex").slice(0, 32)}`;
|
|
72
|
-
|
|
44
|
+
const listenUrl = options.config?.listenDatabaseUrl ?? Option.getOrUndefined(yield* Config.Redacted("LISTEN_DATABASE_URL").pipe(Config.option));
|
|
45
|
+
const listenerClient =
|
|
46
|
+
listenUrl === undefined
|
|
47
|
+
? client
|
|
48
|
+
: yield* Effect.map(
|
|
49
|
+
Layer.build(postgresLayer(listenUrl, { applicationName: options.config?.applicationName, maxConnections: 1 })),
|
|
50
|
+
(context) => Context.get(context, PgClient.PgClient),
|
|
51
|
+
);
|
|
52
|
+
let connectionSetting = options.config?.databaseUrl === undefined ? "DATABASE_URL" : "config.databaseUrl";
|
|
53
|
+
if (listenUrl !== undefined) {
|
|
54
|
+
connectionSetting = options.config?.listenDatabaseUrl === undefined ? "LISTEN_DATABASE_URL" : "config.listenDatabaseUrl";
|
|
55
|
+
}
|
|
56
|
+
return postgresChanges({ ...changes, channel, client, connectionSetting, listenerClient, resync: hints.resync });
|
|
73
57
|
}),
|
|
74
58
|
).pipe(Layer.provideMerge(base));
|
|
75
59
|
}
|
|
76
60
|
|
|
77
|
-
function prepareMigrations<R>(migrations: AppDatabaseOptions<R>["migrations"]) {
|
|
61
|
+
function prepareMigrations<R, E>(migrations: AppDatabaseOptions<R, E>["migrations"]) {
|
|
78
62
|
switch (migrations.mode) {
|
|
79
63
|
case "check": {
|
|
80
64
|
const schema = { ...migrations };
|
|
@@ -88,7 +72,7 @@ function prepareMigrations<R>(migrations: AppDatabaseOptions<R>["migrations"]) {
|
|
|
88
72
|
}
|
|
89
73
|
}
|
|
90
74
|
|
|
91
|
-
function waitForMigrations<R>(migrations: AppDatabaseOptions<R>["migrations"]) {
|
|
75
|
+
function waitForMigrations<R, E>(migrations: AppDatabaseOptions<R, E>["migrations"]) {
|
|
92
76
|
return Effect.gen(function* () {
|
|
93
77
|
const progressInterval = Duration.fromInputUnsafe(migrations.progressInterval ?? "30 seconds");
|
|
94
78
|
let pending: PendingPostgresMigrations = { components: [], migrations: [] };
|
|
@@ -113,10 +97,10 @@ function waitForMigrations<R>(migrations: AppDatabaseOptions<R>["migrations"]) {
|
|
|
113
97
|
}).pipe(
|
|
114
98
|
Effect.retry({
|
|
115
99
|
schedule: Schedule.max([Schedule.spaced(migrations.pollInterval ?? "1 second"), Schedule.during(migrations.waitTimeout ?? "5 minutes")]),
|
|
116
|
-
while: (error) => error
|
|
100
|
+
while: (error) => error instanceof Migrator.MigrationError && error.kind === "BadState",
|
|
117
101
|
}),
|
|
118
102
|
Effect.tapError((error) =>
|
|
119
|
-
error
|
|
103
|
+
error instanceof Migrator.MigrationError && error.kind === "BadState" ? log("Timed out waiting for database migrations") : Effect.void,
|
|
120
104
|
),
|
|
121
105
|
);
|
|
122
106
|
if (progress !== undefined) {
|
package/src/hostRpcRoutes.ts
CHANGED
|
@@ -49,14 +49,14 @@ export function hostRpcRoutes<TRpcs extends Rpc.Any, THandlers, TError, TRequire
|
|
|
49
49
|
options.rpcPath ?? "/rpc",
|
|
50
50
|
socketRpc(websocket, options, trustedProxy, nativeOrigins).pipe(Effect.provideService(RpcSerialization.RpcSerialization, wire)),
|
|
51
51
|
);
|
|
52
|
-
yield* router.add("POST", options.rpcPath ?? "/rpc", (request) =>
|
|
53
|
-
|
|
54
|
-
return guarded.pipe(
|
|
52
|
+
yield* router.add("POST", options.rpcPath ?? "/rpc", (request) =>
|
|
53
|
+
guarded.pipe(
|
|
55
54
|
Effect.provideService(HttpIncomingMessage.MaxBodySize, ByteSize.bytes(bodySize)),
|
|
56
55
|
Effect.provideService(HttpServerRequest.HttpServerRequest, boundedRpcRequest(request)),
|
|
57
56
|
Effect.provideService(RpcSerialization.RpcSerialization, wire),
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
HttpEffect.withPreResponseHandler(rpcCacheControl),
|
|
58
|
+
),
|
|
59
|
+
);
|
|
60
60
|
}),
|
|
61
61
|
).pipe(
|
|
62
62
|
Layer.provide(
|
package/src/hostTransport.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { Config, Effect, Option, Result, Schema } from "effect";
|
|
|
2
2
|
import * as NetAddress from "effect/net/NetAddress";
|
|
3
3
|
import { TrustedProxy } from "@shivaedev/identity/server/trustedProxy.ts";
|
|
4
4
|
import { NativeOrigins } from "#nativeClientOrigins.ts";
|
|
5
|
+
import { TrustedProxyRequired } from "#TrustedProxyRequired.ts";
|
|
5
6
|
|
|
6
7
|
interface Options {
|
|
7
8
|
readonly config?: { readonly host?: string; readonly maxBodySize?: number; readonly trustedProxy?: string };
|
|
@@ -28,9 +29,7 @@ export const hostTransport = Effect.fn("Platform.hostTransport")(function* (opti
|
|
|
28
29
|
onSuccess: (address) => NetAddress.isLoopback(NetAddress.toCanonical(address)),
|
|
29
30
|
});
|
|
30
31
|
if (options.policy !== "loopback" && !loopback && trustedProxy === undefined) {
|
|
31
|
-
yield* Effect.
|
|
32
|
-
"RPC forwarding is refused without a trusted proxy. Set TRUSTED_PROXY or config.trustedProxy for this non-loopback HOST.",
|
|
33
|
-
);
|
|
32
|
+
return yield* Effect.fail(new TrustedProxyRequired());
|
|
34
33
|
}
|
|
35
34
|
const nativeOrigins = yield* Schema.decodeUnknownEffect(NativeOrigins)(options.nativeOrigins ?? []).pipe(
|
|
36
35
|
Effect.mapError((cause) => new Config.ConfigError(cause)),
|
|
@@ -46,7 +46,7 @@ export function nodeSubscriptionSignal(options: NodeSubscriptionSignalOptions):
|
|
|
46
46
|
}
|
|
47
47
|
nodeRequest.once("close", onRequestClose);
|
|
48
48
|
socket.once("close", onSocketClose);
|
|
49
|
-
if (nodeRequest.destroyed || socket.destroyed) {
|
|
49
|
+
if ((nodeRequest.destroyed && !nodeRequest.complete) || socket.destroyed) {
|
|
50
50
|
controller.abort();
|
|
51
51
|
}
|
|
52
52
|
let disposed = false;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"databaseAddress.d.ts","sourceRoot":"","sources":["../src/databaseAddress.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAC5D,OAAO,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC;AAElC,wBAAgB,eAAe,CAAC,KAAK,EAAE,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAAC,CAqBrG"}
|
package/dist/databaseAddress.js
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { Redacted } from "effect";
|
|
2
|
-
export function databaseAddress(value) {
|
|
3
|
-
try {
|
|
4
|
-
const url = new URL(Redacted.value(value));
|
|
5
|
-
if (url.protocol !== "postgres:" && url.protocol !== "postgresql:") {
|
|
6
|
-
return {};
|
|
7
|
-
}
|
|
8
|
-
const hostname = url.hostname.startsWith("[") ? url.hostname.slice(1, -1) : decodeURIComponent(url.hostname);
|
|
9
|
-
// PgConnection applies query parameters in order, after the authority.
|
|
10
|
-
const host = url.searchParams.getAll("host").at(-1) ?? hostname;
|
|
11
|
-
const rawPort = url.searchParams.getAll("port").at(-1) ?? url.port;
|
|
12
|
-
const port = rawPort === "" ? undefined : Number(rawPort);
|
|
13
|
-
if (host.includes(",") || (port !== undefined && (!Number.isInteger(port) || port < 1 || port > 65_535))) {
|
|
14
|
-
return {};
|
|
15
|
-
}
|
|
16
|
-
return {
|
|
17
|
-
...(host === "" && !url.searchParams.has("host") ? {} : { host }),
|
|
18
|
-
...(port === undefined ? {} : { port }),
|
|
19
|
-
};
|
|
20
|
-
}
|
|
21
|
-
catch {
|
|
22
|
-
return {};
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
//# sourceMappingURL=databaseAddress.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"databaseAddress.js","sourceRoot":"","sources":["../src/databaseAddress.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC;AAElC,MAAM,UAAU,eAAe,CAAC,KAAgC;IAC/D,IAAI,CAAC;QACJ,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC;QAC3C,IAAI,GAAG,CAAC,QAAQ,KAAK,WAAW,IAAI,GAAG,CAAC,QAAQ,KAAK,aAAa,EAAE,CAAC;YACpE,OAAO,EAAE,CAAC;QACX,CAAC;QACD,MAAM,QAAQ,GAAG,GAAG,CAAC,QAAQ,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,kBAAkB,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QAC7G,uEAAuE;QACvE,MAAM,IAAI,GAAG,GAAG,CAAC,YAAY,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,QAAQ,CAAC;QAChE,MAAM,OAAO,GAAG,GAAG,CAAC,YAAY,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,IAAI,CAAC;QACnE,MAAM,IAAI,GAAG,OAAO,KAAK,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QAC1D,IAAI,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,KAAK,SAAS,IAAI,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,IAAI,GAAG,CAAC,IAAI,IAAI,GAAG,MAAM,CAAC,CAAC,EAAE,CAAC;YAC1G,OAAO,EAAE,CAAC;QACX,CAAC;QACD,OAAO;YACN,GAAG,CAAC,IAAI,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC;YACjE,GAAG,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC;SACvC,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,EAAE,CAAC;IACX,CAAC;AACF,CAAC"}
|
package/src/databaseAddress.ts
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import type { PgPoolConfig } from "@effect/sql-pg/PgClient";
|
|
2
|
-
import { Redacted } from "effect";
|
|
3
|
-
|
|
4
|
-
export function databaseAddress(value: Redacted.Redacted<string>): Pick<PgPoolConfig, "host" | "port"> {
|
|
5
|
-
try {
|
|
6
|
-
const url = new URL(Redacted.value(value));
|
|
7
|
-
if (url.protocol !== "postgres:" && url.protocol !== "postgresql:") {
|
|
8
|
-
return {};
|
|
9
|
-
}
|
|
10
|
-
const hostname = url.hostname.startsWith("[") ? url.hostname.slice(1, -1) : decodeURIComponent(url.hostname);
|
|
11
|
-
// PgConnection applies query parameters in order, after the authority.
|
|
12
|
-
const host = url.searchParams.getAll("host").at(-1) ?? hostname;
|
|
13
|
-
const rawPort = url.searchParams.getAll("port").at(-1) ?? url.port;
|
|
14
|
-
const port = rawPort === "" ? undefined : Number(rawPort);
|
|
15
|
-
if (host.includes(",") || (port !== undefined && (!Number.isInteger(port) || port < 1 || port > 65_535))) {
|
|
16
|
-
return {};
|
|
17
|
-
}
|
|
18
|
-
return {
|
|
19
|
-
...(host === "" && !url.searchParams.has("host") ? {} : { host }),
|
|
20
|
-
...(port === undefined ? {} : { port }),
|
|
21
|
-
};
|
|
22
|
-
} catch {
|
|
23
|
-
return {};
|
|
24
|
-
}
|
|
25
|
-
}
|