Просмотр исходного кода

Use streams to get stats and major refactor

poeti8 7 лет назад
Родитель
Сommit
dd4f5c5259
2 измененных файлов с 118 добавлено и 122 удалено
  1. 7 0
      server/controllers/urlController.js
  2. 111 122
      server/db/url.js

+ 7 - 0
server/controllers/urlController.js

@@ -285,8 +285,15 @@ exports.getStats = async ({ query: { id, domain }, user }, res) => {
   const redisKey = id + (customDomain || '') + user.email;
   const cached = await redis.get(redisKey);
   if (cached) return res.status(200).json(JSON.parse(cached));
+  const urls = await findUrl({ id, domain: customDomain });
+  if (!urls && !urls.length) return res.status(400).json({ error: "Couldn't find the short URL." });
+  const [url] = urls;
   const stats = await getStats({ id, domain: customDomain, user });
   if (!stats) return res.status(400).json({ error: 'Could not get the short URL stats.' });
+  stats.shortUrl = `http${!domain ? 's' : ''}://${domain ? url.domain : config.DEFAULT_DOMAIN}/${
+    url.id
+  }`;
+  stats.target = url.target;
   const cacheTime = getStatsCacheTime(stats.total);
   redis.set(redisKey, JSON.stringify(stats), 'EX', cacheTime);
   return res.status(200).json(stats);

+ 111 - 122
server/db/url.js

@@ -1,22 +1,14 @@
 const bcrypt = require('bcryptjs');
 const _ = require('lodash/');
-const {
-  isAfter,
-  isSameHour,
-  isSameDay,
-  isSameMonth,
-  subDays,
-  subHours,
-  subMonths,
-} = require('date-fns');
+const { isAfter, subDays } = require('date-fns');
 const driver = require('./neo4j');
 const config = require('../config');
-const { generateShortUrl } = require('../utils');
-
-const getUTCDate = (dateString = Date.now()) => {
-  const date = new Date(dateString);
-  return new Date(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), date.getUTCHours());
-};
+const {
+  generateShortUrl,
+  statsObjectToArray,
+  getDifferenceFunction,
+  getUTCDate,
+} = require('../utils');
 
 const queryNewUrl = 'CREATE (l:URL { id: $id, target: $target, createdAt: $createdAt }) RETURN l';
 
@@ -289,7 +281,10 @@ exports.deleteUrl = ({ id, domain, user }) =>
       .catch(err => session.close() || reject(err));
   });
 
