| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397 |
- const generate = require('nanoid/generate');
- const bcrypt = require('bcryptjs');
- const _ = require('lodash/');
- const {
- isAfter,
- isSameHour,
- isSameDay,
- isSameMonth,
- subDays,
- subHours,
- subMonths,
- } = require('date-fns');
- const driver = require('./neo4j');
- const config = require('../config');
- const getUTCDate = (dateString = Date.now()) => {
- const date = new Date(dateString);
- return new Date(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), date.getUTCHours());
- };
- const generateId = () =>
- generate('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890', 6);
- const queryNewUrl = 'CREATE (l:URL { id: $id, target: $target, createdAt: $createdAt }) RETURN l';
- const queryNewUserUrl = (domain, password) =>
- 'MATCH (u:USER { email: $email })' +
- 'CREATE (l:URL { id: $id, target: $target, createdAt: $createdAt' +
- `${password ? ', password: $password' : ''} })` +
- 'CREATE (u)-[:CREATED]->(l)' +
- `${domain ? 'MERGE (l)-[:USES]->(:DOMAIN { name: $domain })' : ''}` +
- 'RETURN l';
- exports.createShortUrl = params =>
- new Promise(async (resolve, reject) => {
- const query = params.user ? queryNewUserUrl(params.user.domain, params.password) : queryNewUrl;
- const session = driver.session();
- const salt = params.password && (await bcrypt.genSalt(12));
- const hash = params.password && (await bcrypt.hash(params.password, salt));
- session
- .writeTransaction(tx =>
- tx.run(query, {
- createdAt: new Date().toJSON(),
- domain: params.user && params.user.domain,
- email: params.user && params.user.email,
- id: (params.user && params.customurl) || generateId(),
- password: hash || '',
- target: params.target,
- })
- )
- .then(({ records }) => {
- session.close();
- const data = records[0].get('l').properties;
- resolve({
- ...data,
- password: !!data.password,
- shortUrl: `http${!params.user.domain ? 's' : ''}://${params.user.domain ||
- config.DEFAULT_DOMAIN}/${data.id}`,
- });
- })
- .catch(reject);
- });
- exports.createVisit = params =>
- new Promise((resolve, reject) => {
- const session = driver.session();
- session
- .writeTransaction(tx =>
- tx.run(
- 'MATCH (l:URL { id: $id })' +
- `${params.domain ? 'MATCH (l)-[:USES]->({ name: $domain })' : ''}` +
- 'CREATE (v:VISIT)' +
- 'MERGE (b:BROWSER { browser: $browser })' +
- 'MERGE (c:COUNTRY { country: $country })' +
- 'MERGE (o:OS { os: $os })' +
- 'MERGE (r:REFERRER { referrer: $referrer })' +
- 'MERGE (d:DATE { date: $date })' +
- 'MERGE (v)-[:VISITED]->(l)' +
- 'MERGE (v)-[:BROWSED_BY]->(b)' +
- 'MERGE (v)-[:LOCATED_IN]->(c)' +
- 'MERGE (v)-[:OS]->(o)' +
- 'MERGE (v)-[:REFERRED_BY]->(r)' +
- 'MERGE (v)-[:VISITED_IN]->(d)' +
- 'RETURN l',
- {
- id: params.id,
- browser: params.browser,
- domain: params.domain,
- country: params.country,
- os: params.os,
- referrer: params.referrer,
- date: getUTCDate().toJSON(),
- }
- )
- )
- .then(({ records }) => {
- session.close();
- const url = records.length && records[0].get('l').properties;
- resolve(url);
- })
- .catch(reject);
- });
- exports.findUrl = ({ id, domain }) =>
- new Promise((resolve, reject) => {
- const session = driver.session();
- session
- .readTransaction(tx =>
- tx.run(
- 'MATCH (l:URL { id: $id })' +
- `${
- domain
- ? 'MATCH (l)-[:USES]->(d:DOMAIN { name: $domain })'
- : 'OPTIONAL MATCH (l)-[:USES]->(d)'
- }` +
- 'OPTIONAL MATCH (u)-[:CREATED]->(l)' +
- 'RETURN l, d.name AS domain, u.email AS user',
- {
- id,
- domain,
- }
- )
- )
- .then(({ records }) => {
- session.close();
- const url =
- records.length &&
- records.map(record => ({
- ...record.get('l').properties,
- domain: record.get('domain'),
- user: record.get('user'),
- }));
- resolve(url);
- })
- .catch(reject);
- });
- exports.getUrls = ({ user, options }) =>
- new Promise((resolve, reject) => {
- const session = driver.session();
- const { count = 5, page = 1, search = '' } = options;
- const searchQuery = search ? 'WHERE l.id =~ $search OR l.target =~ $search' : '';
- session
- .readTransaction(tx =>
- tx.run(
- `MATCH (u:USER { email: $email })-[:CREATED]->(l) ${searchQuery} ` +
- 'OPTIONAL MATCH (l)-[:USES]->(d)' +
- 'OPTIONAL MATCH (l)<-[:VISITED]-(v:VISIT)' +
- 'RETURN l, d.name AS domain, COUNT(v) AS count ORDER BY l.createdAt DESC',
- {
- email: user.email,
- search: `(?i).*${search}.*`,
- }
- )
- )
- .then(({ records }) => {
- session.close();
- const countAll = records.length;
- const first = (page - 1) * count;
- const last = page * count;
- const urls = records.slice(first, last).map(record => ({
- ...record.get('l').properties,
- password: !!record.get('l').properties.password,
- count: record.get('count').toNumber(),
- shortUrl: `http${!record.get('domain') ? 's' : ''}://${record.get('domain') ||
- config.DEFAULT_DOMAIN}/${record.get('l').properties.id}`,
- }));
- resolve({ list: urls, countAll });
- })
- .catch(reject);
- });
- exports.getCustomDomain = ({ customDomain }) =>
- new Promise((resolve, reject) => {
- const session = driver.session();
- session
- .readTransaction(tx =>
- tx.run('MATCH (d:DOMAIN { name: $customDomain })<-[:OWNS]-(u) RETURN u', {
- customDomain,
- })
- )
- .then(({ records }) => {
- session.close();
- const data = records.length && records[0].get('u').properties;
- resolve(data);
- })
- .catch(reject);
- });
- exports.setCustomDomain = ({ user, customDomain }) =>
- new Promise((resolve, reject) => {
- const session = driver.session();
- session
- .writeTransaction(tx =>
- tx.run(
- 'MATCH (u:USER { email: $email }) ' +
- 'OPTIONAL MATCH (u)-[r:OWNS]->() DELETE r ' +
- 'MERGE (d:DOMAIN { name: $customDomain }) ' +
- 'MERGE (u)-[:OWNS]->(d) RETURN u, d',
- {
- customDomain,
- email: user.email,
- }
- )
- )
- .then(({ records }) => {
- session.close();
- const data = records.length && records[0].get('d').properties;
- resolve(data);
- })
- .catch(reject);
- });
- exports.deleteCustomDomain = ({ user }) =>
- new Promise((resolve, reject) => {
- const session = driver.session();
- session
- .writeTransaction(tx =>
- tx.run('MATCH (u:USER { email: $email }) MATCH (u)-[r:OWNS]->() DELETE r RETURN u', {
- email: user.email,
- })
- )
- .then(({ records }) => {
- session.close();
- const data = records.length && records[0].get('u').properties;
- resolve(data);
- })
- .catch(reject);
- });
- exports.deleteUrl = ({ id, domain, user }) =>
- new Promise((resolve, reject) => {
- const session = driver.session();
- session
- .writeTransaction(tx =>
- tx.run(
- 'MATCH (u:USER { email: $email }) ' +
- 'MATCH (u)-[:CREATED]->(l { id: $id }) ' +
- `${
- domain
- ? 'MATCH (l)-[:USES]->(:DOMAIN { name: $domain })'
- : 'MATCH (l) WHERE NOT (l)-[:USES]->()'
- }` +
- 'OPTIONAL MATCH (l)-[:MATCHES]->(v) ' +
- 'DETACH DELETE l, v RETURN u',
- {
- email: user.email,
- domain,
- id,
- }
- )
- )
- .then(({ records }) => {
- session.close();
- const data = records.length && records[0].get('u').properties;
- resolve(data);
- })
- .catch(reject);
- });
- /* Collecting stats */
- const initialStats = {
- browser: {
- IE: 0,
- Firefox: 0,
- Chrome: 0,
- Opera: 0,
- Safari: 0,
- Edge: 0,
- Other: 0,
- },
- os: {
- Windows: 0,
- 'Mac Os X': 0,
- Linux: 0,
- 'Chrome OS': 0,
- Android: 0,
- iOS: 0,
- Other: 0,
- },
- country: {},
- referrer: {},
- 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 = 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,
- id,
- shortUrl: `http${!domain ? 's' : ''}://${
- domain ? records[0].get('domain') : config.DEFAULT_DOMAIN
- }/${id}`,
- target: records[0].get('l').properties.target,
- lastDay: stats[0],
- lastWeek: stats[1],
- lastMonth: stats[2],
- allTime: stats[3],
- };
- return resolve(response);
- })
- .catch(reject);
- });
|