@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 CHANGED
@@ -1,8 +1,17 @@
1
1
  {
2
2
  "name": "@waline/vercel",
3
- "version": "1.13.5",
3
+ "version": "1.15.0",
4
4
  "description": "vercel server for waline comment system",
5
- "repository": "https://github.com/walinejs/waline",
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.2",
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"
@@ -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,
@@ -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, path, controller } = this;
16
- return `${protocol}://${host}${path.slice(0, -controller.length)}`;
16
+ const { protocol, host } = this;
17
+ return `${protocol}://${host}`;
17
18
  },
18
- },
19
- controller: {
20
- fail(...args) {
21
- if (this.ctx.status === 200) {
22
- this.ctx.status = 500;
19
+ async webhook(type, data) {
20
+ const { WEBHOOK } = process.env;
21
+ if (!WEBHOOK) {
22
+ return;
23
23
  }
24
- this.ctx.fail(...args);
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 comments = await this.modelInstance.select(where, {
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
- const rootCount = comments.filter(({ rid }) => !rid).length;
257
- const pageOffset = Math.max((page - 1) * pageSize, 0);
258
- const rootComments = [
259
- ...comments.filter(({ rid, sticky }) => !rid && sticky),
260
- ...comments.filter(({ rid, sticky }) => !rid && !sticky),
261
- ].slice(pageOffset, pageOffset + pageSize);
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: comments.length,
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 parrentComment;
419
-
507
+ let parentComment;
420
508
  if (pid) {
421
- parrentComment = await this.modelInstance.select({ objectId: pid });
422
- parrentComment = parrentComment[0];
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 }, parrentComment);
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, parrentComment);
525
+ await this.hook('postSave', resp, parentComment);
433
526
 
434
527
  think.logger.debug(`Comment post hooks postSave done!`);
435
528
 
@@ -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 model = this.service(
23
- `storage/${this.config('storage')}`,
24
- tableName
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 model = this.model(tableName);
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({});
@@ -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> <script src="https://cdn.jsdelivr.net/npm/@waline/client/dist/Waline.min.js"></script>
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 = new Waline({
25
+ const waline = Waline.init({
24
26
  el: '#waline',
25
27
  path: params.get('path') || '/',
26
28
  lang: params.get('lng'),
@@ -47,7 +47,7 @@ module.exports = class extends BaseRest {
47
47
  });
48
48
 
49
49
  if (!verified) {
50
- return this.fail('TWO_FACTOR_AUTH_ERROR_DETAIL');
50
+ return this.fail(this.locale('TWO_FACTOR_AUTH_ERROR_DETAIL'));
51
51
  }
52
52
 
53
53
  const userModel = this.service(
@@ -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: `【${SITE_NAME || 'Waline'}】Reset Password`,
40
- html: `Please click <a href="${profileUrl}">${profileUrl}</a> to login and change your password as soon as possible!`,
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();
@@ -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
- const notify = this.service('notify');
60
- const apiUrl =
61
- this.ctx.serverURL +
62
- '/verification?' +
63
- qs.stringify({ token, email: data.email });
64
-
65
- await notify.transporter.sendMail({
66
- from:
67
- SENDER_EMAIL && SENDER_NAME
68
- ? `"${SENDER_NAME}" <${SENDER_EMAIL}>`
69
- : SMTP_USER,
70
- to: data.email,
71
- subject: `【${SITE_NAME || 'Waline'}】注册确认邮件`,
72
- html: `请点击 ${apiUrl} 确认注册,链接有效时间为 1 个小时。如果不是你在注册,请忽略这封邮件。`,
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
  };
@@ -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
  };
@@ -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,12 @@
1
+ const en = require('./en.json');
2
+ const zhCN = require('./zh-CN.json');
3
+ const zhTW = require('./zh-TW.json');
4
+
5
+ module.exports = {
6
+ 'zh-cn': zhCN,
7
+ 'zh-tw': zhTW,
8
+ en: en,
9
+ 'en-us': en,
10
+ jp: en,
11
+ 'jp-jp': en,
12
+ };
@@ -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.where(filter);
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.where(_[where._complex._logic](...filters));
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
- const instance = await this.collection(this.tableName);
152
- const { total } = await this.where(instance, where).count();
153
- return total;
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
- const conditions = this.where(where);
189
- if (think.isArray(conditions)) {
190
- return Promise.all(
191
- conditions.map((condition) => this.count(condition))
192
- ).then((counts) => counts.reduce((a, b) => a + b, 0));
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 { count } = await this.instance.fetch(conditions);
196
- return count;
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 = {}, options = {}) {
290
+ async count(where = {}, { group } = {}) {
291
291
  const instance = await this.collection(this.tableName);
292
292
  const data = this.where(instance, where);
293
- return data.length;
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
- return instance.count(options).catch((e) => {
132
- if (e.code === 101) {
133
- return 0;
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
- throw e;
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
- return instance.count();
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
- return instance.count();
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
- result = parseInt(result);
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
  }