matrix-nodejs-homeserver 0.3.1 → 0.3.3
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 +89 -6
- 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/slidingsync.js +2 -2
- package/dist/api/sync.js +402 -134
- 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 +52 -34
- package/dist/core/pushrules.js +24 -5
- 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 +13 -4
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -2
- package/dist/storage/batch.js +82 -0
- package/dist/storage/memory.js +1 -0
- package/dist/storage/schema.js +8 -3
- package/dist/storage/sql.d.ts +22 -3
- package/dist/storage/sql.js +138 -21
- package/dist/storage/types.d.ts +30 -0
- package/package.json +6 -2
package/dist/storage/schema.js
CHANGED
|
@@ -192,7 +192,11 @@ const schema = {
|
|
|
192
192
|
ts: n
|
|
193
193
|
},
|
|
194
194
|
primaryKey: ["user_id", "event_id"],
|
|
195
|
-
indexes: [[
|
|
195
|
+
indexes: [[
|
|
196
|
+
"user_id",
|
|
197
|
+
"room_id",
|
|
198
|
+
"stream"
|
|
199
|
+
], ["user_id", "stream"]]
|
|
196
200
|
},
|
|
197
201
|
sso_identities: {
|
|
198
202
|
fields: {
|
|
@@ -249,7 +253,7 @@ const schema = {
|
|
|
249
253
|
forgotten: b
|
|
250
254
|
},
|
|
251
255
|
primaryKey: ["room_id", "user_id"],
|
|
252
|
-
indexes: [["user_id"]]
|
|
256
|
+
indexes: [["user_id"], ["stream"]]
|
|
253
257
|
},
|
|
254
258
|
room_aliases: {
|
|
255
259
|
fields: {
|
|
@@ -397,7 +401,8 @@ const schema = {
|
|
|
397
401
|
last_active_ts: n,
|
|
398
402
|
stream: n
|
|
399
403
|
},
|
|
400
|
-
primaryKey: ["user_id"]
|
|
404
|
+
primaryKey: ["user_id"],
|
|
405
|
+
indexes: [["stream"]]
|
|
401
406
|
},
|
|
402
407
|
media: {
|
|
403
408
|
fields: {
|
package/dist/storage/sql.d.ts
CHANGED
|
@@ -2,8 +2,10 @@ import { DatabaseAdapter, Row } from "./types.js";
|
|
|
2
2
|
//#region src/storage/sql.d.ts
|
|
3
3
|
export type SqlDialect = "sqlite" | "postgres" | "mysql";
|
|
4
4
|
export interface SqlExecutor {
|
|
5
|
-
/** Run a query that returns rows. */
|
|
6
|
-
query(sql: string, params: unknown[]
|
|
5
|
+
/** Run a query that returns rows; `prepare: false` when the statement is not worth preparing for reuse. */
|
|
6
|
+
query(sql: string, params: unknown[], options?: {
|
|
7
|
+
prepare?: boolean;
|
|
8
|
+
}): Promise<Row[]>;
|
|
7
9
|
/** Run a statement; resolve with the number of affected rows. */
|
|
8
10
|
execute(sql: string, params: unknown[]): Promise<number>;
|
|
9
11
|
}
|
|
@@ -36,9 +38,20 @@ export interface SqliteDatabaseLike {
|
|
|
36
38
|
/** SQLite adapter for `node:sqlite` (`new DatabaseSync(path)`) or `better-sqlite3`. */
|
|
37
39
|
export declare function sqliteAdapter(db: SqliteDatabaseLike, options?: {
|
|
38
40
|
tablePrefix?: string;
|
|
41
|
+
/**
|
|
42
|
+
* When commits reach the disk (SQLite's `synchronous`). `"normal"` (the default): at
|
|
43
|
+
* checkpoints; a commit survives a crash of the process, and a power loss or operating
|
|
44
|
+
* system crash can roll back the most recent ones, without corrupting the database.
|
|
45
|
+
* `"full"`: on every commit, which blocks the event loop for an fsync each time.
|
|
46
|
+
*/
|
|
47
|
+
synchronous?: "normal" | "full";
|
|
39
48
|
}): DatabaseAdapter;
|
|
40
49
|
interface PgQueryable {
|
|
41
|
-
query(
|
|
50
|
+
query(config: {
|
|
51
|
+
text: string;
|
|
52
|
+
values?: unknown[];
|
|
53
|
+
name?: string;
|
|
54
|
+
}): Promise<{
|
|
42
55
|
rows: Row[];
|
|
43
56
|
rowCount: number | null;
|
|
44
57
|
}>;
|
|
@@ -53,6 +66,12 @@ export interface PgPoolLike extends PgQueryable {
|
|
|
53
66
|
/** PostgreSQL adapter for `pg` (node-postgres). Pass a `Pool` to get transactions. */
|
|
54
67
|
export declare function postgresAdapter(pool: PgPoolLike, options?: {
|
|
55
68
|
tablePrefix?: string;
|
|
69
|
+
/**
|
|
70
|
+
* Prepare statements on each connection and reuse them (default true). Turn off behind a
|
|
71
|
+
* connection pooler that does not support prepared statements, such as PgBouncer before 1.21
|
|
72
|
+
* or in transaction mode without `max_prepared_statements`.
|
|
73
|
+
*/
|
|
74
|
+
preparedStatements?: boolean;
|
|
56
75
|
}): DatabaseAdapter;
|
|
57
76
|
interface MysqlQueryable {
|
|
58
77
|
query(sql: string, values?: unknown[]): Promise<[any, unknown]>;
|
package/dist/storage/sql.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
3
|
//#region src/storage/sql.ts
|
|
4
|
+
/** Parameters per statement for multi-row writes: within SQLite's (32766), Postgres' and MySQL's (65535) limits. */
|
|
5
|
+
const MAX_PARAMS = 4e3;
|
|
3
6
|
const OPS = {
|
|
4
7
|
eq: "=",
|
|
5
8
|
ne: "<>",
|
|
@@ -60,6 +63,12 @@ function sqlAdapter({ dialect, executor: rootExecutor, tablePrefix = "", transac
|
|
|
60
63
|
return out;
|
|
61
64
|
};
|
|
62
65
|
const placeholder = (i) => dialect === "postgres" ? `$${i}` : "?";
|
|
66
|
+
const limitClause = (limit, params) => {
|
|
67
|
+
const n = Math.max(0, Math.floor(limit));
|
|
68
|
+
if (dialect === "mysql") return ` LIMIT ${n}`;
|
|
69
|
+
params.push(n);
|
|
70
|
+
return ` LIMIT ${placeholder(params.length)}`;
|
|
71
|
+
};
|
|
63
72
|
const buildWhere = (model, where = [], params) => {
|
|
64
73
|
if (where.length === 0) return "";
|
|
65
74
|
return ` WHERE ${where.map((w) => {
|
|
@@ -67,6 +76,10 @@ function sqlAdapter({ dialect, executor: rootExecutor, tablePrefix = "", transac
|
|
|
67
76
|
if (op === "in") {
|
|
68
77
|
const values = w.value;
|
|
69
78
|
if (values.length === 0) return "1=0";
|
|
79
|
+
if (dialect === "postgres") {
|
|
80
|
+
params.push(values.map((v) => toDb(model, w.field, v)));
|
|
81
|
+
return `${q(w.field)} = ANY(${placeholder(params.length)})`;
|
|
82
|
+
}
|
|
70
83
|
const ph = values.map((v) => {
|
|
71
84
|
params.push(toDb(model, w.field, v));
|
|
72
85
|
return placeholder(params.length);
|
|
@@ -83,11 +96,15 @@ function sqlAdapter({ dialect, executor: rootExecutor, tablePrefix = "", transac
|
|
|
83
96
|
};
|
|
84
97
|
const build = (executor, inTx) => {
|
|
85
98
|
const insert = async (model, data, mode) => {
|
|
86
|
-
const
|
|
87
|
-
const
|
|
99
|
+
const rows = Array.isArray(data) ? data : [data];
|
|
100
|
+
const fields = Object.keys(rows[0]).filter((k) => rows[0][k] !== void 0);
|
|
101
|
+
const params = [];
|
|
102
|
+
const values = rows.map((row) => `(${fields.map((f) => {
|
|
103
|
+
params.push(toDb(model, f, row[f]));
|
|
104
|
+
return placeholder(params.length);
|
|
105
|
+
}).join(", ")})`);
|
|
88
106
|
const cols = fields.map(q).join(", ");
|
|
89
|
-
|
|
90
|
-
let sql = `INSERT INTO ${t(model)} (${cols}) VALUES (${ph})`;
|
|
107
|
+
let sql = `INSERT INTO ${t(model)} (${cols}) VALUES ${values.join(", ")}`;
|
|
91
108
|
if (mode === "upsert") {
|
|
92
109
|
const pk = schema[model]?.primaryKey ?? [];
|
|
93
110
|
const rest = fields.filter((f) => !pk.includes(f));
|
|
@@ -123,6 +140,20 @@ function sqlAdapter({ dialect, executor: rootExecutor, tablePrefix = "", transac
|
|
|
123
140
|
},
|
|
124
141
|
create: (model, data) => insert(model, data, "insert"),
|
|
125
142
|
upsert: (model, data) => insert(model, data, "upsert"),
|
|
143
|
+
async upsertMany(model, rows) {
|
|
144
|
+
const pk = schema[model]?.primaryKey ?? [];
|
|
145
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
146
|
+
for (const row of rows) byKey.set(JSON.stringify(pk.map((f) => row[f])), row);
|
|
147
|
+
const groups = /* @__PURE__ */ new Map();
|
|
148
|
+
for (const row of byKey.values()) {
|
|
149
|
+
const shape = Object.keys(row).filter((k) => row[k] !== void 0).join("\0");
|
|
150
|
+
groups.set(shape, [...groups.get(shape) ?? [], row]);
|
|
151
|
+
}
|
|
152
|
+
for (const group of groups.values()) {
|
|
153
|
+
const perStatement = Math.max(1, Math.floor(MAX_PARAMS / Object.keys(group[0]).length));
|
|
154
|
+
for (let i = 0; i < group.length; i += perStatement) await insert(model, group.slice(i, i + perStatement), "upsert");
|
|
155
|
+
}
|
|
156
|
+
},
|
|
126
157
|
async findOne(model, where) {
|
|
127
158
|
const params = [];
|
|
128
159
|
const rows = await executor.query(`SELECT * FROM ${t(model)}${buildWhere(model, where, params)} LIMIT 1`, params);
|
|
@@ -130,11 +161,31 @@ function sqlAdapter({ dialect, executor: rootExecutor, tablePrefix = "", transac
|
|
|
130
161
|
},
|
|
131
162
|
async findMany(model, opts = {}) {
|
|
132
163
|
const params = [];
|
|
133
|
-
let sql = `SELECT * FROM ${t(model)}${buildWhere(model, opts.where, params)}`;
|
|
164
|
+
let sql = `SELECT ${opts.fields?.length ? opts.fields.map(q).join(", ") : "*"} FROM ${t(model)}${buildWhere(model, opts.where, params)}`;
|
|
134
165
|
if (opts.sortBy) sql += ` ORDER BY ${q(opts.sortBy.field)} ${opts.sortBy.direction === "asc" ? "ASC" : "DESC"}`;
|
|
135
|
-
if (opts.limit !== void 0) sql +=
|
|
166
|
+
if (opts.limit !== void 0) sql += limitClause(opts.limit, params);
|
|
136
167
|
return (await executor.query(sql, params)).map((r) => fromDb(model, r));
|
|
137
168
|
},
|
|
169
|
+
...dialect === "postgres" ? { async findManyBatch(queries, options) {
|
|
170
|
+
if (queries.length === 0) return [];
|
|
171
|
+
const params = [];
|
|
172
|
+
const parts = queries.map(({ model, opts = {} }, i) => {
|
|
173
|
+
const fields = opts.fields?.length && opts.sortBy && !opts.fields.includes(opts.sortBy.field) ? [...opts.fields, opts.sortBy.field] : opts.fields;
|
|
174
|
+
let inner = `SELECT ${fields?.length ? fields.map(q).join(", ") : "*"} FROM ${t(model)}${buildWhere(model, opts.where, params)}`;
|
|
175
|
+
const direction = opts.sortBy?.direction === "asc" ? "ASC" : "DESC";
|
|
176
|
+
if (opts.sortBy) inner += ` ORDER BY ${q(opts.sortBy.field)} ${direction}`;
|
|
177
|
+
if (opts.limit !== void 0) inner += limitClause(opts.limit, params);
|
|
178
|
+
return `SELECT ${i} AS i, (SELECT json_agg(b${opts.sortBy ? ` ORDER BY b.${q(opts.sortBy.field)} ${direction}` : ""}) FROM (${inner}) b) AS r`;
|
|
179
|
+
});
|
|
180
|
+
const rows = await executor.query(parts.join(" UNION ALL "), params, options);
|
|
181
|
+
const out = queries.map(() => []);
|
|
182
|
+
for (const row of rows) {
|
|
183
|
+
const list = typeof row.r === "string" ? JSON.parse(row.r) : row.r;
|
|
184
|
+
const model = queries[Number(row.i)].model;
|
|
185
|
+
out[Number(row.i)] = (list ?? []).map((r) => fromDb(model, r));
|
|
186
|
+
}
|
|
187
|
+
return out;
|
|
188
|
+
} } : {},
|
|
138
189
|
async update(model, where, data) {
|
|
139
190
|
const fields = Object.keys(data).filter((k) => data[k] !== void 0);
|
|
140
191
|
if (fields.length === 0) return 0;
|
|
@@ -156,21 +207,36 @@ function sqlAdapter({ dialect, executor: rootExecutor, tablePrefix = "", transac
|
|
|
156
207
|
};
|
|
157
208
|
return build(rootExecutor, false);
|
|
158
209
|
}
|
|
210
|
+
/** How long SQLite statements may run back to back before yielding to the event loop (#47). */
|
|
211
|
+
const YIELD_AFTER_MS = 10;
|
|
212
|
+
/** Prepared statements an SQLite adapter keeps (#40). */
|
|
213
|
+
const STATEMENT_CACHE = 500;
|
|
159
214
|
/** SQLite adapter for `node:sqlite` (`new DatabaseSync(path)`) or `better-sqlite3`. */
|
|
160
215
|
function sqliteAdapter(db, options = {}) {
|
|
161
216
|
try {
|
|
162
217
|
db.exec("PRAGMA journal_mode = WAL");
|
|
218
|
+
db.exec(`PRAGMA synchronous = ${options.synchronous === "full" ? "FULL" : "NORMAL"}`);
|
|
163
219
|
} catch {}
|
|
164
220
|
const cache = /* @__PURE__ */ new Map();
|
|
165
221
|
const stmt = (sql) => {
|
|
166
222
|
let s = cache.get(sql);
|
|
167
|
-
if (
|
|
223
|
+
if (s) cache.delete(sql);
|
|
224
|
+
else s = db.prepare(sql);
|
|
225
|
+
cache.set(sql, s);
|
|
226
|
+
if (cache.size > STATEMENT_CACHE) cache.delete(cache.keys().next().value);
|
|
168
227
|
return s;
|
|
169
228
|
};
|
|
170
229
|
const inTransaction = new AsyncLocalStorage();
|
|
171
230
|
let gate = Promise.resolve();
|
|
231
|
+
let lastYield = performance.now();
|
|
172
232
|
const waitGate = async () => {
|
|
173
|
-
if (
|
|
233
|
+
if (inTransaction.getStore()) return;
|
|
234
|
+
await gate;
|
|
235
|
+
if (performance.now() - lastYield > YIELD_AFTER_MS) {
|
|
236
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
237
|
+
lastYield = performance.now();
|
|
238
|
+
await gate;
|
|
239
|
+
}
|
|
174
240
|
};
|
|
175
241
|
const executor = {
|
|
176
242
|
async query(sql, params) {
|
|
@@ -210,29 +276,80 @@ function sqliteAdapter(db, options = {}) {
|
|
|
210
276
|
}
|
|
211
277
|
});
|
|
212
278
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
279
|
+
/**
|
|
280
|
+
* https://github.com/k18k/matrix-nodejs-homeserver/issues/40: unnamed statements are parsed and
|
|
281
|
+
* planned by Postgres on every call, which took most of its CPU under load. Named ones are prepared
|
|
282
|
+
* once per connection and reused. Statement texts depend only on the code (lists and limits are
|
|
283
|
+
* parameters), so there are a few hundred of them; past `MAX_PREPARED` distinct texts, further
|
|
284
|
+
* ones run unnamed, which bounds the memory both node-postgres and Postgres keep per connection.
|
|
285
|
+
*/
|
|
286
|
+
const MAX_PREPARED = 2e3;
|
|
287
|
+
function statementNamer(enabled) {
|
|
288
|
+
let names = /* @__PURE__ */ new Map();
|
|
289
|
+
let generation = 0;
|
|
290
|
+
return {
|
|
291
|
+
name(sql) {
|
|
292
|
+
if (!enabled) return void 0;
|
|
293
|
+
let name = names.get(sql);
|
|
294
|
+
if (!name && names.size < MAX_PREPARED) names.set(sql, name = `m${generation}_${createHash("sha1").update(sql).digest("base64url")}`);
|
|
295
|
+
return name;
|
|
296
|
+
},
|
|
297
|
+
reset() {
|
|
298
|
+
names = /* @__PURE__ */ new Map();
|
|
299
|
+
generation++;
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* A prepared `SELECT *` fails once its table has gained a column, for example when another node of
|
|
305
|
+
* a cluster migrated the schema during a rolling update. Then the statements are prepared again
|
|
306
|
+
* under new names and the query is retried once.
|
|
307
|
+
*/
|
|
308
|
+
const isStalePlan = (err) => err?.code === "0A000" && /cached plan/.test(String(err.message));
|
|
309
|
+
const pgExecutor = (conn, namer) => {
|
|
310
|
+
const run = async (sql, params, prepare = true) => {
|
|
311
|
+
const name = () => prepare ? namer.name(sql) : void 0;
|
|
312
|
+
try {
|
|
313
|
+
return await conn.query({
|
|
314
|
+
text: sql,
|
|
315
|
+
values: params,
|
|
316
|
+
name: name()
|
|
317
|
+
});
|
|
318
|
+
} catch (err) {
|
|
319
|
+
if (!isStalePlan(err)) throw err;
|
|
320
|
+
namer.reset();
|
|
321
|
+
return conn.query({
|
|
322
|
+
text: sql,
|
|
323
|
+
values: params,
|
|
324
|
+
name: name()
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
return {
|
|
329
|
+
async query(sql, params, options) {
|
|
330
|
+
return (await run(sql, params, options?.prepare)).rows;
|
|
331
|
+
},
|
|
332
|
+
async execute(sql, params) {
|
|
333
|
+
return (await run(sql, params)).rowCount ?? 0;
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
};
|
|
221
337
|
/** PostgreSQL adapter for `pg` (node-postgres). Pass a `Pool` to get transactions. */
|
|
222
338
|
function postgresAdapter(pool, options = {}) {
|
|
339
|
+
const namer = statementNamer(options.preparedStatements ?? true);
|
|
223
340
|
return sqlAdapter({
|
|
224
341
|
dialect: "postgres",
|
|
225
342
|
tablePrefix: options.tablePrefix,
|
|
226
|
-
executor: pgExecutor(pool),
|
|
343
|
+
executor: pgExecutor(pool, namer),
|
|
227
344
|
transaction: pool.connect ? async (fn) => {
|
|
228
345
|
const client = await pool.connect();
|
|
229
346
|
try {
|
|
230
|
-
await client.query("BEGIN");
|
|
231
|
-
const result = await fn(pgExecutor(client));
|
|
232
|
-
await client.query("COMMIT");
|
|
347
|
+
await client.query({ text: "BEGIN" });
|
|
348
|
+
const result = await fn(pgExecutor(client, namer));
|
|
349
|
+
await client.query({ text: "COMMIT" });
|
|
233
350
|
return result;
|
|
234
351
|
} catch (err) {
|
|
235
|
-
await client.query("ROLLBACK").catch(() => {});
|
|
352
|
+
await client.query({ text: "ROLLBACK" }).catch(() => {});
|
|
236
353
|
throw err;
|
|
237
354
|
} finally {
|
|
238
355
|
client.release();
|
package/dist/storage/types.d.ts
CHANGED
|
@@ -21,8 +21,26 @@ export interface WhereClause {
|
|
|
21
21
|
operator?: Operator;
|
|
22
22
|
}
|
|
23
23
|
export type Row = Record<string, any>;
|
|
24
|
+
/** How a `findManyBatch` is run. */
|
|
25
|
+
export interface BatchOptions {
|
|
26
|
+
/**
|
|
27
|
+
* The combination of reads is a one-off (as when unrelated reads are coalesced), so preparing
|
|
28
|
+
* the statement for reuse would only fill the database's statement cache. Default true.
|
|
29
|
+
*/
|
|
30
|
+
prepare?: boolean;
|
|
31
|
+
}
|
|
32
|
+
/** One read of a `findManyBatch`. */
|
|
33
|
+
export interface BatchQuery {
|
|
34
|
+
model: string;
|
|
35
|
+
opts?: FindManyOptions;
|
|
36
|
+
}
|
|
24
37
|
export interface FindManyOptions {
|
|
25
38
|
where?: WhereClause[];
|
|
39
|
+
/**
|
|
40
|
+
* Only these fields are needed. Adapters should read just these (a smaller query and less to
|
|
41
|
+
* parse); one that ignores the option and returns whole rows is still correct.
|
|
42
|
+
*/
|
|
43
|
+
fields?: string[];
|
|
26
44
|
sortBy?: {
|
|
27
45
|
field: string;
|
|
28
46
|
direction: "asc" | "desc";
|
|
@@ -35,11 +53,23 @@ export interface FindManyOptions {
|
|
|
35
53
|
* All `where` clauses are combined with AND.
|
|
36
54
|
*/
|
|
37
55
|
export interface DatabaseAdapter {
|
|
56
|
+
/**
|
|
57
|
+
* Several independent reads in one go: the results in the same order as the queries. Optional;
|
|
58
|
+
* an adapter that talks to a database server can send them in one round trip. Without it,
|
|
59
|
+
* callers run the reads separately (see `readBatch`).
|
|
60
|
+
*/
|
|
61
|
+
findManyBatch?(queries: BatchQuery[], options?: BatchOptions): Promise<Row[][]>;
|
|
38
62
|
/** Create tables / collections for the given schema. Must be idempotent. */
|
|
39
63
|
migrate?(schema: Schema): Promise<void>;
|
|
40
64
|
create(model: string, data: Row): Promise<void>;
|
|
41
65
|
/** Insert or replace by primary key. */
|
|
42
66
|
upsert(model: string, data: Row): Promise<void>;
|
|
67
|
+
/**
|
|
68
|
+
* `upsert` for several rows at once (the last row wins when two share a primary key). Optional;
|
|
69
|
+
* an adapter that talks to a database server can write them in a few statements. Without it,
|
|
70
|
+
* callers upsert the rows one by one (see `upsertMany`).
|
|
71
|
+
*/
|
|
72
|
+
upsertMany?(model: string, rows: Row[]): Promise<void>;
|
|
43
73
|
findOne(model: string, where: WhereClause[]): Promise<Row | null>;
|
|
44
74
|
findMany(model: string, options?: FindManyOptions): Promise<Row[]>;
|
|
45
75
|
update(model: string, where: WhereClause[], data: Row): Promise<number>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "matrix-nodejs-homeserver",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
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
|
}
|