-/* Collecting stats */
+/* 
+** Collecting stats 
+*/
+
 const initialStats = {
   browser: {
     IE: 0,
@@ -314,118 +309,112 @@ const initialStats = {
   dates: [],
 };
 
-const filterByDate = days => record => isAfter(record.date, subDays(getUTCDate(), days));
-
-/* eslint-disable no-param-reassign */
-const calcStats = (obj, record) => {
-  obj.browser[record.browser] += 1;
-  obj.os[record.os] += 1;
-  obj.country[record.country] = obj.country[record.country] + 1 || 1;
-  obj.referrer[record.referrer] = obj.referrer[record.referrer] + 1 || 1;
-  obj.dates = [...obj.dates, record.date];
-  return obj;
-};
-/* eslint-enable no-param-reassign */
-
-const objectToArray = item => {
-  const objToArr = key =>
-    Array.from(Object.keys(item[key]))
-      .map(name => ({
-        name,
-        value: item[key][name],
-      }))
-      .sort((a, b) => b.value - a.value);
-
-  return {
-    browser: objToArr('browser'),
-    os: objToArr('os'),
-    country: objToArr('country'),
-    referrer: objToArr('referrer'),
-  };
-};
-
-const calcViewPerDate = (views, period, sub, compare, lastDate = getUTCDate(), arr = []) => {
-  if (arr.length === period) return arr;
-
-  const matchedStats = views.filter(date => compare(date, lastDate));
-  const viewsPerDate = [matchedStats.length, ...arr];
-
-  return calcViewPerDate(views, period, sub, compare, sub(lastDate, 1), viewsPerDate);
-};
-
-const calcViews = {
-  0: views => calcViewPerDate(views, 24, subHours, isSameHour),
-  1: views => calcViewPerDate(views, 7, subDays, isSameDay),
-  2: views => calcViewPerDate(views, 30, subDays, isSameDay),
-  3: views => calcViewPerDate(views, 18, subMonths, isSameMonth),
-};
-
 exports.getStats = ({ id, domain, user }) =>
   new Promise((resolve, reject) => {
     const session = driver.session();
 
-    session
-      .readTransaction(tx =>
-        tx.run(
-          'MATCH (l:URL { id: $id })<-[:CREATED]-(u:USER { email: $email }) ' +
-            `${domain ? 'MATCH (l)-[:USES]->(domain { name: $domain })' : ''}` +
-            'MATCH (v)-[:VISITED]->(l) ' +
-            'MATCH (v)-[:BROWSED_BY]->(b) ' +
-            'MATCH (v)-[:LOCATED_IN]->(c) ' +
-            'MATCH (v)-[:OS]->(o) ' +
-            'MATCH (v)-[:REFERRED_BY]->(r) ' +
-            'MATCH (v)-[:VISITED_IN]->(d) ' +
-            'RETURN l, b.browser AS browser, c.country AS country,' +
-            `${domain ? 'domain.name AS domain, ' : ''}` +
-            'o.os AS os, r.referrer AS referrer, d.date AS date ' +
-            'ORDER BY d.date DESC',
-          {
-            email: user.email,
-            domain,
-            id,
-          }
-        )
-      )
-      .then(({ records }) => {
-        session.close();
-
-        if (!records.length) resolve([]);
-
-        const allStats = records.map(record => ({
-          browser: record.get('browser'),
-          os: record.get('os'),
-          country: record.get('country'),
-          referrer: record.get('referrer'),
-          date: record.get('date'),
-        }));
-
-        const statsPeriods = [1, 7, 30, 550];
+    const stats = {
+      lastDay: {
+        stats: _.cloneDeep(initialStats),
+        views: new Array(24).fill(0),
+      },
+      lastWeek: {
+        stats: _.cloneDeep(initialStats),
+        views: new Array(7).fill(0),
+      },
+      lastMonth: {
+        stats: _.cloneDeep(initialStats),
+        views: new Array(30).fill(0),
+      },
+      allTime: {
+        stats: _.cloneDeep(initialStats),
+        views: new Array(18).fill(0),
+      },
+    };
+
+    let total = 0;
+
+    const statsPeriods = [[1, 'lastDay'], [7, 'lastWeek'], [30, 'lastMonth']];
 
-        const stats = statsPeriods
-          .map(statsPeriod => allStats.filter(filterByDate(statsPeriod)))
-          .map(statsPeriod => statsPeriod.reduce(calcStats, _.cloneDeep(initialStats)))
-          .map((statsPeriod, index) => ({
-            stats: objectToArray(statsPeriod),
-            views: calcViews[index](statsPeriod.dates),
-          }));
-
-        const response = {
-          total: records.length,
+    session
+      .run(
+        'MATCH (l:URL { id: $id })<-[:CREATED]-(u:USER { email: $email }) ' +
+          `${domain ? 'MATCH (l)-[:USES]->(domain { name: $domain })' : ''}` +
+          'MATCH (v)-[:VISITED]->(l) ' +
+          'MATCH (v)-[:BROWSED_BY]->(b) ' +
+          'MATCH (v)-[:LOCATED_IN]->(c) ' +
+          'MATCH (v)-[:OS]->(o) ' +
+          'MATCH (v)-[:REFERRED_BY]->(r) ' +
+          'MATCH (v)-[:VISITED_IN]->(d) ' +
+          'RETURN l, b.browser AS browser, c.country AS country,' +
+          'o.os AS os, r.referrer AS referrer, d.date AS date ' +
+          'ORDER BY d.date DESC',
+        {
+          email: user.email,
+          domain,
           id,
-          shortUrl: `http${!domain ? 's' : ''}://${
-            domain ? records[0].get('domain') : config.DEFAULT_DOMAIN
-          }/${id}`,
-          target: records[0].get('l').properties.target,
-          updatedAt: new Date().toISOString(),
-          lastDay: stats[0],
-          lastWeek: stats[1],
-          lastMonth: stats[2],
-          allTime: stats[3],
-        };
-
-        return resolve(response);
-      })
-      .catch(err => session.close() || reject(err));
+        }
+      )
+      .subscribe({
+        onNext(record) {
+          total += 1;
+          const browser = record.get('browser');
+          const os = record.get('os');
+          const country = record.get('country');
+          const referrer = record.get('referrer');
+          const date = record.get('date');
+
+          statsPeriods.forEach(([days, type]) => {
+            const isIncluded = isAfter(date, subDays(getUTCDate(), days));
+            if (isIncluded) {
+              const period = stats[type].stats;
+              const diffFunction = getDifferenceFunction(type);
+              const now = new Date();
+              const diff = diffFunction(now, date);
+              const index = stats[type].views.length - diff - 1;
+              const view = stats[type].views[index];
+              period.browser[browser] += 1;
+              period.os[os] += 1;
+              period.country[country] = period.country[country] + 1 || 1;
+              period.referrer[referrer] = period.referrer[referrer] + 1 || 1;
+              stats[type].views[index] = view + 1 || 1;
+            }
+          });
+
+          const allTime = stats.allTime.stats;
+          const diffFunction = getDifferenceFunction('allTime');
+          const now = new Date();
+          const diff = diffFunction(now, date);
+          const index = stats.allTime.views.length - diff - 1;
+          const view = stats.allTime.views[index];
+          allTime.browser[browser] += 1;
+          allTime.os[os] += 1;
+          allTime.country[country] = allTime.country[country] + 1 || 1;
+          allTime.referrer[referrer] = allTime.referrer[referrer] + 1 || 1;
+          allTime.dates = [...allTime.dates, date];
+          stats.allTime.views[index] = view + 1 || 1;
+        },
+        onCompleted() {
+          stats.lastDay.stats = statsObjectToArray(stats.lastDay.stats);
+          stats.lastWeek.stats = statsObjectToArray(stats.lastWeek.stats);
+          stats.lastMonth.stats = statsObjectToArray(stats.lastMonth.stats);
+          stats.allTime.stats = statsObjectToArray(stats.allTime.stats);
+          const response = {
+            total,
+            id: 'sv-v104',
+            updatedAt: new Date().toISOString(),
+            lastDay: stats.lastDay,
+            lastWeek: stats.lastWeek,
+            lastMonth: stats.lastMonth,
+            allTime: stats.allTime,
+          };
+          return resolve(response);
+        },
+        onError(error) {
+          session.close();
+          return reject(error);
+        },
+      });
   });
 
 exports.urlCountFromDate = ({ date, email }) =>