@waline/vercel 1.13.5 → 1.15.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/package.json +12 -3
- package/src/config/config.js +5 -0
- package/src/config/extend.js +16 -8
- package/src/controller/comment.js +110 -17
- package/src/controller/db.js +6 -6
- package/src/controller/index.js +4 -2
- package/src/controller/token/2fa.js +1 -1
- package/src/controller/user/password.js +7 -2
- package/src/controller/user.js +32 -16
- package/src/controller/verification.js +3 -3
- package/src/extend/controller.js +11 -0
- package/src/extend/think.js +11 -0
- package/src/locales/en.json +15 -0
- package/src/locales/index.js +12 -0
- package/src/locales/zh-CN.json +15 -0
- package/src/locales/zh-TW.json +15 -0
- package/src/middleware/dashboard.js +1 -2
- package/src/service/storage/cloudbase.js +27 -7
- package/src/service/storage/deta.js +39 -8
- package/src/service/storage/github.js +17 -2
- package/src/service/storage/leancloud.js +40 -5
- package/src/service/storage/mongodb.js +14 -2
- package/src/service/storage/mysql.js +13 -2
- package/src/service/storage/postgresql.js +7 -1
package/package.json
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@waline/vercel",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.15.0",
|
|
4
4
|
"description": "vercel server for waline comment system",
|
|
5
|
-
"
|
|
5
|
+
"keywords": [
|
|
6
|
+
"waline",
|
|
7
|
+
"vercel",
|
|
8
|
+
"comment",
|
|
9
|
+
"blog"
|
|
10
|
+
],
|
|
11
|
+
"repository": {
|
|
12
|
+
"url": "https://github.com/walinejs/waline",
|
|
13
|
+
"directory": "packages/server"
|
|
14
|
+
},
|
|
6
15
|
"license": "MIT",
|
|
7
16
|
"author": "lizheming <i@imnerd.org>",
|
|
8
17
|
"dependencies": {
|
|
@@ -33,7 +42,7 @@
|
|
|
33
42
|
"think-model-mysql": "^1.1.7",
|
|
34
43
|
"think-model-postgresql": "1.1.6",
|
|
35
44
|
"think-model-sqlite": "^1.2.3",
|
|
36
|
-
"think-mongo": "^2.1
|
|
45
|
+
"think-mongo": "^2.2.1",
|
|
37
46
|
"think-router-rest": "^1.0.5",
|
|
38
47
|
"thinkjs": "^3.2.14",
|
|
39
48
|
"ua-parser-js": "^1.0.2"
|
package/src/config/config.js
CHANGED
|
@@ -37,6 +37,8 @@ const {
|
|
|
37
37
|
TG_TEMPLATE,
|
|
38
38
|
WX_TEMPLATE,
|
|
39
39
|
DISCORD_TEMPLATE,
|
|
40
|
+
|
|
41
|
+
LEVELS,
|
|
40
42
|
} = process.env;
|
|
41
43
|
|
|
42
44
|
let storage = 'leancloud';
|
|
@@ -104,6 +106,9 @@ module.exports = {
|
|
|
104
106
|
disallowIPList: [],
|
|
105
107
|
secureDomains: SECURE_DOMAINS ? SECURE_DOMAINS.split(/\s*,\s*/) : undefined,
|
|
106
108
|
disableUserAgent: DISABLE_USERAGENT && !isFalse(DISABLE_USERAGENT),
|
|
109
|
+
levels: isFalse(LEVELS)
|
|
110
|
+
? false
|
|
111
|
+
: LEVELS.split(/\s*,\s*/).map((v) => Number(v)),
|
|
107
112
|
avatarProxy,
|
|
108
113
|
oauthUrl,
|
|
109
114
|
markdown,
|
package/src/config/extend.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const Model = require('think-model');
|
|
2
2
|
const Mongo = require('think-mongo');
|
|
3
|
+
const request = require('request-promise-native');
|
|
3
4
|
|
|
4
5
|
module.exports = [
|
|
5
6
|
Model(think.app),
|
|
@@ -12,16 +13,23 @@ module.exports = [
|
|
|
12
13
|
return SERVER_URL;
|
|
13
14
|
}
|
|
14
15
|
|
|
15
|
-
const { protocol, host
|
|
16
|
-
return `${protocol}://${host}
|
|
16
|
+
const { protocol, host } = this;
|
|
17
|
+
return `${protocol}://${host}`;
|
|
17
18
|
},
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
this.ctx.status = 500;
|
|
19
|
+
async webhook(type, data) {
|
|
20
|
+
const { WEBHOOK } = process.env;
|
|
21
|
+
if (!WEBHOOK) {
|
|
22
|
+
return;
|
|
23
23
|
}
|
|
24
|
-
|
|
24
|
+
|
|
25
|
+
return request({
|
|
26
|
+
uri: WEBHOOK,
|
|
27
|
+
method: 'POST',
|
|
28
|
+
headers: {
|
|
29
|
+
'content-type': 'application/json',
|
|
30
|
+
},
|
|
31
|
+
body: JSON.stringify({ type, data }),
|
|
32
|
+
});
|
|
25
33
|
},
|
|
26
34
|
},
|
|
27
35
|
},
|
|
@@ -218,7 +218,12 @@ module.exports = class extends BaseRest {
|
|
|
218
218
|
};
|
|
219
219
|
}
|
|
220
220
|
|
|
221
|
-
const
|
|
221
|
+
const totalCount = await this.modelInstance.count(where);
|
|
222
|
+
const pageOffset = Math.max((page - 1) * pageSize, 0);
|
|
223
|
+
let comments = [];
|
|
224
|
+
let rootComments = [];
|
|
225
|
+
let rootCount = 0;
|
|
226
|
+
const selectOptions = {
|
|
222
227
|
desc: 'insertedAt',
|
|
223
228
|
field: [
|
|
224
229
|
'status',
|
|
@@ -233,7 +238,54 @@ module.exports = class extends BaseRest {
|
|
|
233
238
|
'user_id',
|
|
234
239
|
'sticky',
|
|
235
240
|
],
|
|
236
|
-
}
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* most of case we have just little comments
|
|
245
|
+
* while if we want get rootComments, rootCount, childComments with pagination
|
|
246
|
+
* we have to query three times from storage service
|
|
247
|
+
* That's so expensive for user, especially in the serverless.
|
|
248
|
+
* so we have a comments length check
|
|
249
|
+
* If you have less than 1000 comments, then we'll get all comments one time
|
|
250
|
+
* then we'll compute rootComment, rootCount, childComments in program to reduce http request query
|
|
251
|
+
*
|
|
252
|
+
* Why we have limit and the limit is 1000?
|
|
253
|
+
* Many serverless storages have fetch data limit, for example LeanCloud is 100, and CloudBase is 1000
|
|
254
|
+
* If we have much commments, We should use more request to fetch all comments
|
|
255
|
+
* If we have 3000 comments, We have to use 30 http request to fetch comments, things go athwart.
|
|
256
|
+
* And Serverless Service like vercel have excute time limit
|
|
257
|
+
* if we have more http requests in a serverless function, it may timeout easily.
|
|
258
|
+
* so we use limit to avoid it.
|
|
259
|
+
*/
|
|
260
|
+
if (totalCount < 1000) {
|
|
261
|
+
comments = await this.modelInstance.select(where, selectOptions);
|
|
262
|
+
rootCount = comments.filter(({ rid }) => !rid).length;
|
|
263
|
+
rootComments = [
|
|
264
|
+
...comments.filter(({ rid, sticky }) => !rid && sticky),
|
|
265
|
+
...comments.filter(({ rid, sticky }) => !rid && !sticky),
|
|
266
|
+
].slice(pageOffset, pageOffset + pageSize);
|
|
267
|
+
} else {
|
|
268
|
+
rootComments = await this.modelInstance.select(
|
|
269
|
+
{ ...where, rid: undefined },
|
|
270
|
+
{
|
|
271
|
+
...selectOptions,
|
|
272
|
+
offset: pageOffset,
|
|
273
|
+
limit: pageSize,
|
|
274
|
+
}
|
|
275
|
+
);
|
|
276
|
+
const children = await this.modelInstance.select(
|
|
277
|
+
{
|
|
278
|
+
...where,
|
|
279
|
+
rid: ['IN', rootComments.map(({ objectId }) => objectId)],
|
|
280
|
+
},
|
|
281
|
+
selectOptions
|
|
282
|
+
);
|
|
283
|
+
comments = [...rootComments, ...children];
|
|
284
|
+
rootCount = await this.modelInstance.count({
|
|
285
|
+
...where,
|
|
286
|
+
rid: undefined,
|
|
287
|
+
});
|
|
288
|
+
}
|
|
237
289
|
|
|
238
290
|
const userModel = this.service(
|
|
239
291
|
`storage/${this.config('storage')}`,
|
|
@@ -253,18 +305,55 @@ module.exports = class extends BaseRest {
|
|
|
253
305
|
);
|
|
254
306
|
}
|
|
255
307
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
308
|
+
if (think.isArray(this.config('levels'))) {
|
|
309
|
+
const countWhere = {
|
|
310
|
+
status: ['NOT IN', ['waiting', 'spam']],
|
|
311
|
+
_complex: {},
|
|
312
|
+
};
|
|
313
|
+
if (user_ids.length) {
|
|
314
|
+
countWhere._complex.user_id = ['IN', user_ids];
|
|
315
|
+
}
|
|
316
|
+
const mails = Array.from(
|
|
317
|
+
new Set(comments.map(({ mail }) => mail).filter((v) => v))
|
|
318
|
+
);
|
|
319
|
+
if (mails.length) {
|
|
320
|
+
countWhere._complex.mail = ['IN', mails];
|
|
321
|
+
}
|
|
322
|
+
if (!think.isEmpty(countWhere._complex)) {
|
|
323
|
+
countWhere._complex._logic = 'or';
|
|
324
|
+
} else {
|
|
325
|
+
delete countWhere._complex;
|
|
326
|
+
}
|
|
327
|
+
const counts = await this.modelInstance.count(countWhere, {
|
|
328
|
+
group: ['user_id', 'mail'],
|
|
329
|
+
});
|
|
330
|
+
comments.forEach((cmt) => {
|
|
331
|
+
const countItem = (counts || []).find(({ mail, user_id }) => {
|
|
332
|
+
if (user_id) {
|
|
333
|
+
return user_id === cmt.user_id;
|
|
334
|
+
}
|
|
335
|
+
return mail === cmt.mail;
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
let level = 0;
|
|
339
|
+
if (countItem) {
|
|
340
|
+
const _level = think.findLastIndex(
|
|
341
|
+
this.config('levels'),
|
|
342
|
+
(l) => l <= countItem.count
|
|
343
|
+
);
|
|
344
|
+
if (_level !== -1) {
|
|
345
|
+
level = _level;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
cmt.level = level;
|
|
349
|
+
});
|
|
350
|
+
}
|
|
262
351
|
|
|
263
352
|
return this.json({
|
|
264
353
|
page,
|
|
265
354
|
totalPages: Math.ceil(rootCount / pageSize),
|
|
266
355
|
pageSize,
|
|
267
|
-
count:
|
|
356
|
+
count: totalCount,
|
|
268
357
|
data: await Promise.all(
|
|
269
358
|
rootComments.map(async (comment) => {
|
|
270
359
|
const cmt = await formatCmt(
|
|
@@ -343,7 +432,7 @@ module.exports = class extends BaseRest {
|
|
|
343
432
|
'The comment author had post same comment content before'
|
|
344
433
|
);
|
|
345
434
|
|
|
346
|
-
return this.fail('Duplicate Content');
|
|
435
|
+
return this.fail(this.locale('Duplicate Content'));
|
|
347
436
|
}
|
|
348
437
|
|
|
349
438
|
think.logger.debug('Comment duplicate check OK!');
|
|
@@ -358,7 +447,7 @@ module.exports = class extends BaseRest {
|
|
|
358
447
|
|
|
359
448
|
if (!think.isEmpty(recent)) {
|
|
360
449
|
think.logger.debug(`The author has posted in ${IPQPS} seconeds.`);
|
|
361
|
-
return this.fail('Comment too fast!');
|
|
450
|
+
return this.fail(this.locale('Comment too fast!'));
|
|
362
451
|
}
|
|
363
452
|
|
|
364
453
|
think.logger.debug(`Comment post frequence check OK!`);
|
|
@@ -415,21 +504,25 @@ module.exports = class extends BaseRest {
|
|
|
415
504
|
|
|
416
505
|
think.logger.debug(`Comment have been added to storage.`);
|
|
417
506
|
|
|
418
|
-
let
|
|
419
|
-
|
|
507
|
+
let parentComment;
|
|
420
508
|
if (pid) {
|
|
421
|
-
|
|
422
|
-
|
|
509
|
+
parentComment = await this.modelInstance.select({ objectId: pid });
|
|
510
|
+
parentComment = parentComment[0];
|
|
423
511
|
}
|
|
424
512
|
|
|
513
|
+
await this.ctx.webhook('new_comment', {
|
|
514
|
+
comment: { ...resp, rawComment: comment },
|
|
515
|
+
reply: parentComment,
|
|
516
|
+
});
|
|
517
|
+
|
|
425
518
|
if (comment.status !== 'spam') {
|
|
426
519
|
const notify = this.service('notify');
|
|
427
|
-
await notify.run({ ...resp, rawComment: comment },
|
|
520
|
+
await notify.run({ ...resp, rawComment: comment }, parentComment);
|
|
428
521
|
}
|
|
429
522
|
|
|
430
523
|
think.logger.debug(`Comment notify done!`);
|
|
431
524
|
|
|
432
|
-
await this.hook('postSave', resp,
|
|
525
|
+
await this.hook('postSave', resp, parentComment);
|
|
433
526
|
|
|
434
527
|
think.logger.debug(`Comment post hooks postSave done!`);
|
|
435
528
|
|
package/src/controller/db.js
CHANGED
|
@@ -19,10 +19,9 @@ module.exports = class extends BaseRest {
|
|
|
19
19
|
|
|
20
20
|
for (let i = 0; i < exportData.tables.length; i++) {
|
|
21
21
|
const tableName = exportData.tables[i];
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
);
|
|
22
|
+
const storage = this.config('storage');
|
|
23
|
+
const model = this.service(`storage/${storage}`, tableName);
|
|
24
|
+
|
|
26
25
|
const data = await model.select({});
|
|
27
26
|
exportData.data[tableName] = data;
|
|
28
27
|
}
|
|
@@ -36,12 +35,13 @@ module.exports = class extends BaseRest {
|
|
|
36
35
|
const jsonText = await readFileAsync(file.path, 'utf-8');
|
|
37
36
|
const importData = JSON.parse(jsonText);
|
|
38
37
|
if (!importData || importData.type !== 'waline') {
|
|
39
|
-
return this.fail('import data format not support!');
|
|
38
|
+
return this.fail(this.locale('import data format not support!'));
|
|
40
39
|
}
|
|
41
40
|
|
|
42
41
|
for (let i = 0; i < importData.tables.length; i++) {
|
|
43
42
|
const tableName = importData.tables[i];
|
|
44
|
-
const
|
|
43
|
+
const storage = this.config('storage');
|
|
44
|
+
const model = this.service(`storage/${storage}`, tableName);
|
|
45
45
|
|
|
46
46
|
// delete all data at first
|
|
47
47
|
await model.delete({});
|
package/src/controller/index.js
CHANGED
|
@@ -12,7 +12,9 @@ module.exports = class extends think.Controller {
|
|
|
12
12
|
<title>Waline Example</title>
|
|
13
13
|
</head>
|
|
14
14
|
<body>
|
|
15
|
-
<div id="waline" style="max-width: 800px;margin: 0 auto;"></div>
|
|
15
|
+
<div id="waline" style="max-width: 800px;margin: 0 auto;"></div>
|
|
16
|
+
<script src="https://unpkg.com/@waline/client@v2/dist/waline.js"></script>
|
|
17
|
+
<link href='//unpkg.com/@waline/client@v2/dist/waline.css' rel='stylesheet' />
|
|
16
18
|
<script>
|
|
17
19
|
console.log(
|
|
18
20
|
'%c @waline/server %c v${version} ',
|
|
@@ -20,7 +22,7 @@ module.exports = class extends think.Controller {
|
|
|
20
22
|
'padding:4px;border:1px solid #0078E7;'
|
|
21
23
|
);
|
|
22
24
|
const params = new URLSearchParams(location.search.slice(1));
|
|
23
|
-
const waline =
|
|
25
|
+
const waline = Waline.init({
|
|
24
26
|
el: '#waline',
|
|
25
27
|
path: params.get('path') || '/',
|
|
26
28
|
lang: params.get('lng'),
|
|
@@ -36,8 +36,13 @@ module.exports = class extends BaseRest {
|
|
|
36
36
|
? `"${SENDER_NAME}" <${SENDER_EMAIL}>`
|
|
37
37
|
: SMTP_USER,
|
|
38
38
|
to: user[0].email,
|
|
39
|
-
subject:
|
|
40
|
-
|
|
39
|
+
subject: this.locale('[{{name}}] Reset Password', {
|
|
40
|
+
name: SITE_NAME || 'Waline',
|
|
41
|
+
}),
|
|
42
|
+
html: this.locale(
|
|
43
|
+
'Please click <a href="{{url}}">{{url}}</a> to login and change your password as soon as possible!',
|
|
44
|
+
{ url: profileUrl }
|
|
45
|
+
),
|
|
41
46
|
});
|
|
42
47
|
|
|
43
48
|
return this.success();
|
package/src/controller/user.js
CHANGED
|
@@ -21,7 +21,7 @@ module.exports = class extends BaseRest {
|
|
|
21
21
|
!think.isEmpty(resp) &&
|
|
22
22
|
['administrator', 'guest'].includes(resp[0].type)
|
|
23
23
|
) {
|
|
24
|
-
return this.fail('USER_EXIST');
|
|
24
|
+
return this.fail(this.locale('USER_EXIST'));
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
const count = await this.modelInstance.count();
|
|
@@ -56,21 +56,37 @@ module.exports = class extends BaseRest {
|
|
|
56
56
|
return this.success();
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
59
|
+
try {
|
|
60
|
+
const notify = this.service('notify');
|
|
61
|
+
const apiUrl =
|
|
62
|
+
this.ctx.serverURL +
|
|
63
|
+
'/verification?' +
|
|
64
|
+
qs.stringify({ token, email: data.email });
|
|
65
|
+
|
|
66
|
+
await notify.transporter.sendMail({
|
|
67
|
+
from:
|
|
68
|
+
SENDER_EMAIL && SENDER_NAME
|
|
69
|
+
? `"${SENDER_NAME}" <${SENDER_EMAIL}>`
|
|
70
|
+
: SMTP_USER,
|
|
71
|
+
to: data.email,
|
|
72
|
+
subject: this.locale('[{{name}}] Registration Confirm Mail', {
|
|
73
|
+
name: SITE_NAME || 'Waline',
|
|
74
|
+
}),
|
|
75
|
+
html: this.locale(
|
|
76
|
+
'Please click <a href="{{url}}">{{url}}<a/> to confirm registration, the link is valid for 1 hour. If you are not registering, please ignore this email.',
|
|
77
|
+
{ url: apiUrl }
|
|
78
|
+
),
|
|
79
|
+
});
|
|
80
|
+
} catch (e) {
|
|
81
|
+
console.log(e);
|
|
82
|
+
|
|
83
|
+
return this.fail(
|
|
84
|
+
this.locale(
|
|
85
|
+
'Registeration confirm mail send failed, please {%- if isAdmin -%}check your mail configuration{%- else -%}check your email address and contact administrator{%- endif -%}.',
|
|
86
|
+
{ isAdmin: think.isEmpty(count) }
|
|
87
|
+
)
|
|
88
|
+
);
|
|
89
|
+
}
|
|
74
90
|
|
|
75
91
|
return this.success({ verify: true });
|
|
76
92
|
}
|
|
@@ -13,13 +13,13 @@ module.exports = class extends BaseRest {
|
|
|
13
13
|
const { token, email } = this.get();
|
|
14
14
|
const users = await this.modelInstance.select({ email });
|
|
15
15
|
if (think.isEmpty(users)) {
|
|
16
|
-
return this.fail('USER_NOT_EXIST');
|
|
16
|
+
return this.fail(this.locale('USER_NOT_EXIST'));
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
const user = users[0];
|
|
20
20
|
const match = user.type.match(/^verify:(\d{4}):(\d+)$/i);
|
|
21
21
|
if (!match) {
|
|
22
|
-
return this.fail('USER_REGISTED');
|
|
22
|
+
return this.fail(this.locale('USER_REGISTED'));
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
if (token === match[1] && Date.now() < parseInt(match[2])) {
|
|
@@ -27,6 +27,6 @@ module.exports = class extends BaseRest {
|
|
|
27
27
|
return this.redirect('/ui/login');
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
return this.fail('TOKEN_EXPIRED');
|
|
30
|
+
return this.fail(this.locale('TOKEN_EXPIRED'));
|
|
31
31
|
}
|
|
32
32
|
};
|
package/src/extend/controller.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
const nunjucks = require('nunjucks');
|
|
2
|
+
const locales = require('../locales');
|
|
3
|
+
|
|
1
4
|
module.exports = {
|
|
2
5
|
success(...args) {
|
|
3
6
|
this.ctx.success(...args);
|
|
@@ -7,4 +10,12 @@ module.exports = {
|
|
|
7
10
|
this.ctx.fail(...args);
|
|
8
11
|
return think.prevent();
|
|
9
12
|
},
|
|
13
|
+
locale(message, variables) {
|
|
14
|
+
const { lang } = this.get();
|
|
15
|
+
const locale = locales[(lang || '').toLowerCase()];
|
|
16
|
+
if (locale && locale[message]) {
|
|
17
|
+
message = locale[message];
|
|
18
|
+
}
|
|
19
|
+
return nunjucks.renderString(message, variables);
|
|
20
|
+
},
|
|
10
21
|
};
|
package/src/extend/think.js
CHANGED
|
@@ -7,4 +7,15 @@ module.exports = {
|
|
|
7
7
|
isPrevent(err) {
|
|
8
8
|
return think.isError(err) && err.message === preventMessage;
|
|
9
9
|
},
|
|
10
|
+
findLastIndex(arr, fn) {
|
|
11
|
+
for (let i = arr.length - 1; i >= 0; i--) {
|
|
12
|
+
const ret = fn(arr[i], i, arr);
|
|
13
|
+
if (!ret) {
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
return i;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return -1;
|
|
20
|
+
},
|
|
10
21
|
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"import data format not support!": "import data format not support!",
|
|
3
|
+
"USER_EXIST": "USER_EXIST",
|
|
4
|
+
"USER_NOT_EXIST": "USER_NOT_EXIST",
|
|
5
|
+
"USER_REGISTED": "USER_REGISTED",
|
|
6
|
+
"TOKEN_EXPIRED": "TOKEN_EXPIRED",
|
|
7
|
+
"TWO_FACTOR_AUTH_ERROR_DETAIL": "TWO_FACTOR_AUTH_ERROR_DETAIL",
|
|
8
|
+
"[{{name}}] Registration Confirm Mail": "[{{name}}] Registration Confirm Mail",
|
|
9
|
+
"Please click <a href=\"{{url}}\">{{url}}<a/> to confirm registration, the link is valid for 1 hour. If you are not registering, please ignore this email.": "Please click <a href=\"{{url}}\">{{url}}<a/> to confirm registration, the link is valid for 1 hour. If you are not registering, please ignore this email.",
|
|
10
|
+
"[{{name}}] Reset Password": "[{{name}}] Reset Password",
|
|
11
|
+
"Please click <a href=\"{{url}}\">{{url}}</a> to login and change your password as soon as possible!": "Please click <a href=\"{{url}}\">{{url}}</a> to login and change your password as soon as possible!",
|
|
12
|
+
"Duplicate Content": "Duplicate Content",
|
|
13
|
+
"Comment too fast": "Comment too fast",
|
|
14
|
+
"Registeration confirm mail send failed, please {%- if isAdmin -%}check your mail configuration{%- else -%}check your email address and contact administrator{%- endif -%}.": "Registeration confirm mail send failed, please {%- if isAdmin -%}check your mail configuration{%- else -%}check your email address and contact administrator{%- endif -%}."
|
|
15
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"import data format not support!": "文件格式不支持",
|
|
3
|
+
"USER_EXIST": "用户已存在",
|
|
4
|
+
"USER_NOT_EXIST": "用户不存在",
|
|
5
|
+
"USER_REGISTED": "用户已注册",
|
|
6
|
+
"TOKEN_EXPIRED": "密钥已过期",
|
|
7
|
+
"TWO_FACTOR_AUTH_ERROR_DETAIL": "二步验证失败",
|
|
8
|
+
"[{{name}}] Registration Confirm Mail": "【{{name}}】注册确认邮件",
|
|
9
|
+
"Please click <a href=\"{{url}}\">{{url}}<a/> to confirm registration, the link is valid for 1 hour. If you are not registering, please ignore this email.": "请点击 <a href=\"{{url}}\">{{url}}</a> 确认注册,链接有效时间为 1 个小时。如果不是你在注册,请忽略这封邮件。",
|
|
10
|
+
"[{{name}}] Reset Password": "【{{name}}】重置密码",
|
|
11
|
+
"Please click <a href=\"{{url}}\">{{url}}</a> to login and change your password as soon as possible!": "请尽快点击链接 <a href=\"{{url}}\">{{url}}</a> 登录并修改你的密码!",
|
|
12
|
+
"Duplicate Content": "发送的内容之前已经发过",
|
|
13
|
+
"Comment too fast": "评论太快啦,请慢点!",
|
|
14
|
+
"Registeration confirm mail send failed, please {%- if isAdmin -%}check your mail configuration{%- else -%}check your email address and contact administrator{%- endif -%}.": "注册确认邮件发送失败,请{%- if isAdmin -%}检查一下网站的邮件相关配置{% else %}确认你的邮箱输入无误并联系管理员{%- endif -%}。"
|
|
15
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"import data format not support!": "文件格式不支持",
|
|
3
|
+
"USER_EXIST": "用戶已存在",
|
|
4
|
+
"USER_NOT_EXIST": "用戶不存在",
|
|
5
|
+
"USER_REGISTED": "用戶已註冊",
|
|
6
|
+
"TOKEN_EXPIRED": "密鑰已過期",
|
|
7
|
+
"TWO_FACTOR_AUTH_ERROR_DETAIL": "二步驗證失敗",
|
|
8
|
+
"[{{name}}] Registration Confirm Mail": "『{{name}}』註冊確認郵件",
|
|
9
|
+
"Please click <a href=\"{{url}}\">{{url}}<a/> to confirm registration, the link is valid for 1 hour. If you are not registering, please ignore this email.": "請點擊 <a href=\"{{url}}\">{{url}}</a> 確認註冊,鏈接有效時間為 1 個小時。如果不是你在註冊,請忽略這封郵件。",
|
|
10
|
+
"[{{name}}] Reset Password": "『{{name}}』重置密碼",
|
|
11
|
+
"Please click <a href=\"{{url}}\">{{url}}</a> to login and change your password as soon as possible!": "請盡快點擊鏈接 <a href=\"{{url}}\">{{url}}</a> 登錄並修改你的密碼!",
|
|
12
|
+
"Duplicate Content": "發送的內容之前已經發過",
|
|
13
|
+
"Comment too fast": "評論太快啦,請慢點!",
|
|
14
|
+
"Registeration confirm mail send failed, please {%- if isAdmin -%}check your mail configuration{%- else -%}check your email address and contact administrator{%- endif -%}.": "註冊確認郵件發送失敗,{%- if isAdmin -%}檢查一下網站的郵件相關配置{% else %}確認你的郵箱輸入無誤後聯繫管理員{%- endif -%}。"
|
|
15
|
+
}
|
|
@@ -14,8 +14,7 @@ module.exports = function () {
|
|
|
14
14
|
window.SITE_NAME = ${JSON.stringify(process.env.SITE_NAME)};
|
|
15
15
|
</script>
|
|
16
16
|
<script src="${
|
|
17
|
-
process.env.WALINE_ADMIN_MODULE_ASSET_URL ||
|
|
18
|
-
'https://cdn.jsdelivr.net/npm/@waline/admin'
|
|
17
|
+
process.env.WALINE_ADMIN_MODULE_ASSET_URL || '//unpkg.com/@waline/admin'
|
|
19
18
|
}"></script>
|
|
20
19
|
</body>
|
|
21
20
|
</html>`;
|
|
@@ -10,6 +10,7 @@ const app = cloudbase.init({
|
|
|
10
10
|
|
|
11
11
|
const db = app.database();
|
|
12
12
|
const _ = db.command;
|
|
13
|
+
const $ = db.command.aggregate;
|
|
13
14
|
const collections = {};
|
|
14
15
|
|
|
15
16
|
module.exports = class extends Base {
|
|
@@ -90,10 +91,10 @@ module.exports = class extends Base {
|
|
|
90
91
|
return filter;
|
|
91
92
|
}
|
|
92
93
|
|
|
93
|
-
where(instance, where) {
|
|
94
|
+
where(instance, where, method = 'where') {
|
|
94
95
|
const filter = this.parseWhere(where);
|
|
95
96
|
if (!where._complex) {
|
|
96
|
-
return instance
|
|
97
|
+
return instance[method](filter);
|
|
97
98
|
}
|
|
98
99
|
|
|
99
100
|
const filters = [];
|
|
@@ -106,7 +107,7 @@ module.exports = class extends Base {
|
|
|
106
107
|
...filter,
|
|
107
108
|
});
|
|
108
109
|
}
|
|
109
|
-
return instance
|
|
110
|
+
return instance[method](_[where._complex._logic](...filters));
|
|
110
111
|
}
|
|
111
112
|
|
|
112
113
|
async _select(where, { desc, limit, offset, field } = {}) {
|
|
@@ -147,13 +148,32 @@ module.exports = class extends Base {
|
|
|
147
148
|
return data;
|
|
148
149
|
}
|
|
149
150
|
|
|
150
|
-
async count(where = {}) {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
151
|
+
async count(where = {}, { group } = {}) {
|
|
152
|
+
let instance = await this.collection(this.tableName);
|
|
153
|
+
if (!group) {
|
|
154
|
+
instance = this.where(instance, where);
|
|
155
|
+
const { total } = await instance.count();
|
|
156
|
+
return total;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const _id = {};
|
|
160
|
+
group.forEach((f) => {
|
|
161
|
+
_id[f] = `$${f}`;
|
|
162
|
+
});
|
|
163
|
+
instance = instance.aggregate();
|
|
164
|
+
this.where(instance, where, 'match');
|
|
165
|
+
instance = instance.group({ _id, count: $.sum(1) });
|
|
166
|
+
const { data } = await instance.end();
|
|
167
|
+
|
|
168
|
+
return data.map(({ _id, count }) => ({ ..._id, count }));
|
|
154
169
|
}
|
|
155
170
|
|
|
156
171
|
async add(data) {
|
|
172
|
+
if (data.objectId) {
|
|
173
|
+
data._id = data.objectId;
|
|
174
|
+
delete data.objectId;
|
|
175
|
+
}
|
|
176
|
+
|
|
157
177
|
const instance = await this.collection(this.tableName);
|
|
158
178
|
const { id } = await instance.add(data);
|
|
159
179
|
return { ...data, objectId: id };
|
|
@@ -184,16 +184,47 @@ module.exports = class extends Base {
|
|
|
184
184
|
return data;
|
|
185
185
|
}
|
|
186
186
|
|
|
187
|
-
async count(where = {}) {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
187
|
+
async count(where = {}, { group } = {}) {
|
|
188
|
+
if (!group) {
|
|
189
|
+
const conditions = this.where(where);
|
|
190
|
+
if (think.isArray(conditions)) {
|
|
191
|
+
return Promise.all(
|
|
192
|
+
conditions.map((condition) => this.count(condition))
|
|
193
|
+
).then((counts) => counts.reduce((a, b) => a + b, 0));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const { count } = await this.instance.fetch(conditions);
|
|
197
|
+
return count;
|
|
193
198
|
}
|
|
194
199
|
|
|
195
|
-
const
|
|
196
|
-
|
|
200
|
+
const counts = [];
|
|
201
|
+
for (let i = 0; i < group.length; i++) {
|
|
202
|
+
const groupName = group[i];
|
|
203
|
+
if (!where._complex || !Array.isArray(where._complex[groupName])) {
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const groupFlatValue = {};
|
|
208
|
+
group.slice(0, i).forEach((group) => {
|
|
209
|
+
groupFlatValue[group] = null;
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
for (let j = 0; j < where._complex[groupName][1].length; j++) {
|
|
213
|
+
const groupWhere = {
|
|
214
|
+
...where,
|
|
215
|
+
...groupFlatValue,
|
|
216
|
+
_complex: undefined,
|
|
217
|
+
[groupName]: where._complex[groupName][1][j],
|
|
218
|
+
};
|
|
219
|
+
const num = await this.count(groupWhere);
|
|
220
|
+
counts.push({
|
|
221
|
+
...groupFlatValue,
|
|
222
|
+
[groupName]: where._complex[groupName][1][j],
|
|
223
|
+
count: num,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return counts;
|
|
197
228
|
}
|
|
198
229
|
|
|
199
230
|
async add(data) {
|
|
@@ -287,10 +287,25 @@ module.exports = class extends Base {
|
|
|
287
287
|
}
|
|
288
288
|
|
|
289
289
|
// eslint-disable-next-line no-unused-vars
|
|
290
|
-
async count(where = {},
|
|
290
|
+
async count(where = {}, { group } = {}) {
|
|
291
291
|
const instance = await this.collection(this.tableName);
|
|
292
292
|
const data = this.where(instance, where);
|
|
293
|
-
|
|
293
|
+
if (!group) {
|
|
294
|
+
return data.length;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const counts = {};
|
|
298
|
+
for (let i = 0; i < data.length; i++) {
|
|
299
|
+
const key = group.map((field) => data[field]).join();
|
|
300
|
+
if (!counts[key]) {
|
|
301
|
+
counts[key] = { count: 0 };
|
|
302
|
+
group.forEach((field) => {
|
|
303
|
+
counts[key][field] = data[field];
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
counts[key].count += 1;
|
|
307
|
+
}
|
|
308
|
+
return Object.keys(counts);
|
|
294
309
|
}
|
|
295
310
|
|
|
296
311
|
async add(
|
|
@@ -128,12 +128,47 @@ module.exports = class extends Base {
|
|
|
128
128
|
|
|
129
129
|
async count(where = {}, options = {}) {
|
|
130
130
|
const instance = this.where(this.tableName, where);
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
131
|
+
if (!options.group) {
|
|
132
|
+
return instance.count(options).catch((e) => {
|
|
133
|
+
if (e.code === 101) {
|
|
134
|
+
return 0;
|
|
135
|
+
}
|
|
136
|
+
throw e;
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// todo: query optimize
|
|
141
|
+
const counts = [];
|
|
142
|
+
for (let i = 0; i < options.group.length; i++) {
|
|
143
|
+
const groupName = options.group[i];
|
|
144
|
+
if (!where._complex || !Array.isArray(where._complex[groupName])) {
|
|
145
|
+
continue;
|
|
134
146
|
}
|
|
135
|
-
|
|
136
|
-
|
|
147
|
+
|
|
148
|
+
const groupFlatValue = {};
|
|
149
|
+
options.group.slice(0, i).forEach((group) => {
|
|
150
|
+
groupFlatValue[group] = null;
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
for (let j = 0; j < where._complex[groupName][1].length; j++) {
|
|
154
|
+
const groupWhere = {
|
|
155
|
+
...where,
|
|
156
|
+
...groupFlatValue,
|
|
157
|
+
_complex: undefined,
|
|
158
|
+
[groupName]: where._complex[groupName][1][j],
|
|
159
|
+
};
|
|
160
|
+
const num = await this.count(groupWhere, {
|
|
161
|
+
...options,
|
|
162
|
+
group: undefined,
|
|
163
|
+
});
|
|
164
|
+
counts.push({
|
|
165
|
+
...groupFlatValue,
|
|
166
|
+
[groupName]: where._complex[groupName][1][j],
|
|
167
|
+
count: num,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return counts;
|
|
137
172
|
}
|
|
138
173
|
|
|
139
174
|
async add(
|
|
@@ -114,13 +114,25 @@ module.exports = class extends Base {
|
|
|
114
114
|
}));
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
-
async count(where = {}) {
|
|
117
|
+
async count(where = {}, { group } = {}) {
|
|
118
118
|
const instance = this.mongo(this.tableName);
|
|
119
119
|
this.where(instance, where);
|
|
120
|
-
|
|
120
|
+
if (group) {
|
|
121
|
+
instance.group(group);
|
|
122
|
+
}
|
|
123
|
+
const data = await instance.count({ raw: group });
|
|
124
|
+
if (!Array.isArray(data)) {
|
|
125
|
+
return data;
|
|
126
|
+
}
|
|
127
|
+
return data.map(({ _id, total: count }) => ({ ..._id, count }));
|
|
121
128
|
}
|
|
122
129
|
|
|
123
130
|
async add(data) {
|
|
131
|
+
if (data.objectId) {
|
|
132
|
+
data._id = data.objectId;
|
|
133
|
+
delete data.objectId;
|
|
134
|
+
}
|
|
135
|
+
|
|
124
136
|
const instance = this.mongo(this.tableName);
|
|
125
137
|
const id = await instance.add(data);
|
|
126
138
|
return { ...data, objectId: id.toString() };
|
|
@@ -50,13 +50,24 @@ module.exports = class extends Base {
|
|
|
50
50
|
return data.map(({ id, ...cmt }) => ({ ...cmt, objectId: id }));
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
async count(where = {}) {
|
|
53
|
+
async count(where = {}, { group } = {}) {
|
|
54
54
|
const instance = this.model(this.tableName);
|
|
55
55
|
instance.where(this.parseWhere(where));
|
|
56
|
-
|
|
56
|
+
if (!group) {
|
|
57
|
+
return instance.count();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
instance.field([...group, 'COUNT(*) as count']);
|
|
61
|
+
instance.group(group);
|
|
62
|
+
return instance.select();
|
|
57
63
|
}
|
|
58
64
|
|
|
59
65
|
async add(data) {
|
|
66
|
+
if (data.objectId) {
|
|
67
|
+
data.id = data.objectId;
|
|
68
|
+
delete data.objectId;
|
|
69
|
+
}
|
|
70
|
+
|
|
60
71
|
const instance = this.model(this.tableName);
|
|
61
72
|
const id = await instance.add(data);
|
|
62
73
|
return { ...data, objectId: id };
|
|
@@ -26,7 +26,13 @@ module.exports = class extends MySQL {
|
|
|
26
26
|
async count(...args) {
|
|
27
27
|
let result = await super.count(...args);
|
|
28
28
|
try {
|
|
29
|
-
|
|
29
|
+
if (Array.isArray(result)) {
|
|
30
|
+
result.forEach((r) => {
|
|
31
|
+
r.count = parseInt(r.count);
|
|
32
|
+
});
|
|
33
|
+
} else {
|
|
34
|
+
result = parseInt(result);
|
|
35
|
+
}
|
|
30
36
|
} catch (e) {
|
|
31
37
|
console.log(e);
|
|
32
38
|
}
|