@koishijs/plugin-database-mongo 3.1.1 → 3.2.1
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/lib/index.d.ts +6 -81
- package/lib/index.js +25 -403
- package/lib/index.js.map +3 -3
- package/package.json +3 -8
package/lib/index.d.ts
CHANGED
|
@@ -1,81 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
declare module 'koishi' {
|
|
8
|
-
interface Database {
|
|
9
|
-
mongo: MongoDatabase;
|
|
10
|
-
}
|
|
11
|
-
}
|
|
12
|
-
declare class MongoDatabase extends Database {
|
|
13
|
-
ctx: Context;
|
|
14
|
-
private config;
|
|
15
|
-
client: MongoClient;
|
|
16
|
-
db: Db;
|
|
17
|
-
mongo: this;
|
|
18
|
-
private _tableTasks;
|
|
19
|
-
private _evalTasks;
|
|
20
|
-
constructor(ctx: Context, config: MongoDatabase.Config);
|
|
21
|
-
private connectionStringFromConfig;
|
|
22
|
-
start(): Promise<void>;
|
|
23
|
-
stop(): Promise<void>;
|
|
24
|
-
private _createIndexes;
|
|
25
|
-
private _createFields;
|
|
26
|
-
/** synchronize table schema */
|
|
27
|
-
private _syncTable;
|
|
28
|
-
private _createFilter;
|
|
29
|
-
drop(): Promise<void>;
|
|
30
|
-
private _collStats;
|
|
31
|
-
stats(): Promise<{
|
|
32
|
-
size: any;
|
|
33
|
-
tables: {
|
|
34
|
-
[k: string]: {
|
|
35
|
-
readonly count: number;
|
|
36
|
-
readonly size: number;
|
|
37
|
-
};
|
|
38
|
-
};
|
|
39
|
-
}>;
|
|
40
|
-
get(name: keyof Tables, query: Query, modifier: Modifier): Promise<any>;
|
|
41
|
-
set(name: keyof Tables, query: Query, update: {}): Promise<void>;
|
|
42
|
-
remove(name: keyof Tables, query: Query): Promise<void>;
|
|
43
|
-
private queue;
|
|
44
|
-
create<T extends keyof Tables>(name: T, data: any): Promise<any>;
|
|
45
|
-
upsert(name: keyof Tables, data: any[], keys: string | string[]): Promise<void>;
|
|
46
|
-
eval(table: keyof Tables, expr: any, query: Query): Promise<any>;
|
|
47
|
-
private _flushEvalTasks;
|
|
48
|
-
}
|
|
49
|
-
declare namespace MongoDatabase {
|
|
50
|
-
const name = "database-mongo";
|
|
51
|
-
interface Config {
|
|
52
|
-
username?: string;
|
|
53
|
-
password?: string;
|
|
54
|
-
protocol?: string;
|
|
55
|
-
host?: string;
|
|
56
|
-
port?: number;
|
|
57
|
-
/** database name */
|
|
58
|
-
database?: string;
|
|
59
|
-
/** default auth database */
|
|
60
|
-
authDatabase?: string;
|
|
61
|
-
connectOptions?: ConstructorParameters<typeof URLSearchParams>[0];
|
|
62
|
-
/** connection string (will overwrite all configs except 'name') */
|
|
63
|
-
uri?: string;
|
|
64
|
-
}
|
|
65
|
-
const Config: Schema<{
|
|
66
|
-
protocol?: string;
|
|
67
|
-
host?: string;
|
|
68
|
-
port?: number;
|
|
69
|
-
username?: string;
|
|
70
|
-
password?: string;
|
|
71
|
-
database?: string;
|
|
72
|
-
} & import("schemastery").Dict<any, string>, {
|
|
73
|
-
protocol?: string;
|
|
74
|
-
host?: string;
|
|
75
|
-
port?: number;
|
|
76
|
-
username?: string;
|
|
77
|
-
password?: string;
|
|
78
|
-
database?: string;
|
|
79
|
-
} & import("schemastery").Dict<any, string>>;
|
|
80
|
-
}
|
|
81
|
-
export default MongoDatabase;
|
|
1
|
+
import { Context, Schema } from 'koishi';
|
|
2
|
+
import MongoDriver from '@cosmotype/driver-mongo';
|
|
3
|
+
export declare const name = "MemoryDatabase";
|
|
4
|
+
export declare type Config = MongoDriver.Config;
|
|
5
|
+
export declare const Config: Schema<Config>;
|
|
6
|
+
export declare function apply(ctx: Context, config: Config): void;
|
package/lib/index.js
CHANGED
|
@@ -8,8 +8,8 @@ var __markAsModule = (target) => __defProp(target, "__esModule", { value: true }
|
|
|
8
8
|
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
9
9
|
var __export = (target, all) => {
|
|
10
10
|
__markAsModule(target);
|
|
11
|
-
for (var
|
|
12
|
-
__defProp(target,
|
|
11
|
+
for (var name2 in all)
|
|
12
|
+
__defProp(target, name2, { get: all[name2], enumerable: true });
|
|
13
13
|
};
|
|
14
14
|
var __reExport = (target, module2, desc) => {
|
|
15
15
|
if (module2 && typeof module2 === "object" || typeof module2 === "function") {
|
|
@@ -25,409 +25,31 @@ var __toModule = (module2) => {
|
|
|
25
25
|
|
|
26
26
|
// plugins/database/mongo/src/index.ts
|
|
27
27
|
__export(exports, {
|
|
28
|
-
|
|
28
|
+
Config: () => Config,
|
|
29
|
+
apply: () => apply,
|
|
30
|
+
name: () => name
|
|
29
31
|
});
|
|
30
|
-
var import_mongodb = __toModule(require("mongodb"));
|
|
31
|
-
var import_koishi2 = __toModule(require("koishi"));
|
|
32
|
-
var import_orm = __toModule(require("@koishijs/orm"));
|
|
33
|
-
var import_url = __toModule(require("url"));
|
|
34
|
-
|
|
35
|
-
// plugins/database/mongo/src/utils.ts
|
|
36
32
|
var import_koishi = __toModule(require("koishi"));
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
__name(createFieldFilter, "createFieldFilter");
|
|
52
|
-
function transformFieldQuery(query, key, filters) {
|
|
53
|
-
if (typeof query === "string" || typeof query === "number" || query instanceof Date) {
|
|
54
|
-
return { $eq: query };
|
|
55
|
-
} else if (Array.isArray(query)) {
|
|
56
|
-
if (!query.length)
|
|
57
|
-
return false;
|
|
58
|
-
return { $in: query };
|
|
59
|
-
} else if (query instanceof RegExp) {
|
|
60
|
-
return { $regex: query };
|
|
61
|
-
}
|
|
62
|
-
const result = {};
|
|
63
|
-
for (const prop in query) {
|
|
64
|
-
if (prop === "$and") {
|
|
65
|
-
for (const item of query[prop]) {
|
|
66
|
-
const child = createFieldFilter(item, key);
|
|
67
|
-
if (child === false)
|
|
68
|
-
return false;
|
|
69
|
-
if (child !== true)
|
|
70
|
-
filters.push(child);
|
|
71
|
-
}
|
|
72
|
-
} else if (prop === "$or") {
|
|
73
|
-
const $or = [];
|
|
74
|
-
if (!query[prop].length)
|
|
75
|
-
return false;
|
|
76
|
-
const always = query[prop].some((item) => {
|
|
77
|
-
const child = createFieldFilter(item, key);
|
|
78
|
-
if (typeof child === "boolean")
|
|
79
|
-
return child;
|
|
80
|
-
$or.push(child);
|
|
81
|
-
});
|
|
82
|
-
if (!always)
|
|
83
|
-
filters.push({ $or });
|
|
84
|
-
} else if (prop === "$not") {
|
|
85
|
-
const child = createFieldFilter(query[prop], key);
|
|
86
|
-
if (child === true)
|
|
87
|
-
return false;
|
|
88
|
-
if (child !== false)
|
|
89
|
-
filters.push({ $nor: [child] });
|
|
90
|
-
} else if (prop === "$el") {
|
|
91
|
-
const child = transformFieldQuery(query[prop], key, filters);
|
|
92
|
-
if (child === false)
|
|
93
|
-
return false;
|
|
94
|
-
if (child !== true)
|
|
95
|
-
result.$elemMatch = child;
|
|
96
|
-
} else if (prop === "$regexFor") {
|
|
97
|
-
filters.push({
|
|
98
|
-
$expr: {
|
|
99
|
-
$function: {
|
|
100
|
-
body: function(data, value) {
|
|
101
|
-
return new RegExp(data, "i").test(value);
|
|
102
|
-
}.toString(),
|
|
103
|
-
args: ["$" + key, query.$regexFor],
|
|
104
|
-
lang: "js"
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
});
|
|
108
|
-
} else {
|
|
109
|
-
result[prop] = query[prop];
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
if (!Object.keys(result).length)
|
|
113
|
-
return true;
|
|
114
|
-
return result;
|
|
115
|
-
}
|
|
116
|
-
__name(transformFieldQuery, "transformFieldQuery");
|
|
117
|
-
function transformQuery(query) {
|
|
118
|
-
const filter = {};
|
|
119
|
-
const additional = [];
|
|
120
|
-
for (const key in query) {
|
|
121
|
-
const value = query[key];
|
|
122
|
-
if (key === "$and" || key === "$or") {
|
|
123
|
-
if (value.length) {
|
|
124
|
-
filter[key] = value.map(transformQuery);
|
|
125
|
-
} else if (key === "$or") {
|
|
126
|
-
return;
|
|
127
|
-
}
|
|
128
|
-
} else if (key === "$not") {
|
|
129
|
-
const query2 = transformQuery(value);
|
|
130
|
-
if (query2)
|
|
131
|
-
filter.$nor = [query2];
|
|
132
|
-
} else if (key === "$expr") {
|
|
133
|
-
additional.push({ $expr: transformEval(value) });
|
|
134
|
-
} else {
|
|
135
|
-
const query2 = transformFieldQuery(value, key, additional);
|
|
136
|
-
if (query2 === false)
|
|
137
|
-
return;
|
|
138
|
-
if (query2 !== true)
|
|
139
|
-
filter[key] = query2;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
if (additional.length) {
|
|
143
|
-
(filter.$and || (filter.$and = [])).push(...additional);
|
|
144
|
-
}
|
|
145
|
-
return filter;
|
|
146
|
-
}
|
|
147
|
-
__name(transformQuery, "transformQuery");
|
|
148
|
-
function transformEvalExpr(expr, onAggr) {
|
|
149
|
-
return (0, import_koishi.valueMap)(expr, (value) => {
|
|
150
|
-
if (Array.isArray(value)) {
|
|
151
|
-
return value.map((val) => transformEval(val, onAggr));
|
|
152
|
-
} else {
|
|
153
|
-
return transformEval(value, onAggr);
|
|
154
|
-
}
|
|
155
|
-
});
|
|
156
|
-
}
|
|
157
|
-
__name(transformEvalExpr, "transformEvalExpr");
|
|
158
|
-
function transformAggr(expr) {
|
|
159
|
-
if (typeof expr === "string") {
|
|
160
|
-
return "$" + expr;
|
|
161
|
-
}
|
|
162
|
-
return transformEvalExpr(expr);
|
|
163
|
-
}
|
|
164
|
-
__name(transformAggr, "transformAggr");
|
|
165
|
-
var aggrKeys = ["$sum", "$avg", "$min", "$max", "$count"];
|
|
166
|
-
function transformEval(expr, onAggr) {
|
|
167
|
-
if (typeof expr === "number" || typeof expr === "string" || typeof expr === "boolean") {
|
|
168
|
-
return expr;
|
|
169
|
-
} else if (expr.$) {
|
|
170
|
-
return "$" + expr.$;
|
|
171
|
-
}
|
|
172
|
-
for (const key of aggrKeys) {
|
|
173
|
-
if (!expr[key])
|
|
174
|
-
continue;
|
|
175
|
-
const value = transformAggr(expr[key]);
|
|
176
|
-
const $ = import_koishi.Random.id();
|
|
177
|
-
if (key === "$count") {
|
|
178
|
-
onAggr([
|
|
179
|
-
{ $group: { _id: value } },
|
|
180
|
-
{ $group: { _id: null, [$]: { $count: {} } } }
|
|
181
|
-
]);
|
|
182
|
-
} else {
|
|
183
|
-
onAggr([{ $group: { _id: null, [$]: { [key]: value } } }]);
|
|
184
|
-
}
|
|
185
|
-
return { $ };
|
|
186
|
-
}
|
|
187
|
-
return transformEvalExpr(expr, onAggr);
|
|
33
|
+
var import_driver_mongo = __toModule(require("@cosmotype/driver-mongo"));
|
|
34
|
+
var name = "MemoryDatabase";
|
|
35
|
+
var Config = import_koishi.Schema.object({
|
|
36
|
+
protocol: import_koishi.Schema.string().description("要使用的协议名。").default("mongodb"),
|
|
37
|
+
host: import_koishi.Schema.string().description("要连接到的主机名。").default("localhost"),
|
|
38
|
+
port: import_koishi.Schema.natural().max(65535).description("要连接到的端口号。"),
|
|
39
|
+
username: import_koishi.Schema.string().description("要使用的用户名。"),
|
|
40
|
+
password: import_koishi.Schema.string().description("要使用的密码。").role("secret"),
|
|
41
|
+
database: import_koishi.Schema.string().description("要访问的数据库名。").default("koishi")
|
|
42
|
+
});
|
|
43
|
+
function apply(ctx, config) {
|
|
44
|
+
const driver = new import_driver_mongo.default(ctx.model, config);
|
|
45
|
+
ctx.on("ready", () => driver.start());
|
|
46
|
+
ctx.on("dispose", () => driver.stop());
|
|
188
47
|
}
|
|
189
|
-
__name(
|
|
190
|
-
|
|
191
|
-
// plugins/database/mongo/src/index.ts
|
|
192
|
-
var MongoDatabase = class extends import_koishi2.Database {
|
|
193
|
-
constructor(ctx, config) {
|
|
194
|
-
super(ctx);
|
|
195
|
-
this.ctx = ctx;
|
|
196
|
-
this.config = config;
|
|
197
|
-
this.mongo = this;
|
|
198
|
-
this._tableTasks = {};
|
|
199
|
-
this._evalTasks = [];
|
|
200
|
-
}
|
|
201
|
-
connectionStringFromConfig() {
|
|
202
|
-
const { authDatabase, connectOptions, host, database: name, password, port, protocol, username } = this.config;
|
|
203
|
-
let mongourl = `${protocol}://`;
|
|
204
|
-
if (username)
|
|
205
|
-
mongourl += `${encodeURIComponent(username)}${password ? `:${encodeURIComponent(password)}` : ""}@`;
|
|
206
|
-
mongourl += `${host}${port ? `:${port}` : ""}/${authDatabase || name}`;
|
|
207
|
-
if (connectOptions) {
|
|
208
|
-
const params = new import_url.URLSearchParams(connectOptions);
|
|
209
|
-
mongourl += `?${params}`;
|
|
210
|
-
}
|
|
211
|
-
return mongourl;
|
|
212
|
-
}
|
|
213
|
-
async start() {
|
|
214
|
-
const mongourl = this.config.uri || this.connectionStringFromConfig();
|
|
215
|
-
this.client = await import_mongodb.MongoClient.connect(mongourl);
|
|
216
|
-
this.db = this.client.db(this.config.database);
|
|
217
|
-
for (const name in this.model.config) {
|
|
218
|
-
this._tableTasks[name] = this._syncTable(name);
|
|
219
|
-
}
|
|
220
|
-
this.ctx.on("model", (name) => {
|
|
221
|
-
this._tableTasks[name] = this._syncTable(name);
|
|
222
|
-
});
|
|
223
|
-
}
|
|
224
|
-
stop() {
|
|
225
|
-
return this.client.close();
|
|
226
|
-
}
|
|
227
|
-
async _createIndexes(name) {
|
|
228
|
-
const { primary, unique } = this.model.config[name];
|
|
229
|
-
const coll = this.db.collection(name);
|
|
230
|
-
const newSpecs = [];
|
|
231
|
-
const oldSpecs = await coll.indexes();
|
|
232
|
-
[primary, ...unique].forEach((keys, index) => {
|
|
233
|
-
keys = (0, import_koishi2.makeArray)(keys);
|
|
234
|
-
const name2 = (index ? "unique:" : "primary:") + keys.join("+");
|
|
235
|
-
if (oldSpecs.find((spec) => spec.name === name2))
|
|
236
|
-
return;
|
|
237
|
-
newSpecs.push({
|
|
238
|
-
name: name2,
|
|
239
|
-
key: Object.fromEntries(keys.map((key) => [key, 1])),
|
|
240
|
-
unique: true,
|
|
241
|
-
partialFilterExpression: Object.fromEntries(keys.map((key) => [key, { $exists: true }]))
|
|
242
|
-
});
|
|
243
|
-
});
|
|
244
|
-
if (!newSpecs.length)
|
|
245
|
-
return;
|
|
246
|
-
await coll.createIndexes(newSpecs);
|
|
247
|
-
}
|
|
248
|
-
async _createFields(name) {
|
|
249
|
-
const { fields } = this.model.config[name];
|
|
250
|
-
const coll = this.db.collection(name);
|
|
251
|
-
await Promise.all(Object.keys(fields).map((key) => {
|
|
252
|
-
if ((0, import_koishi2.isNullable)(fields[key].initial))
|
|
253
|
-
return;
|
|
254
|
-
return coll.updateMany({ [key]: { $exists: false } }, { $set: { [key]: fields[key].initial } });
|
|
255
|
-
}));
|
|
256
|
-
}
|
|
257
|
-
async _syncTable(name) {
|
|
258
|
-
await Promise.resolve(this._tableTasks[name]).catch(import_koishi2.noop);
|
|
259
|
-
await this.db.createCollection(name).catch(import_koishi2.noop);
|
|
260
|
-
await Promise.all([
|
|
261
|
-
this._createIndexes(name),
|
|
262
|
-
this._createFields(name)
|
|
263
|
-
]);
|
|
264
|
-
}
|
|
265
|
-
_createFilter(name, query) {
|
|
266
|
-
return transformQuery(this.resolveQuery(name, query));
|
|
267
|
-
}
|
|
268
|
-
async drop() {
|
|
269
|
-
await Promise.all(Object.keys(this.model.config).map((name) => this.db.dropCollection(name)));
|
|
270
|
-
}
|
|
271
|
-
async _collStats() {
|
|
272
|
-
const tables = Object.keys(this.model.config);
|
|
273
|
-
const entries = await Promise.all(tables.map(async (name) => {
|
|
274
|
-
const coll = this.db.collection(name);
|
|
275
|
-
const { count, size } = await coll.stats();
|
|
276
|
-
return [coll.collectionName, { count, size }];
|
|
277
|
-
}));
|
|
278
|
-
return Object.fromEntries(entries);
|
|
279
|
-
}
|
|
280
|
-
async stats() {
|
|
281
|
-
const [stats, tables] = await Promise.all([
|
|
282
|
-
this.db.stats(),
|
|
283
|
-
this._collStats()
|
|
284
|
-
]);
|
|
285
|
-
const totalSize = stats.indexSize + stats.storageSize;
|
|
286
|
-
return { size: totalSize, tables };
|
|
287
|
-
}
|
|
288
|
-
async get(name, query, modifier) {
|
|
289
|
-
const filter = this._createFilter(name, query);
|
|
290
|
-
if (!filter)
|
|
291
|
-
return [];
|
|
292
|
-
await Promise.resolve(this._tableTasks[name]).catch(import_koishi2.noop);
|
|
293
|
-
let cursor = this.db.collection(name).find(filter);
|
|
294
|
-
const { fields, limit, offset = 0, sort } = this.resolveModifier(name, modifier);
|
|
295
|
-
if (offset)
|
|
296
|
-
cursor = cursor.skip(offset);
|
|
297
|
-
if (limit)
|
|
298
|
-
cursor = cursor.limit(offset + limit);
|
|
299
|
-
if (sort)
|
|
300
|
-
cursor = cursor.sort(sort);
|
|
301
|
-
return (await cursor.toArray()).map((row) => this.resolveData(name, row, fields));
|
|
302
|
-
}
|
|
303
|
-
async set(name, query, update) {
|
|
304
|
-
const filter = this._createFilter(name, query);
|
|
305
|
-
if (!filter)
|
|
306
|
-
return;
|
|
307
|
-
await Promise.resolve(this._tableTasks[name]).catch(import_koishi2.noop);
|
|
308
|
-
const { primary } = this.model.config[name];
|
|
309
|
-
const indexFields = (0, import_koishi2.makeArray)(primary);
|
|
310
|
-
const coll = this.db.collection(name);
|
|
311
|
-
const original = await coll.find(filter).toArray();
|
|
312
|
-
if (!original.length)
|
|
313
|
-
return;
|
|
314
|
-
update = this.model.format(name, update);
|
|
315
|
-
const updateFields = new Set(Object.keys(update).map((key) => key.split(".", 1)[0]));
|
|
316
|
-
const bulk = coll.initializeUnorderedBulkOp();
|
|
317
|
-
for (const item of original) {
|
|
318
|
-
bulk.find((0, import_koishi2.pick)(item, indexFields)).updateOne({
|
|
319
|
-
$set: (0, import_koishi2.pick)((0, import_orm.executeUpdate)(item, update), updateFields)
|
|
320
|
-
});
|
|
321
|
-
}
|
|
322
|
-
await bulk.execute();
|
|
323
|
-
}
|
|
324
|
-
async remove(name, query) {
|
|
325
|
-
const filter = this._createFilter(name, query);
|
|
326
|
-
if (!filter)
|
|
327
|
-
return;
|
|
328
|
-
await this.db.collection(name).deleteMany(filter);
|
|
329
|
-
}
|
|
330
|
-
queue(name, callback) {
|
|
331
|
-
return this._tableTasks[name] = Promise.resolve(this._tableTasks[name]).catch(import_koishi2.noop).then(callback);
|
|
332
|
-
}
|
|
333
|
-
async create(name, data) {
|
|
334
|
-
const coll = this.db.collection(name);
|
|
335
|
-
return this.queue(name, async () => {
|
|
336
|
-
const { primary, fields, autoInc } = this.model.config[name];
|
|
337
|
-
if (autoInc && !Array.isArray(primary) && !(primary in data)) {
|
|
338
|
-
const [latest] = await coll.find().sort(primary, -1).limit(1).toArray();
|
|
339
|
-
data[primary] = latest ? +latest[primary] + 1 : 1;
|
|
340
|
-
if (import_koishi2.Model.Field.string.includes(fields[primary].type)) {
|
|
341
|
-
data[primary] += "";
|
|
342
|
-
data[primary] = data[primary].padStart(8, "0");
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
const copy = this.model.create(name, data);
|
|
346
|
-
try {
|
|
347
|
-
await coll.insertOne(copy);
|
|
348
|
-
delete copy["_id"];
|
|
349
|
-
return copy;
|
|
350
|
-
} catch (err) {
|
|
351
|
-
if (err instanceof import_mongodb.MongoError && err.code === 11e3) {
|
|
352
|
-
throw new import_koishi2.DriverError("duplicate-entry", err.message);
|
|
353
|
-
}
|
|
354
|
-
throw err;
|
|
355
|
-
}
|
|
356
|
-
});
|
|
357
|
-
}
|
|
358
|
-
async upsert(name, data, keys) {
|
|
359
|
-
if (!data.length)
|
|
360
|
-
return;
|
|
361
|
-
if (!keys)
|
|
362
|
-
keys = this.model.config[name].primary;
|
|
363
|
-
const indexFields = (0, import_koishi2.makeArray)(keys);
|
|
364
|
-
await Promise.resolve(this._tableTasks[name]).catch(import_koishi2.noop);
|
|
365
|
-
const coll = this.db.collection(name);
|
|
366
|
-
const original = await coll.find({ $or: data.map((item) => (0, import_koishi2.pick)(item, indexFields)) }).toArray();
|
|
367
|
-
const bulk = coll.initializeUnorderedBulkOp();
|
|
368
|
-
for (const _item of data) {
|
|
369
|
-
const update = this.model.format(name, _item);
|
|
370
|
-
const item = original.find((item2) => indexFields.every((key) => item2[key].valueOf() === update[key].valueOf()));
|
|
371
|
-
if (item) {
|
|
372
|
-
const updateFields = new Set(Object.keys(update).map((key) => key.split(".", 1)[0]));
|
|
373
|
-
const override = (0, import_koishi2.omit)((0, import_koishi2.pick)((0, import_orm.executeUpdate)(item, update), updateFields), indexFields);
|
|
374
|
-
bulk.find((0, import_koishi2.pick)(item, indexFields)).updateOne({ $set: override });
|
|
375
|
-
} else {
|
|
376
|
-
bulk.insert((0, import_orm.executeUpdate)(this.model.create(name), update));
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
await bulk.execute();
|
|
380
|
-
}
|
|
381
|
-
eval(table, expr, query) {
|
|
382
|
-
return new Promise((resolve, reject) => {
|
|
383
|
-
this._evalTasks.push({ expr, table, query, resolve, reject });
|
|
384
|
-
process.nextTick(() => this._flushEvalTasks());
|
|
385
|
-
});
|
|
386
|
-
}
|
|
387
|
-
async _flushEvalTasks() {
|
|
388
|
-
const tasks = this._evalTasks;
|
|
389
|
-
if (!tasks.length)
|
|
390
|
-
return;
|
|
391
|
-
this._evalTasks = [];
|
|
392
|
-
const stages = [{ $match: { _id: null } }];
|
|
393
|
-
for (const task of tasks) {
|
|
394
|
-
const { expr, table, query } = task;
|
|
395
|
-
task.expr = transformEval(expr, (pipeline) => {
|
|
396
|
-
const filter = this._createFilter(table, query) || { _id: null };
|
|
397
|
-
pipeline.unshift({ $match: filter });
|
|
398
|
-
stages.push({ $unionWith: { coll: table, pipeline } });
|
|
399
|
-
});
|
|
400
|
-
}
|
|
401
|
-
let data;
|
|
402
|
-
try {
|
|
403
|
-
const results = await this.db.collection("user").aggregate(stages).toArray();
|
|
404
|
-
data = Object.assign({}, ...results);
|
|
405
|
-
} catch (error) {
|
|
406
|
-
tasks.forEach((task) => task.reject(error));
|
|
407
|
-
return;
|
|
408
|
-
}
|
|
409
|
-
for (const { expr, resolve, reject } of tasks) {
|
|
410
|
-
try {
|
|
411
|
-
resolve((0, import_orm.executeEval)(data, expr));
|
|
412
|
-
} catch (error) {
|
|
413
|
-
reject(error);
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
};
|
|
418
|
-
__name(MongoDatabase, "MongoDatabase");
|
|
419
|
-
(function(MongoDatabase2) {
|
|
420
|
-
MongoDatabase2.name = "database-mongo";
|
|
421
|
-
MongoDatabase2.Config = import_koishi2.Schema.object({
|
|
422
|
-
protocol: import_koishi2.Schema.string().description("要使用的协议名。").default("mongodb"),
|
|
423
|
-
host: import_koishi2.Schema.string().description("要连接到的主机名。").default("localhost"),
|
|
424
|
-
port: import_koishi2.Schema.natural().max(65535).description("要连接到的端口号。"),
|
|
425
|
-
username: import_koishi2.Schema.string().description("要使用的用户名。"),
|
|
426
|
-
password: import_koishi2.Schema.string().description("要使用的密码。").role("secret"),
|
|
427
|
-
database: import_koishi2.Schema.string().description("要访问的数据库名。").default("koishi")
|
|
428
|
-
});
|
|
429
|
-
})(MongoDatabase || (MongoDatabase = {}));
|
|
430
|
-
var src_default = MongoDatabase;
|
|
48
|
+
__name(apply, "apply");
|
|
431
49
|
// Annotate the CommonJS export names for ESM import in node:
|
|
432
|
-
0 && (module.exports = {
|
|
50
|
+
0 && (module.exports = {
|
|
51
|
+
Config,
|
|
52
|
+
apply,
|
|
53
|
+
name
|
|
54
|
+
});
|
|
433
55
|
//# sourceMappingURL=index.js.map
|
package/lib/index.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["../src/index.ts"
|
|
4
|
-
"sourcesContent": ["import { Db, IndexDescription, MongoClient, MongoError } from 'mongodb'\nimport { Context, Database, Dict, DriverError, isNullable, makeArray, Model, noop, omit, pick, Schema, Tables } from 'koishi'\nimport { executeEval, executeUpdate, Modifier, Query } from '@koishijs/orm'\nimport { URLSearchParams } from 'url'\nimport { transformEval, transformQuery } from './utils'\n\ndeclare module 'koishi' {\n interface Database {\n mongo: MongoDatabase\n }\n}\n\ninterface EvalTask {\n expr: any\n table: keyof Tables\n query: Query\n resolve: (value: any) => void\n reject: (error: Error) => void\n}\n\nclass MongoDatabase extends Database {\n public client: MongoClient\n public db: Db\n public mongo = this\n private _tableTasks: Dict<Promise<any>> = {}\n private _evalTasks: EvalTask[] = []\n\n constructor(public ctx: Context, private config: MongoDatabase.Config) {\n super(ctx)\n }\n\n private connectionStringFromConfig() {\n const { authDatabase, connectOptions, host, database: name, password, port, protocol, username } = this.config\n let mongourl = `${protocol}://`\n if (username) mongourl += `${encodeURIComponent(username)}${password ? `:${encodeURIComponent(password)}` : ''}@`\n mongourl += `${host}${port ? `:${port}` : ''}/${authDatabase || name}`\n if (connectOptions) {\n const params = new URLSearchParams(connectOptions)\n mongourl += `?${params}`\n }\n return mongourl\n }\n\n async start() {\n const mongourl = this.config.uri || this.connectionStringFromConfig()\n this.client = await MongoClient.connect(mongourl)\n this.db = this.client.db(this.config.database)\n\n for (const name in this.model.config) {\n this._tableTasks[name] = this._syncTable(name)\n }\n\n this.ctx.on('model', (name) => {\n this._tableTasks[name] = this._syncTable(name)\n })\n }\n\n stop() {\n return this.client.close()\n }\n\n private async _createIndexes(name: string) {\n const { primary, unique } = this.model.config[name]\n const coll = this.db.collection(name)\n const newSpecs: IndexDescription[] = []\n const oldSpecs = await coll.indexes()\n ;[primary, ...unique].forEach((keys, index) => {\n keys = makeArray(keys)\n const name = (index ? 'unique:' : 'primary:') + keys.join('+')\n if (oldSpecs.find(spec => spec.name === name)) return\n newSpecs.push({\n name,\n key: Object.fromEntries(keys.map(key => [key, 1])),\n unique: true,\n // https://docs.mongodb.com/manual/core/index-partial/#std-label-partial-index-with-unique-constraints\n partialFilterExpression: Object.fromEntries(keys.map(key => [key, { $exists: true }])),\n })\n })\n if (!newSpecs.length) return\n await coll.createIndexes(newSpecs)\n }\n\n private async _createFields(name: string) {\n const { fields } = this.model.config[name]\n const coll = this.db.collection(name)\n await Promise.all(Object.keys(fields).map((key) => {\n if (isNullable(fields[key].initial)) return\n return coll.updateMany({ [key]: { $exists: false } }, { $set: { [key]: fields[key].initial } })\n }))\n }\n\n /** synchronize table schema */\n private async _syncTable(name: string) {\n await Promise.resolve(this._tableTasks[name]).catch(noop)\n await this.db.createCollection(name).catch(noop)\n await Promise.all([\n this._createIndexes(name),\n this._createFields(name),\n ])\n }\n\n private _createFilter(name: keyof Tables, query: Query) {\n return transformQuery(this.resolveQuery(name, query))\n }\n\n async drop() {\n await Promise.all(Object.keys(this.model.config).map(name => this.db.dropCollection(name)))\n }\n\n private async _collStats() {\n const tables = Object.keys(this.model.config)\n const entries = await Promise.all(tables.map(async (name) => {\n const coll = this.db.collection(name)\n const { count, size } = await coll.stats()\n return [coll.collectionName, { count, size }] as const\n }))\n return Object.fromEntries(entries)\n }\n\n async stats() {\n // https://docs.mongodb.com/manual/reference/command/dbStats/#std-label-dbstats-output\n const [stats, tables] = await Promise.all([\n this.db.stats(),\n this._collStats(),\n ])\n // While MongoDB's document above says that the `stats.totalSize` is the sum of\n // `stats.dataSize` and `stats.storageSize`, it's actually `undefined` in some cases.\n // So we have to calculate it manually.\n const totalSize = stats.indexSize + stats.storageSize\n return { size: totalSize, tables }\n }\n\n async get(name: keyof Tables, query: Query, modifier: Modifier) {\n const filter = this._createFilter(name, query)\n if (!filter) return []\n await Promise.resolve(this._tableTasks[name]).catch(noop)\n let cursor = this.db.collection(name).find(filter)\n const { fields, limit, offset = 0, sort } = this.resolveModifier(name, modifier)\n if (offset) cursor = cursor.skip(offset)\n if (limit) cursor = cursor.limit(offset + limit)\n if (sort) cursor = cursor.sort(sort)\n return (await cursor.toArray()).map(row => this.resolveData(name, row, fields)) as any\n }\n\n async set(name: keyof Tables, query: Query, update: {}) {\n const filter = this._createFilter(name, query)\n if (!filter) return\n await Promise.resolve(this._tableTasks[name]).catch(noop)\n const { primary } = this.model.config[name]\n const indexFields = makeArray(primary)\n const coll = this.db.collection(name)\n const original = await coll.find(filter).toArray()\n if (!original.length) return\n update = this.model.format(name, update)\n const updateFields = new Set(Object.keys(update).map(key => key.split('.', 1)[0]))\n const bulk = coll.initializeUnorderedBulkOp()\n for (const item of original) {\n bulk.find(pick(item, indexFields)).updateOne({\n $set: pick(executeUpdate(item, update), updateFields),\n })\n }\n await bulk.execute()\n }\n\n async remove(name: keyof Tables, query: Query) {\n const filter = this._createFilter(name, query)\n if (!filter) return\n await this.db.collection(name).deleteMany(filter)\n }\n\n private queue(name: keyof Tables, callback: () => Promise<any>) {\n return this._tableTasks[name] = Promise.resolve(this._tableTasks[name]).catch(noop).then(callback)\n }\n\n async create<T extends keyof Tables>(name: T, data: any) {\n const coll = this.db.collection(name)\n return this.queue(name, async () => {\n const { primary, fields, autoInc } = this.model.config[name]\n if (autoInc && !Array.isArray(primary) && !(primary in data)) {\n const [latest] = await coll.find().sort(primary, -1).limit(1).toArray()\n data[primary] = latest ? +latest[primary] + 1 : 1\n if (Model.Field.string.includes(fields[primary].type)) {\n data[primary] += ''\n data[primary] = data[primary].padStart(8, '0')\n }\n }\n const copy = this.model.create(name, data)\n try {\n await coll.insertOne(copy)\n delete copy['_id']\n return copy\n } catch (err) {\n if (err instanceof MongoError && err.code === 11000) {\n throw new DriverError('duplicate-entry', err.message)\n }\n throw err\n }\n })\n }\n\n async upsert(name: keyof Tables, data: any[], keys: string | string[]) {\n if (!data.length) return\n if (!keys) keys = this.model.config[name].primary\n const indexFields = makeArray(keys)\n await Promise.resolve(this._tableTasks[name]).catch(noop)\n const coll = this.db.collection(name)\n const original = await coll.find({ $or: data.map(item => pick(item, indexFields)) }).toArray()\n const bulk = coll.initializeUnorderedBulkOp()\n for (const _item of data) {\n const update = this.model.format(name, _item)\n const item = original.find(item => indexFields.every(key => item[key].valueOf() === update[key].valueOf()))\n if (item) {\n const updateFields = new Set(Object.keys(update).map(key => key.split('.', 1)[0]))\n const override = omit(pick(executeUpdate(item, update), updateFields), indexFields)\n bulk.find(pick(item, indexFields)).updateOne({ $set: override })\n } else {\n bulk.insert(executeUpdate(this.model.create(name), update))\n }\n }\n await bulk.execute()\n }\n\n eval(table: keyof Tables, expr: any, query: Query) {\n return new Promise<any>((resolve, reject) => {\n this._evalTasks.push({ expr, table, query, resolve, reject })\n process.nextTick(() => this._flushEvalTasks())\n })\n }\n\n private async _flushEvalTasks() {\n const tasks = this._evalTasks\n if (!tasks.length) return\n this._evalTasks = []\n\n const stages: any[] = [{ $match: { _id: null } }]\n for (const task of tasks) {\n const { expr, table, query } = task\n task.expr = transformEval(expr, (pipeline) => {\n const filter = this._createFilter(table, query) || { _id: null }\n pipeline.unshift({ $match: filter })\n stages.push({ $unionWith: { coll: table, pipeline } })\n })\n }\n\n let data: any\n try {\n const results = await this.db.collection('user').aggregate(stages).toArray()\n data = Object.assign({}, ...results)\n } catch (error) {\n tasks.forEach(task => task.reject(error))\n return\n }\n\n for (const { expr, resolve, reject } of tasks) {\n try {\n resolve(executeEval(data, expr))\n } catch (error) {\n reject(error)\n }\n }\n }\n}\n\nnamespace MongoDatabase {\n export const name = 'database-mongo'\n\n export interface Config {\n username?: string\n password?: string\n protocol?: string\n host?: string\n port?: number\n /** database name */\n database?: string\n /** default auth database */\n authDatabase?: string\n connectOptions?: ConstructorParameters<typeof URLSearchParams>[0]\n /** connection string (will overwrite all configs except 'name') */\n uri?: string\n }\n\n export const Config = Schema.object({\n protocol: Schema.string().description('要使用的协议名。').default('mongodb'),\n host: Schema.string().description('要连接到的主机名。').default('localhost'),\n port: Schema.natural().max(65535).description('要连接到的端口号。'),\n username: Schema.string().description('要使用的用户名。'),\n password: Schema.string().description('要使用的密码。').role('secret'),\n database: Schema.string().description('要访问的数据库名。').default('koishi'),\n })\n}\n\nexport default MongoDatabase\n", "import { Random, valueMap } from 'koishi'\nimport { Query } from '@koishijs/orm'\nimport { Filter, FilterOperators } from 'mongodb'\n\nfunction createFieldFilter(query: Query.FieldQuery, key: string) {\n const filters: Filter<any>[] = []\n const result: Filter<any> = {}\n const child = transformFieldQuery(query, key, filters)\n if (child === false) return false\n if (child !== true) result[key] = child\n if (filters.length) result.$and = filters\n if (Object.keys(result).length) return result\n return true\n}\n\nfunction transformFieldQuery(query: Query.FieldQuery, key: string, filters: Filter<any>[]) {\n // shorthand syntax\n if (typeof query === 'string' || typeof query === 'number' || query instanceof Date) {\n return { $eq: query }\n } else if (Array.isArray(query)) {\n if (!query.length) return false\n return { $in: query }\n } else if (query instanceof RegExp) {\n return { $regex: query }\n }\n\n // query operators\n const result: FilterOperators<any> = {}\n for (const prop in query) {\n if (prop === '$and') {\n for (const item of query[prop]) {\n const child = createFieldFilter(item, key)\n if (child === false) return false\n if (child !== true) filters.push(child)\n }\n } else if (prop === '$or') {\n const $or: Filter<any>[] = []\n if (!query[prop].length) return false\n const always = query[prop].some((item) => {\n const child = createFieldFilter(item, key)\n if (typeof child === 'boolean') return child\n $or.push(child)\n })\n if (!always) filters.push({ $or })\n } else if (prop === '$not') {\n const child = createFieldFilter(query[prop], key)\n if (child === true) return false\n if (child !== false) filters.push({ $nor: [child] })\n } else if (prop === '$el') {\n const child = transformFieldQuery(query[prop], key, filters)\n if (child === false) return false\n if (child !== true) result.$elemMatch = child\n } else if (prop === '$regexFor') {\n filters.push({\n $expr: {\n $function: {\n body: function (data: string, value: string) {\n return new RegExp(data, 'i').test(value)\n }.toString(),\n args: ['$' + key, query.$regexFor],\n lang: 'js',\n },\n },\n })\n } else {\n result[prop] = query[prop]\n }\n }\n if (!Object.keys(result).length) return true\n return result\n}\n\nexport function transformQuery(query: Query.Expr) {\n const filter: Filter<any> = {}\n const additional: Filter<any>[] = []\n for (const key in query) {\n const value = query[key]\n if (key === '$and' || key === '$or') {\n // MongoError: $and/$or/$nor must be a nonempty array\n // { $and: [] } matches everything\n // { $or: [] } matches nothing\n if (value.length) {\n filter[key] = value.map(transformQuery)\n } else if (key === '$or') {\n return\n }\n } else if (key === '$not') {\n // MongoError: unknown top level operator: $not\n // https://stackoverflow.com/questions/25270396/mongodb-how-to-invert-query-with-not\n // this may solve this problem but lead to performance degradation\n const query = transformQuery(value)\n if (query) filter.$nor = [query]\n } else if (key === '$expr') {\n additional.push({ $expr: transformEval(value) })\n } else {\n const query = transformFieldQuery(value, key, additional)\n if (query === false) return\n if (query !== true) filter[key] = query\n }\n }\n if (additional.length) {\n (filter.$and ||= []).push(...additional)\n }\n return filter\n}\n\nfunction transformEvalExpr(expr: any, onAggr?: (pipeline: any[]) => void) {\n return valueMap(expr as any, (value) => {\n if (Array.isArray(value)) {\n return value.map(val => transformEval(val, onAggr))\n } else {\n return transformEval(value, onAggr)\n }\n })\n}\n\nfunction transformAggr(expr: any) {\n if (typeof expr === 'string') {\n return '$' + expr\n }\n return transformEvalExpr(expr)\n}\n\nconst aggrKeys = ['$sum', '$avg', '$min', '$max', '$count']\n\nexport function transformEval(expr: any, onAggr?: (pipeline: any[]) => void) {\n if (typeof expr === 'number' || typeof expr === 'string' || typeof expr === 'boolean') {\n return expr\n } else if (expr.$) {\n return '$' + expr.$\n }\n\n for (const key of aggrKeys) {\n if (!expr[key]) continue\n const value = transformAggr(expr[key])\n const $ = Random.id()\n if (key === '$count') {\n onAggr([\n { $group: { _id: value } },\n { $group: { _id: null, [$]: { $count: {} } } },\n ])\n } else {\n onAggr([{ $group: { _id: null, [$]: { [key]: value } } }])\n }\n return { $ }\n }\n\n return transformEvalExpr(expr, onAggr)\n}\n"],
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA
|
|
3
|
+
"sources": ["../src/index.ts"],
|
|
4
|
+
"sourcesContent": ["import { Context, Schema } from 'koishi'\nimport MongoDriver from '@cosmotype/driver-mongo'\n\nexport const name = 'MemoryDatabase'\n\nexport type Config = MongoDriver.Config\n\nexport const Config: Schema<Config> = Schema.object({\n protocol: Schema.string().description('要使用的协议名。').default('mongodb'),\n host: Schema.string().description('要连接到的主机名。').default('localhost'),\n port: Schema.natural().max(65535).description('要连接到的端口号。'),\n username: Schema.string().description('要使用的用户名。'),\n password: Schema.string().description('要使用的密码。').role('secret'),\n database: Schema.string().description('要访问的数据库名。').default('koishi'),\n})\n\nexport function apply(ctx: Context, config: Config) {\n const driver = new MongoDriver(ctx.model, config)\n ctx.on('ready', () => driver.start())\n ctx.on('dispose', () => driver.stop())\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,oBAAgC;AAChC,0BAAwB;AAEjB,IAAM,OAAO;AAIb,IAAM,SAAyB,qBAAO,OAAO;AAAA,EAClD,UAAU,qBAAO,SAAS,YAAY,YAAY,QAAQ;AAAA,EAC1D,MAAM,qBAAO,SAAS,YAAY,aAAa,QAAQ;AAAA,EACvD,MAAM,qBAAO,UAAU,IAAI,OAAO,YAAY;AAAA,EAC9C,UAAU,qBAAO,SAAS,YAAY;AAAA,EACtC,UAAU,qBAAO,SAAS,YAAY,WAAW,KAAK;AAAA,EACtD,UAAU,qBAAO,SAAS,YAAY,aAAa,QAAQ;AAAA;AAGtD,eAAe,KAAc,QAAgB;AAClD,QAAM,SAAS,IAAI,4BAAY,IAAI,OAAO;AAC1C,MAAI,GAAG,SAAS,MAAM,OAAO;AAC7B,MAAI,GAAG,WAAW,MAAM,OAAO;AAAA;AAHjB;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@koishijs/plugin-database-mongo",
|
|
3
3
|
"description": "MongoDB support for Koishi",
|
|
4
|
-
"version": "3.
|
|
4
|
+
"version": "3.2.1",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"typings": "lib/index.d.ts",
|
|
7
7
|
"files": [
|
|
@@ -29,15 +29,10 @@
|
|
|
29
29
|
"mysql",
|
|
30
30
|
"impl:database"
|
|
31
31
|
],
|
|
32
|
-
"devDependencies": {
|
|
33
|
-
"@koishijs/database-tests": "^1.0.0",
|
|
34
|
-
"@koishijs/plugin-mock": "^1.0.3"
|
|
35
|
-
},
|
|
36
32
|
"peerDependencies": {
|
|
37
|
-
"koishi": "^4.
|
|
33
|
+
"koishi": "^4.6.1"
|
|
38
34
|
},
|
|
39
35
|
"dependencies": {
|
|
40
|
-
"@
|
|
41
|
-
"mongodb": "^4.4.1"
|
|
36
|
+
"@cosmotype/driver-mongo": "^1.0.2"
|
|
42
37
|
}
|
|
43
38
|
}
|