matrix-nodejs-homeserver 0.3.1 → 0.3.2
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 +61 -5
- package/dist/adapters/cluster.d.ts +1 -26
- package/dist/adapters/cluster.js +50 -103
- package/dist/api/account.js +22 -3
- package/dist/api/admin.js +9 -4
- package/dist/api/media.js +1 -1
- package/dist/api/preview.js +33 -70
- package/dist/api/sync.js +268 -87
- package/dist/core/cluster.d.ts +12 -5
- package/dist/core/cluster.js +46 -29
- package/dist/core/html.js +13 -0
- package/dist/core/identity.js +1 -1
- package/dist/core/json.js +10 -1
- package/dist/core/keys.d.ts +10 -1
- package/dist/core/keys.js +60 -41
- package/dist/core/outbound.d.ts +16 -0
- package/dist/core/outbound.js +172 -0
- package/dist/core/push.js +29 -17
- package/dist/core/ratelimit.d.ts +1 -1
- package/dist/core/rooms.d.ts +10 -1
- package/dist/core/rooms.js +25 -0
- package/dist/core/semaphore.js +23 -0
- package/dist/core/sso.d.ts +15 -1
- package/dist/core/sso.js +38 -2
- package/dist/core/threepid.js +1 -9
- package/dist/federation/client.js +7 -0
- package/dist/federation/index.d.ts +1 -1
- package/dist/federation/index.js +13 -6
- package/dist/homeserver.d.ts +21 -3
- package/dist/homeserver.js +11 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -2
- package/dist/storage/batch.js +11 -0
- package/dist/storage/memory.js +1 -0
- package/dist/storage/schema.js +1 -1
- package/dist/storage/sql.js +20 -1
- package/dist/storage/types.d.ts +16 -0
- package/package.json +6 -2
|
@@ -2,6 +2,7 @@ import { canonicalJson } from "../core/json.js";
|
|
|
2
2
|
import { unpaddedBase64 } from "../core/crypto.js";
|
|
3
3
|
import { M } from "../core/m.js";
|
|
4
4
|
import { MatrixError } from "../core/errors.js";
|
|
5
|
+
import { checkLiteral } from "../core/outbound.js";
|
|
5
6
|
import { sign } from "node:crypto";
|
|
6
7
|
import { resolveSrv } from "node:dns/promises";
|
|
7
8
|
import { Agent, request } from "node:https";
|
|
@@ -169,6 +170,11 @@ var FederationClient = class {
|
|
|
169
170
|
return null;
|
|
170
171
|
}
|
|
171
172
|
raw(target, method, pathWithQuery, body, headers, timeoutMs, poolKey = target.hostHeader) {
|
|
173
|
+
try {
|
|
174
|
+
checkLiteral(this.hs.addressPolicy, target.host);
|
|
175
|
+
} catch (err) {
|
|
176
|
+
return Promise.reject(err);
|
|
177
|
+
}
|
|
172
178
|
return new Promise((resolve, reject) => {
|
|
173
179
|
let connectTimer = null;
|
|
174
180
|
const req = request({
|
|
@@ -178,6 +184,7 @@ var FederationClient = class {
|
|
|
178
184
|
path: pathWithQuery,
|
|
179
185
|
servername: isIP(target.tlsName) ? void 0 : target.tlsName,
|
|
180
186
|
agent: this.agentFor(poolKey),
|
|
187
|
+
lookup: this.hs.addressPolicy.lookup,
|
|
181
188
|
headers: {
|
|
182
189
|
host: target.hostHeader,
|
|
183
190
|
...headers,
|
|
@@ -136,7 +136,7 @@ export declare class FederationService {
|
|
|
136
136
|
*/
|
|
137
137
|
backfill(roomId: string, fromEventIds: string[], limit?: number): Promise<number>;
|
|
138
138
|
/** Download remote media (authenticated federation endpoint, falling back to the legacy one). */
|
|
139
|
-
fetchMedia(server: string, mediaId: string): Promise<{
|
|
139
|
+
fetchMedia(server: string, mediaId: string, maxBytes: number): Promise<{
|
|
140
140
|
data: Uint8Array;
|
|
141
141
|
contentType: string | null;
|
|
142
142
|
filename: string | null;
|
package/dist/federation/index.js
CHANGED
|
@@ -8,6 +8,7 @@ import { authStateFromAuthEvents, checkAuth, getState, membershipOf } from "../c
|
|
|
8
8
|
import { globMatch } from "../core/glob.js";
|
|
9
9
|
import { resolveState } from "../core/stateres.js";
|
|
10
10
|
import { MissingStateError } from "../core/rooms.js";
|
|
11
|
+
import { publicGet } from "../core/outbound.js";
|
|
11
12
|
import { FederationClient } from "./client.js";
|
|
12
13
|
import { KeyRing } from "./keyring.js";
|
|
13
14
|
import { TransactionSender } from "./sender.js";
|
|
@@ -961,13 +962,16 @@ var FederationService = class {
|
|
|
961
962
|
return 0;
|
|
962
963
|
}
|
|
963
964
|
/** Download remote media (authenticated federation endpoint, falling back to the legacy one). */
|
|
964
|
-
async fetchMedia(server, mediaId) {
|
|
965
|
+
async fetchMedia(server, mediaId, maxBytes) {
|
|
965
966
|
this.requireEnabled();
|
|
966
967
|
const res = await this.client.requestRaw(server, "GET", `/_matrix/federation/v1/media/download/${encodeURIComponent(mediaId)}`, { timeoutMs: 6e4 });
|
|
967
968
|
if (res.status === 200) {
|
|
968
969
|
const ct = String(res.headers["content-type"] ?? "");
|
|
969
970
|
const m = /boundary=("?)([^";]+)\1/.exec(ct);
|
|
970
|
-
if (ct.startsWith("multipart/") && m) return parseMultipartMedia(res.body, m[2],
|
|
971
|
+
if (ct.startsWith("multipart/") && m) return parseMultipartMedia(res.body, m[2], {
|
|
972
|
+
maxBytes,
|
|
973
|
+
fetch: this.hs.publicFetch
|
|
974
|
+
});
|
|
971
975
|
}
|
|
972
976
|
const legacy = await this.client.requestRaw(server, "GET", `/_matrix/media/v3/download/${encodeURIComponent(server)}/${encodeURIComponent(mediaId)}`, {
|
|
973
977
|
query: { allow_remote: "false" },
|
|
@@ -1020,7 +1024,7 @@ function filenameFromDisposition(header) {
|
|
|
1020
1024
|
return plain ? plain[1] : null;
|
|
1021
1025
|
}
|
|
1022
1026
|
/** Parse a federation media multipart/mixed response: a JSON part then the file (or a redirect). */
|
|
1023
|
-
async function parseMultipartMedia(body, boundary,
|
|
1027
|
+
async function parseMultipartMedia(body, boundary, limits) {
|
|
1024
1028
|
const delimiter = Buffer.from(`--${boundary}`);
|
|
1025
1029
|
const parts = [];
|
|
1026
1030
|
let idx = body.indexOf(delimiter);
|
|
@@ -1041,10 +1045,13 @@ async function parseMultipartMedia(body, boundary, fed) {
|
|
|
1041
1045
|
if (i > 0) headers[line.slice(0, i).trim().toLowerCase()] = line.slice(i + 1).trim();
|
|
1042
1046
|
}
|
|
1043
1047
|
if (headers.location) {
|
|
1044
|
-
const res = await fetch
|
|
1045
|
-
|
|
1048
|
+
const res = await publicGet(limits.fetch, headers.location, {
|
|
1049
|
+
maxBytes: limits.maxBytes,
|
|
1050
|
+
timeoutMs: 6e4
|
|
1051
|
+
});
|
|
1052
|
+
if (res.status !== 200) throw errors.notFound("Remote media redirect failed");
|
|
1046
1053
|
return {
|
|
1047
|
-
data: new Uint8Array(
|
|
1054
|
+
data: new Uint8Array(res.body),
|
|
1048
1055
|
contentType: res.headers.get("content-type"),
|
|
1049
1056
|
filename: filenameFromDisposition(res.headers.get("content-disposition") ?? void 0)
|
|
1050
1057
|
};
|
package/dist/homeserver.d.ts
CHANGED
|
@@ -6,10 +6,10 @@ import { IdentityServers } from "./core/identity.js";
|
|
|
6
6
|
import { M } from "./core/m.js";
|
|
7
7
|
import { AppServiceRegistration, AppServiceService } from "./core/appservices.js";
|
|
8
8
|
import { AuthService } from "./core/auth.js";
|
|
9
|
+
import { DatabaseAdapter } from "./storage/types.js";
|
|
9
10
|
import { KeyService } from "./core/keys.js";
|
|
10
11
|
import { ClusterAdapter, StreamManager } from "./core/cluster.js";
|
|
11
12
|
import { Notifier } from "./core/notifier.js";
|
|
12
|
-
import { DatabaseAdapter } from "./storage/types.js";
|
|
13
13
|
import { RoomService } from "./core/rooms.js";
|
|
14
14
|
import { TypingService } from "./core/typing.js";
|
|
15
15
|
import { PushService } from "./core/push.js";
|
|
@@ -26,6 +26,7 @@ import { SettingsService } from "./core/settings.js";
|
|
|
26
26
|
import { AdminRoomOptions, AdminRoomService } from "./core/adminroom.js";
|
|
27
27
|
import { ResolvedUserAuth, UserAuthAdapter } from "./adapters/auth.js";
|
|
28
28
|
import { MediaAdapter, Thumbnailer } from "./adapters/media.js";
|
|
29
|
+
import { AddressPolicy } from "./core/outbound.js";
|
|
29
30
|
import { EventEmitter } from "node:events";
|
|
30
31
|
//#region src/homeserver.d.ts
|
|
31
32
|
export interface Logger {
|
|
@@ -82,6 +83,11 @@ export interface HomeserverOptions {
|
|
|
82
83
|
accessTokenLifetimeMs?: number;
|
|
83
84
|
/** Maximum `/sync` long-poll duration in ms. Default: 30000. */
|
|
84
85
|
maxSyncTimeoutMs?: number;
|
|
86
|
+
/**
|
|
87
|
+
* How many `/sync` responses one process builds at once (default 4). Requests woken together
|
|
88
|
+
* wait in the process instead of queueing their database reads ahead of everything else.
|
|
89
|
+
*/
|
|
90
|
+
maxConcurrentSyncs?: number;
|
|
85
91
|
/**
|
|
86
92
|
* Enables `/preview_url`. Off by default because it makes the server fetch arbitrary URLs.
|
|
87
93
|
* Private/internal addresses stay blocked unless `allowPrivateAddresses` is set.
|
|
@@ -92,6 +98,13 @@ export interface HomeserverOptions {
|
|
|
92
98
|
logger?: Logger;
|
|
93
99
|
/** Custom `fetch` for outgoing HTTP requests. */
|
|
94
100
|
fetch?: typeof globalThis.fetch;
|
|
101
|
+
/**
|
|
102
|
+
* Let requests to addresses that users or remote servers choose (federation, push gateways,
|
|
103
|
+
* identity servers, URL previews, remote media) reach private addresses: localhost, LAN and
|
|
104
|
+
* other non-public ranges. `true` allows all of them, a list allows those ranges only (e.g.
|
|
105
|
+
* `["127.0.0.1/32"]` for a push gateway on the same machine). Default: none.
|
|
106
|
+
*/
|
|
107
|
+
allowPrivateAddresses?: boolean | string[];
|
|
95
108
|
/**
|
|
96
109
|
* Rate limiting. `false` disables it; otherwise per-action overrides and an optional
|
|
97
110
|
* shared limiter (defaults to an in-memory token bucket). Admins are never limited.
|
|
@@ -102,7 +115,7 @@ export interface HomeserverOptions {
|
|
|
102
115
|
sso?: SsoOptions;
|
|
103
116
|
/**
|
|
104
117
|
* Coordination between instances. The default (`localCluster()`) supports one process;
|
|
105
|
-
* use `postgresCluster()`
|
|
118
|
+
* use `postgresCluster()` to scale horizontally.
|
|
106
119
|
*/
|
|
107
120
|
cluster?: ClusterAdapter;
|
|
108
121
|
/**
|
|
@@ -151,6 +164,7 @@ export interface ResolvedConfig {
|
|
|
151
164
|
defaultRoomVersion: string;
|
|
152
165
|
accessTokenLifetimeMs: number | null;
|
|
153
166
|
maxSyncTimeoutMs: number;
|
|
167
|
+
maxConcurrentSyncs: number;
|
|
154
168
|
urlPreviews: {
|
|
155
169
|
allowPrivateAddresses: boolean;
|
|
156
170
|
} | null;
|
|
@@ -189,8 +203,12 @@ export declare class Homeserver extends EventEmitter {
|
|
|
189
203
|
readonly policyServers: PolicyServers;
|
|
190
204
|
readonly threepids: ThreepidService;
|
|
191
205
|
readonly identityServers: IdentityServers;
|
|
192
|
-
/** `fetch`
|
|
206
|
+
/** `fetch` for requests to addresses the operator configured (OAuth, SSO, application services). */
|
|
193
207
|
readonly fetch: typeof globalThis.fetch;
|
|
208
|
+
/** Which addresses requests to user- or remote-chosen destinations may reach (see `allowPrivateAddresses`). */
|
|
209
|
+
readonly addressPolicy: AddressPolicy;
|
|
210
|
+
/** `fetch` for user- or remote-chosen URLs: refuses private addresses unless allowed, never follows redirects. */
|
|
211
|
+
readonly publicFetch: typeof globalThis.fetch;
|
|
194
212
|
/** Resolves once storage is migrated and the server is ready to serve requests. */
|
|
195
213
|
readonly ready: Promise<void>;
|
|
196
214
|
private _signingKey;
|
package/dist/homeserver.js
CHANGED
|
@@ -3,6 +3,7 @@ import "./core/m.js";
|
|
|
3
3
|
import { SUPPORTED_ROOM_VERSIONS } from "./core/events.js";
|
|
4
4
|
import { PartialStateService } from "./core/partialstate.js";
|
|
5
5
|
import { RoomService } from "./core/rooms.js";
|
|
6
|
+
import { addressPolicy, policyFetch } from "./core/outbound.js";
|
|
6
7
|
import { FederationService } from "./federation/index.js";
|
|
7
8
|
import { PolicyServers } from "./core/policy.js";
|
|
8
9
|
import { ThreepidService } from "./core/threepid.js";
|
|
@@ -13,9 +14,9 @@ import { Notifier } from "./core/notifier.js";
|
|
|
13
14
|
import { StreamManager, localCluster, newInstanceId } from "./core/cluster.js";
|
|
14
15
|
import { TypingService } from "./core/typing.js";
|
|
15
16
|
import { PushService } from "./core/push.js";
|
|
17
|
+
import { SsoService } from "./core/sso.js";
|
|
16
18
|
import { AdminService } from "./core/admin.js";
|
|
17
19
|
import { OAuthDelegation } from "./core/oauth.js";
|
|
18
|
-
import { SsoService } from "./core/sso.js";
|
|
19
20
|
import { DelayedEventService } from "./core/delayed.js";
|
|
20
21
|
import { AuditService } from "./core/audit.js";
|
|
21
22
|
import { SettingsService } from "./core/settings.js";
|
|
@@ -78,8 +79,12 @@ var Homeserver = class extends EventEmitter {
|
|
|
78
79
|
policyServers;
|
|
79
80
|
threepids;
|
|
80
81
|
identityServers;
|
|
81
|
-
/** `fetch`
|
|
82
|
+
/** `fetch` for requests to addresses the operator configured (OAuth, SSO, application services). */
|
|
82
83
|
fetch;
|
|
84
|
+
/** Which addresses requests to user- or remote-chosen destinations may reach (see `allowPrivateAddresses`). */
|
|
85
|
+
addressPolicy;
|
|
86
|
+
/** `fetch` for user- or remote-chosen URLs: refuses private addresses unless allowed, never follows redirects. */
|
|
87
|
+
publicFetch;
|
|
83
88
|
/** Resolves once storage is migrated and the server is ready to serve requests. */
|
|
84
89
|
ready;
|
|
85
90
|
_signingKey = null;
|
|
@@ -109,6 +114,7 @@ var Homeserver = class extends EventEmitter {
|
|
|
109
114
|
defaultRoomVersion,
|
|
110
115
|
accessTokenLifetimeMs: options.accessTokenLifetimeMs ?? null,
|
|
111
116
|
maxSyncTimeoutMs: options.maxSyncTimeoutMs ?? 3e4,
|
|
117
|
+
maxConcurrentSyncs: Math.max(1, options.maxConcurrentSyncs ?? 4),
|
|
112
118
|
urlPreviews: options.urlPreviews ? { allowPrivateAddresses: typeof options.urlPreviews === "object" && !!options.urlPreviews.allowPrivateAddresses } : null,
|
|
113
119
|
thumbnailer: options.thumbnailer ?? null,
|
|
114
120
|
rtc: options.rtc ?? {},
|
|
@@ -138,6 +144,9 @@ var Homeserver = class extends EventEmitter {
|
|
|
138
144
|
this.push = new PushService(this);
|
|
139
145
|
this.admin = new AdminService(this);
|
|
140
146
|
this.oauth = options.oauth ? new OAuthDelegation(this, options.oauth) : null;
|
|
147
|
+
this.fetch = options.fetch ?? globalThis.fetch;
|
|
148
|
+
this.addressPolicy = addressPolicy(options.allowPrivateAddresses);
|
|
149
|
+
this.publicFetch = policyFetch(this.addressPolicy, options.fetch);
|
|
141
150
|
this.federation = new FederationService(this, options.federation);
|
|
142
151
|
this.delayedEvents = new DelayedEventService(this, options.delayedEvents);
|
|
143
152
|
this.audit = new AuditService(this, options.audit);
|
|
@@ -157,7 +166,6 @@ var Homeserver = class extends EventEmitter {
|
|
|
157
166
|
this.cleanupTimer = setInterval(() => void this.cleanup().catch((err) => this.logger.warn("Cleanup failed", err)), cleanupEvery);
|
|
158
167
|
this.cleanupTimer.unref?.();
|
|
159
168
|
}
|
|
160
|
-
this.fetch = options.fetch ?? globalThis.fetch;
|
|
161
169
|
registerRoutes(this.router);
|
|
162
170
|
this.ready = this.init(options);
|
|
163
171
|
this.ready.catch((err) => this.logger.error("Homeserver failed to start", err));
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { ThreepidEmail, ThreepidOptions, ThreepidPurpose, ThreepidSms } from "./core/threepid.js";
|
|
2
2
|
import { M, UNSTABLE } from "./core/m.js";
|
|
3
3
|
import { AppServiceRegistration, loadAppServiceRegistrations, parseAppServiceRegistration } from "./core/appservices.js";
|
|
4
|
-
import { ClusterAdapter, localCluster } from "./core/cluster.js";
|
|
5
4
|
import { DatabaseAdapter, FieldDef, FindManyOptions, ModelDef, Operator, Row, Schema, WhereClause } from "./storage/types.js";
|
|
5
|
+
import { ClusterAdapter, localCluster } from "./core/cluster.js";
|
|
6
6
|
import { AdminService, ServerNoticesOptions } from "./core/admin.js";
|
|
7
7
|
import { OAuthDelegationOptions } from "./core/oauth.js";
|
|
8
8
|
import { SsoOptions, SsoProvider } from "./core/sso.js";
|
|
@@ -22,5 +22,5 @@ import { memoryAdapter } from "./storage/memory.js";
|
|
|
22
22
|
import { MysqlPoolLike, PgPoolLike, SqlAdapterOptions, SqlDialect, SqlExecutor, SqliteDatabaseLike, mysqlAdapter, postgresAdapter, sqlAdapter, sqliteAdapter } from "./storage/sql.js";
|
|
23
23
|
import { schema } from "./storage/schema.js";
|
|
24
24
|
import { S3MediaOptions, s3Media } from "./adapters/s3.js";
|
|
25
|
-
import { PgClusterPoolLike,
|
|
26
|
-
export { type AdminRoomOptions, type AdminService, type AppServiceRegistration, type AuditAction, type AuditEntry, type AuditOptions, type ClusterAdapter, type DatabaseAdapter, type DelayedEventsOptions, type FederationOptions, type FieldDef, type FindManyOptions, Homeserver, type HomeserverOptions, type Logger, M, MatrixError, type MediaAdapter, type MediaMetadata, type ModelDef, type MysqlPoolLike, type NodeHandler, type OAuthDelegationOptions, type Operator, type PasswordCheckContext, type PgClusterPoolLike, type PgPoolLike, type RateLimitAction, type RateLimitRule, type RateLimiter, type
|
|
25
|
+
import { PgClusterPoolLike, postgresCluster } from "./adapters/cluster.js";
|
|
26
|
+
export { type AdminRoomOptions, type AdminService, type AppServiceRegistration, type AuditAction, type AuditEntry, type AuditOptions, type ClusterAdapter, type DatabaseAdapter, type DelayedEventsOptions, type FederationOptions, type FieldDef, type FindManyOptions, Homeserver, type HomeserverOptions, type Logger, M, MatrixError, type MediaAdapter, type MediaMetadata, type ModelDef, type MysqlPoolLike, type NodeHandler, type OAuthDelegationOptions, type Operator, type PasswordCheckContext, type PgClusterPoolLike, type PgPoolLike, type RateLimitAction, type RateLimitRule, type RateLimiter, type Row, type RtcOptions, type S3MediaOptions, type Schema, type ServerNoticesOptions, type SqlAdapterOptions, type SqlDialect, type SqlExecutor, type SqliteDatabaseLike, type SsoOptions, type SsoProvider, type ThreepidEmail, type ThreepidOptions, type ThreepidPurpose, type ThreepidSms, type ThumbnailRequest, type Thumbnailer, type TurnOptions, UNSTABLE, type UserAuthAdapter, type WhereClause, createHomeserver, filesystemMedia, getExpressHandler, getFastifyPlugin, getFetchHandler, getHonoHandler, getNodeHandler, handleRequest, isMatrixPath, loadAppServiceRegistrations, localCluster, memoryAdapter, memoryMedia, memoryRateLimiter, mysqlAdapter, parseAppServiceRegistration, postgresAdapter, postgresCluster, s3Media, schema, sharpThumbnailer, sqlAdapter, sqliteAdapter };
|
package/dist/index.js
CHANGED
|
@@ -11,5 +11,5 @@ import { Homeserver, createHomeserver } from "./homeserver.js";
|
|
|
11
11
|
import { getExpressHandler, getFastifyPlugin, getFetchHandler, getHonoHandler, getNodeHandler, isMatrixPath } from "./http/adapters.js";
|
|
12
12
|
import { mysqlAdapter, postgresAdapter, sqlAdapter, sqliteAdapter } from "./storage/sql.js";
|
|
13
13
|
import { s3Media } from "./adapters/s3.js";
|
|
14
|
-
import { postgresCluster
|
|
15
|
-
export { Homeserver, M, MatrixError, UNSTABLE, createHomeserver, filesystemMedia, getExpressHandler, getFastifyPlugin, getFetchHandler, getHonoHandler, getNodeHandler, handleRequest, isMatrixPath, loadAppServiceRegistrations, localCluster, memoryAdapter, memoryMedia, memoryRateLimiter, mysqlAdapter, parseAppServiceRegistration, postgresAdapter, postgresCluster,
|
|
14
|
+
import { postgresCluster } from "./adapters/cluster.js";
|
|
15
|
+
export { Homeserver, M, MatrixError, UNSTABLE, createHomeserver, filesystemMedia, getExpressHandler, getFastifyPlugin, getFetchHandler, getHonoHandler, getNodeHandler, handleRequest, isMatrixPath, loadAppServiceRegistrations, localCluster, memoryAdapter, memoryMedia, memoryRateLimiter, mysqlAdapter, parseAppServiceRegistration, postgresAdapter, postgresCluster, s3Media, schema, sharpThumbnailer, sqlAdapter, sqliteAdapter };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
//#region src/storage/batch.ts
|
|
2
|
+
/**
|
|
3
|
+
* Run independent reads together: in one round trip when the adapter can (`findManyBatch`),
|
|
4
|
+
* otherwise concurrently. Results come in the order of the queries.
|
|
5
|
+
*/
|
|
6
|
+
function readBatch(db, queries) {
|
|
7
|
+
if (db.findManyBatch) return db.findManyBatch(queries);
|
|
8
|
+
return Promise.all(queries.map((query) => db.findMany(query.model, query.opts)));
|
|
9
|
+
}
|
|
10
|
+
//#endregion
|
|
11
|
+
export { readBatch };
|
package/dist/storage/memory.js
CHANGED
|
@@ -72,6 +72,7 @@ function memoryAdapter() {
|
|
|
72
72
|
rows.sort((a, b) => a[field] < b[field] ? -dir : a[field] > b[field] ? dir : 0);
|
|
73
73
|
}
|
|
74
74
|
if (opts.limit !== void 0) rows = rows.slice(0, opts.limit);
|
|
75
|
+
if (opts.fields?.length) return rows.map((r) => Object.fromEntries(opts.fields.map((f) => [f, structuredClone(r[f] ?? null)])));
|
|
75
76
|
return rows.map((r) => structuredClone(r));
|
|
76
77
|
},
|
|
77
78
|
async update(model, where, data) {
|
package/dist/storage/schema.js
CHANGED
package/dist/storage/sql.js
CHANGED
|
@@ -130,11 +130,30 @@ function sqlAdapter({ dialect, executor: rootExecutor, tablePrefix = "", transac
|
|
|
130
130
|
},
|
|
131
131
|
async findMany(model, opts = {}) {
|
|
132
132
|
const params = [];
|
|
133
|
-
let sql = `SELECT * FROM ${t(model)}${buildWhere(model, opts.where, params)}`;
|
|
133
|
+
let sql = `SELECT ${opts.fields?.length ? opts.fields.map(q).join(", ") : "*"} FROM ${t(model)}${buildWhere(model, opts.where, params)}`;
|
|
134
134
|
if (opts.sortBy) sql += ` ORDER BY ${q(opts.sortBy.field)} ${opts.sortBy.direction === "asc" ? "ASC" : "DESC"}`;
|
|
135
135
|
if (opts.limit !== void 0) sql += ` LIMIT ${Math.max(0, Math.floor(opts.limit))}`;
|
|
136
136
|
return (await executor.query(sql, params)).map((r) => fromDb(model, r));
|
|
137
137
|
},
|
|
138
|
+
...dialect === "postgres" ? { async findManyBatch(queries) {
|
|
139
|
+
if (queries.length === 0) return [];
|
|
140
|
+
const params = [];
|
|
141
|
+
const parts = queries.map(({ model, opts = {} }, i) => {
|
|
142
|
+
const fields = opts.fields?.length && opts.sortBy && !opts.fields.includes(opts.sortBy.field) ? [...opts.fields, opts.sortBy.field] : opts.fields;
|
|
143
|
+
let inner = `SELECT ${fields?.length ? fields.map(q).join(", ") : "*"} FROM ${t(model)}${buildWhere(model, opts.where, params)}`;
|
|
144
|
+
const order = opts.sortBy ? `${q(opts.sortBy.field)} ${opts.sortBy.direction === "asc" ? "ASC" : "DESC"}` : "";
|
|
145
|
+
if (order) inner += ` ORDER BY ${order}`;
|
|
146
|
+
if (opts.limit !== void 0) inner += ` LIMIT ${Math.max(0, Math.floor(opts.limit))}`;
|
|
147
|
+
return `SELECT ${i} AS i, row_number() OVER (${order ? `ORDER BY ${order.replace(/^"/, "b.\"")}` : ""}) AS n, to_jsonb(b) AS r FROM (${inner}) b`;
|
|
148
|
+
});
|
|
149
|
+
const rows = await executor.query(`${parts.join(" UNION ALL ")} ORDER BY i, n`, params);
|
|
150
|
+
const out = queries.map(() => []);
|
|
151
|
+
for (const row of rows) {
|
|
152
|
+
const r = typeof row.r === "string" ? JSON.parse(row.r) : row.r;
|
|
153
|
+
out[Number(row.i)].push(fromDb(queries[Number(row.i)].model, r));
|
|
154
|
+
}
|
|
155
|
+
return out;
|
|
156
|
+
} } : {},
|
|
138
157
|
async update(model, where, data) {
|
|
139
158
|
const fields = Object.keys(data).filter((k) => data[k] !== void 0);
|
|
140
159
|
if (fields.length === 0) return 0;
|
package/dist/storage/types.d.ts
CHANGED
|
@@ -21,8 +21,18 @@ export interface WhereClause {
|
|
|
21
21
|
operator?: Operator;
|
|
22
22
|
}
|
|
23
23
|
export type Row = Record<string, any>;
|
|
24
|
+
/** One read of a `findManyBatch`. */
|
|
25
|
+
export interface BatchQuery {
|
|
26
|
+
model: string;
|
|
27
|
+
opts?: FindManyOptions;
|
|
28
|
+
}
|
|
24
29
|
export interface FindManyOptions {
|
|
25
30
|
where?: WhereClause[];
|
|
31
|
+
/**
|
|
32
|
+
* Only these fields are needed. Adapters should read just these (a smaller query and less to
|
|
33
|
+
* parse); one that ignores the option and returns whole rows is still correct.
|
|
34
|
+
*/
|
|
35
|
+
fields?: string[];
|
|
26
36
|
sortBy?: {
|
|
27
37
|
field: string;
|
|
28
38
|
direction: "asc" | "desc";
|
|
@@ -35,6 +45,12 @@ export interface FindManyOptions {
|
|
|
35
45
|
* All `where` clauses are combined with AND.
|
|
36
46
|
*/
|
|
37
47
|
export interface DatabaseAdapter {
|
|
48
|
+
/**
|
|
49
|
+
* Several independent reads in one go: the results in the same order as the queries. Optional;
|
|
50
|
+
* an adapter that talks to a database server can send them in one round trip. Without it,
|
|
51
|
+
* callers run the reads separately (see `readBatch`).
|
|
52
|
+
*/
|
|
53
|
+
findManyBatch?(queries: BatchQuery[]): Promise<Row[][]>;
|
|
38
54
|
/** Create tables / collections for the given schema. Must be idempotent. */
|
|
39
55
|
migrate?(schema: Schema): Promise<void>;
|
|
40
56
|
create(model: string, data: Row): Promise<void>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "matrix-nodejs-homeserver",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "A framework- and database-agnostic Matrix homeserver library for Node.js",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"chat",
|
|
@@ -41,10 +41,12 @@
|
|
|
41
41
|
},
|
|
42
42
|
"scripts": {
|
|
43
43
|
"typecheck": "tsc -p tsconfig.json",
|
|
44
|
+
"test": "node --test \"test/**/*.test.ts\"",
|
|
44
45
|
"lint": "oxlint",
|
|
45
46
|
"format": "oxfmt .",
|
|
46
47
|
"format:check": "oxfmt --check .",
|
|
47
48
|
"build": "tsdown",
|
|
49
|
+
"check:package": "publint && attw --pack . --profile esm-only",
|
|
48
50
|
"prepare": "npm run build",
|
|
49
51
|
"start:example": "node examples/node-http.ts",
|
|
50
52
|
"start:multi-node-example": "node examples/multi-node.ts",
|
|
@@ -53,15 +55,17 @@
|
|
|
53
55
|
"release": "changeset publish"
|
|
54
56
|
},
|
|
55
57
|
"devDependencies": {
|
|
58
|
+
"@arethetypeswrong/cli": "^0.18.5",
|
|
56
59
|
"@changesets/cli": "^3.0.3",
|
|
57
60
|
"@types/node": "^26.6.5",
|
|
58
61
|
"oxfmt": "^0.73.0",
|
|
59
62
|
"oxlint": "^1.88.0",
|
|
63
|
+
"publint": "^0.3.25",
|
|
60
64
|
"tsdown": "^0.23.1",
|
|
61
65
|
"typescript": "7.0.2",
|
|
62
66
|
"zod": "^4.6.7"
|
|
63
67
|
},
|
|
64
68
|
"engines": {
|
|
65
|
-
"node": ">=
|
|
69
|
+
"node": ">=22"
|
|
66
70
|
}
|
|
67
71
|
}
|