2020-08-12 05:05:40 +02:00
|
|
|
import moment from 'moment-timezone';
|
2020-09-25 08:02:11 +02:00
|
|
|
import prisma from 'lib/db';
|
2020-08-18 09:51:32 +02:00
|
|
|
import { subMinutes } from 'date-fns';
|
2021-03-13 08:45:19 +01:00
|
|
|
import {
|
|
|
|
MYSQL,
|
|
|
|
POSTGRESQL,
|
|
|
|
MYSQL_DATE_FORMATS,
|
|
|
|
POSTGRESQL_DATE_FORMATS,
|
|
|
|
URL_LENGTH,
|
|
|
|
} from 'lib/constants';
|
2020-08-30 08:17:32 +02:00
|
|
|
|
2020-08-12 05:05:40 +02:00
|
|
|
export function getDatabase() {
|
2020-09-04 20:06:32 +02:00
|
|
|
const type =
|
2020-08-30 09:02:03 +02:00
|
|
|
process.env.DATABASE_TYPE ||
|
2020-09-04 20:06:32 +02:00
|
|
|
(process.env.DATABASE_URL && process.env.DATABASE_URL.split(':')[0]);
|
|
|
|
|
|
|
|
if (type === 'postgres') {
|
|
|
|
return 'postgresql';
|
|
|
|
}
|
|
|
|
|
|
|
|
return type;
|
2020-08-12 05:05:40 +02:00
|
|
|
}
|
|
|
|
|
2020-09-25 08:02:11 +02:00
|
|
|
export async function runQuery(query) {
|
2020-10-07 17:31:44 +02:00
|
|
|
return query.catch(e => {
|
|
|
|
throw e;
|
|
|
|
});
|
2020-09-25 08:02:11 +02:00
|
|
|
}
|
|
|
|
|
2020-10-02 02:32:49 +02:00
|
|
|
export async function rawQuery(query, params = []) {
|
2020-09-25 08:02:11 +02:00
|
|
|
const db = getDatabase();
|
|
|
|
|
|
|
|
if (db !== POSTGRESQL && db !== MYSQL) {
|
|
|
|
return Promise.reject(new Error('Unknown database.'));
|
|
|
|
}
|
|
|
|
|
|
|
|
const sql = db === MYSQL ? query.replace(/\$[0-9]+/g, '?') : query;
|
|
|
|
|
|
|
|
return prisma.$queryRaw.apply(prisma, [sql, ...params]);
|
|
|
|
}
|
|
|
|
|
|
|
|
export function getDateQuery(field, unit, timezone) {
|
|
|
|
const db = getDatabase();
|
|
|
|
|
2020-08-30 08:17:32 +02:00
|
|
|
if (db === POSTGRESQL) {
|
|
|
|
if (timezone) {
|
|
|
|
return `to_char(date_trunc('${unit}', ${field} at time zone '${timezone}'), '${POSTGRESQL_DATE_FORMATS[unit]}')`;
|
|
|
|
}
|
|
|
|
return `to_char(date_trunc('${unit}', ${field}), '${POSTGRESQL_DATE_FORMATS[unit]}')`;
|
2020-08-28 08:45:37 +02:00
|
|
|
}
|
|
|
|
|
2020-08-30 08:17:32 +02:00
|
|
|
if (db === MYSQL) {
|
|
|
|
if (timezone) {
|
|
|
|
const tz = moment.tz(timezone).format('Z');
|
|
|
|
|
|
|
|
return `DATE_FORMAT(convert_tz(${field},'+00:00','${tz}'), '${MYSQL_DATE_FORMATS[unit]}')`;
|
|
|
|
}
|
|
|
|
|
|
|
|
return `DATE_FORMAT(${field}, '${MYSQL_DATE_FORMATS[unit]}')`;
|
|
|
|
}
|
2020-08-28 08:45:37 +02:00
|
|
|
}
|
|
|
|
|
2020-09-25 08:02:11 +02:00
|
|
|
export function getTimestampInterval(field) {
|
|
|
|
const db = getDatabase();
|
|
|
|
|
|
|
|
if (db === POSTGRESQL) {
|
|
|
|
return `floor(extract(epoch from max(${field}) - min(${field})))`;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (db === MYSQL) {
|
|
|
|
return `floor(unix_timestamp(max(${field})) - unix_timestamp(min(${field})))`;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-08-12 07:24:41 +02:00
|
|
|
export async function getWebsiteById(website_id) {
|
|
|
|
return runQuery(
|
2020-12-01 21:29:36 +01:00
|
|
|
prisma.website.findUnique({
|
2020-08-12 07:24:41 +02:00
|
|
|
where: {
|
|
|
|
website_id,
|
|
|
|
},
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
export async function getWebsiteByUuid(website_uuid) {
|
|
|
|
return runQuery(
|
2020-12-01 21:29:36 +01:00
|
|
|
prisma.website.findUnique({
|
2020-08-12 07:24:41 +02:00
|
|
|
where: {
|
|
|
|
website_uuid,
|
|
|
|
},
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-08-15 10:17:15 +02:00
|
|
|
export async function getWebsiteByShareId(share_id) {
|
|
|
|
return runQuery(
|
2020-12-01 21:29:36 +01:00
|
|
|
prisma.website.findUnique({
|
2020-08-15 10:17:15 +02:00
|
|
|
where: {
|
|
|
|
share_id,
|
|
|
|
},
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-08-12 07:24:41 +02:00
|
|
|
export async function getUserWebsites(user_id) {
|
|
|
|
return runQuery(
|
|
|
|
prisma.website.findMany({
|
|
|
|
where: {
|
|
|
|
user_id,
|
|
|
|
},
|
|
|
|
orderBy: {
|
|
|
|
name: 'asc',
|
|
|
|
},
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
export async function createWebsite(user_id, data) {
|
|
|
|
return runQuery(
|
|
|
|
prisma.website.create({
|
|
|
|
data: {
|
|
|
|
account: {
|
|
|
|
connect: {
|
|
|
|
user_id,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
...data,
|
|
|
|
},
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
export async function updateWebsite(website_id, data) {
|
|
|
|
return runQuery(
|
|
|
|
prisma.website.update({
|
|
|
|
where: {
|
|
|
|
website_id,
|
|
|
|
},
|
|
|
|
data,
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
export async function deleteWebsite(website_id) {
|
|
|
|
return runQuery(
|
|
|
|
/* Prisma bug, does not cascade on non-nullable foreign keys
|
|
|
|
prisma.website.delete({
|
|
|
|
where: {
|
|
|
|
website_id,
|
|
|
|
},
|
|
|
|
}),
|
|
|
|
*/
|
|
|
|
prisma.$queryRaw`delete from website where website_id=${website_id}`,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
export async function createSession(website_id, data) {
|
|
|
|
return runQuery(
|
|
|
|
prisma.session.create({
|
|
|
|
data: {
|
|
|
|
website: {
|
|
|
|
connect: {
|
|
|
|
website_id,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
...data,
|
|
|
|
},
|
|
|
|
select: {
|
|
|
|
session_id: true,
|
|
|
|
},
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
export async function getSessionByUuid(session_uuid) {
|
|
|
|
return runQuery(
|
2020-12-01 21:29:36 +01:00
|
|
|
prisma.session.findUnique({
|
2020-08-12 07:24:41 +02:00
|
|
|
where: {
|
|
|
|
session_uuid,
|
|
|
|
},
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
export async function savePageView(website_id, session_id, url, referrer) {
|
|
|
|
return runQuery(
|
|
|
|
prisma.pageview.create({
|
|
|
|
data: {
|
|
|
|
website: {
|
|
|
|
connect: {
|
|
|
|
website_id,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
session: {
|
|
|
|
connect: {
|
|
|
|
session_id,
|
|
|
|
},
|
|
|
|
},
|
2021-03-13 08:45:19 +01:00
|
|
|
url: url.substr(0, URL_LENGTH),
|
|
|
|
referrer: referrer.substr(0, URL_LENGTH),
|
2020-08-12 07:24:41 +02:00
|
|
|
},
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
export async function saveEvent(website_id, session_id, url, event_type, event_value) {
|
|
|
|
return runQuery(
|
|
|
|
prisma.event.create({
|
|
|
|
data: {
|
|
|
|
website: {
|
|
|
|
connect: {
|
|
|
|
website_id,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
session: {
|
|
|
|
connect: {
|
|
|
|
session_id,
|
|
|
|
},
|
|
|
|
},
|
2021-03-13 08:45:19 +01:00
|
|
|
url: url.substr(0, URL_LENGTH),
|
2020-08-12 07:24:41 +02:00
|
|
|
event_type,
|
|
|
|
event_value,
|
|
|
|
},
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
export async function getAccounts() {
|
|
|
|
return runQuery(prisma.account.findMany());
|
|
|
|
}
|
|
|
|
|
|
|
|
export async function getAccountById(user_id) {
|
|
|
|
return runQuery(
|
2020-12-01 21:29:36 +01:00
|
|
|
prisma.account.findUnique({
|
2020-08-12 07:24:41 +02:00
|
|
|
where: {
|
|
|
|
user_id,
|
|
|
|
},
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
export async function getAccountByUsername(username) {
|
|
|
|
return runQuery(
|
2020-12-01 21:29:36 +01:00
|
|
|
prisma.account.findUnique({
|
2020-08-12 07:24:41 +02:00
|
|
|
where: {
|
|
|
|
username,
|
|
|
|
},
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
export async function updateAccount(user_id, data) {
|
|
|
|
return runQuery(
|
|
|
|
prisma.account.update({
|
|
|
|
where: {
|
|
|
|
user_id,
|
|
|
|
},
|
|
|
|
data,
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
export async function deleteAccount(user_id) {
|
|
|
|
return runQuery(
|
|
|
|
/* Prisma bug, does not cascade on non-nullable foreign keys
|
|
|
|
prisma.account.delete({
|
|
|
|
where: {
|
|
|
|
user_id,
|
|
|
|
},
|
|
|
|
}),
|
|
|
|
*/
|
|
|
|
prisma.$queryRaw`delete from account where user_id=${user_id}`,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
export async function createAccount(data) {
|
|
|
|
return runQuery(
|
|
|
|
prisma.account.create({
|
|
|
|
data,
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-10-09 08:26:05 +02:00
|
|
|
export async function getSessions(websites, start_at) {
|
|
|
|
return runQuery(
|
|
|
|
prisma.session.findMany({
|
|
|
|
where: {
|
|
|
|
website: {
|
|
|
|
website_id: {
|
|
|
|
in: websites,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
created_at: {
|
|
|
|
gte: start_at,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
export async function getPageviews(websites, start_at) {
|
|
|
|
return runQuery(
|
|
|
|
prisma.pageview.findMany({
|
|
|
|
where: {
|
|
|
|
website: {
|
|
|
|
website_id: {
|
|
|
|
in: websites,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
created_at: {
|
|
|
|
gte: start_at,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
export async function getEvents(websites, start_at) {
|
|
|
|
return runQuery(
|
|
|
|
prisma.event.findMany({
|
|
|
|
where: {
|
|
|
|
website: {
|
|
|
|
website_id: {
|
|
|
|
in: websites,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
created_at: {
|
|
|
|
gte: start_at,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-10-09 00:02:48 +02:00
|
|
|
export function getWebsiteStats(website_id, start_at, end_at, filters = {}) {
|
2020-09-26 07:31:18 +02:00
|
|
|
const params = [website_id, start_at, end_at];
|
2020-09-29 05:23:42 +02:00
|
|
|
const { url } = filters;
|
2020-09-26 07:31:18 +02:00
|
|
|
let urlFilter = '';
|
|
|
|
|
|
|
|
if (url) {
|
|
|
|
urlFilter = `and url=$${params.length + 1}`;
|
|
|
|
params.push(decodeURIComponent(url));
|
|
|
|
}
|
|
|
|
|
2020-09-25 08:02:11 +02:00
|
|
|
return rawQuery(
|
|
|
|
`
|
2020-08-12 05:05:40 +02:00
|
|
|
select sum(t.c) as "pageviews",
|
|
|
|
count(distinct t.session_id) as "uniques",
|
2020-08-24 19:52:47 +02:00
|
|
|
sum(case when t.c = 1 then 1 else 0 end) as "bounces",
|
2020-08-12 05:05:40 +02:00
|
|
|
sum(t.time) as "totaltime"
|
|
|
|
from (
|
|
|
|
select session_id,
|
2020-09-25 08:02:11 +02:00
|
|
|
${getDateQuery('created_at', 'hour')},
|
2020-08-12 05:05:40 +02:00
|
|
|
count(*) c,
|
2020-09-25 08:02:11 +02:00
|
|
|
${getTimestampInterval('created_at')} as "time"
|
2020-08-12 05:05:40 +02:00
|
|
|
from pageview
|
|
|
|
where website_id=$1
|
|
|
|
and created_at between $2 and $3
|
2020-09-26 07:31:18 +02:00
|
|
|
${urlFilter}
|
2020-08-12 05:05:40 +02:00
|
|
|
group by 1, 2
|
|
|
|
) t
|
2020-09-25 08:02:11 +02:00
|
|
|
`,
|
2020-09-26 07:31:18 +02:00
|
|
|
params,
|
2020-09-25 08:02:11 +02:00
|
|
|
);
|
2020-08-12 05:05:40 +02:00
|
|
|
}
|
|
|
|
|
2020-10-09 00:02:48 +02:00
|
|
|
export function getPageviewStats(
|
2020-08-12 07:24:41 +02:00
|
|
|
website_id,
|
|
|
|
start_at,
|
|
|
|
end_at,
|
|
|
|
timezone = 'utc',
|
|
|
|
unit = 'day',
|
|
|
|
count = '*',
|
2020-09-26 07:31:18 +02:00
|
|
|
url,
|
2020-08-12 07:24:41 +02:00
|
|
|
) {
|
2020-09-26 07:31:18 +02:00
|
|
|
const params = [website_id, start_at, end_at];
|
|
|
|
let urlFilter = '';
|
|
|
|
|
|
|
|
if (url) {
|
|
|
|
urlFilter = `and url=$${params.length + 1}`;
|
|
|
|
params.push(decodeURIComponent(url));
|
|
|
|
}
|
|
|
|
|
2020-09-25 08:02:11 +02:00
|
|
|
return rawQuery(
|
|
|
|
`
|
|
|
|
select ${getDateQuery('created_at', unit, timezone)} t,
|
|
|
|
count(${count}) y
|
|
|
|
from pageview
|
|
|
|
where website_id=$1
|
|
|
|
and created_at between $2 and $3
|
2020-09-26 07:31:18 +02:00
|
|
|
${urlFilter}
|
2020-09-25 08:02:11 +02:00
|
|
|
group by 1
|
|
|
|
order by 1
|
|
|
|
`,
|
2020-09-26 07:31:18 +02:00
|
|
|
params,
|
2020-09-25 08:02:11 +02:00
|
|
|
);
|
2020-08-12 05:05:40 +02:00
|
|
|
}
|
|
|
|
|
2020-09-29 05:23:42 +02:00
|
|
|
export function getSessionMetrics(website_id, start_at, end_at, field, filters = {}) {
|
2020-09-26 07:31:18 +02:00
|
|
|
const params = [website_id, start_at, end_at];
|
2020-09-29 05:23:42 +02:00
|
|
|
const { url } = filters;
|
|
|
|
|
2020-09-26 07:31:18 +02:00
|
|
|
let urlFilter = '';
|
|
|
|
|
|
|
|
if (url) {
|
|
|
|
urlFilter = `and url=$${params.length + 1}`;
|
|
|
|
params.push(decodeURIComponent(url));
|
|
|
|
}
|
|
|
|
|
2020-09-25 09:15:58 +02:00
|
|
|
return rawQuery(
|
|
|
|
`
|
|
|
|
select ${field} x, count(*) y
|
|
|
|
from session
|
|
|
|
where session_id in (
|
|
|
|
select session_id
|
|
|
|
from pageview
|
|
|
|
where website_id=$1
|
|
|
|
and created_at between $2 and $3
|
2020-09-26 07:31:18 +02:00
|
|
|
${urlFilter}
|
2020-09-25 09:15:58 +02:00
|
|
|
)
|
|
|
|
group by 1
|
|
|
|
order by 2 desc
|
|
|
|
`,
|
2020-09-26 07:31:18 +02:00
|
|
|
params,
|
2020-09-25 09:15:58 +02:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-09-29 05:23:42 +02:00
|
|
|
export function getPageviewMetrics(website_id, start_at, end_at, field, table, filters = {}) {
|
2020-09-26 07:31:18 +02:00
|
|
|
const params = [website_id, start_at, end_at];
|
2020-09-29 05:23:42 +02:00
|
|
|
const { domain, url } = filters;
|
|
|
|
|
2020-09-26 07:31:18 +02:00
|
|
|
let domainFilter = '';
|
|
|
|
let urlFilter = '';
|
|
|
|
|
|
|
|
if (domain) {
|
2020-09-29 05:23:42 +02:00
|
|
|
domainFilter = `and referrer not like $${params.length + 1} and referrer not like '/%'`;
|
2021-02-27 04:50:44 +01:00
|
|
|
params.push(`%://${domain}/%`);
|
2020-09-26 07:31:18 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if (url) {
|
|
|
|
urlFilter = `and url=$${params.length + 1}`;
|
|
|
|
params.push(decodeURIComponent(url));
|
|
|
|
}
|
2020-08-29 06:34:20 +02:00
|
|
|
|
2020-09-25 08:02:11 +02:00
|
|
|
return rawQuery(
|
|
|
|
`
|
2020-09-25 09:15:58 +02:00
|
|
|
select ${field} x, count(*) y
|
2020-09-25 08:02:11 +02:00
|
|
|
from ${table}
|
|
|
|
where website_id=$1
|
|
|
|
and created_at between $2 and $3
|
2020-09-26 07:31:18 +02:00
|
|
|
${domainFilter}
|
|
|
|
${urlFilter}
|
2020-09-25 08:02:11 +02:00
|
|
|
group by 1
|
|
|
|
order by 2 desc
|
|
|
|
`,
|
2020-09-26 07:31:18 +02:00
|
|
|
params,
|
2020-09-25 08:02:11 +02:00
|
|
|
);
|
2020-08-12 05:05:40 +02:00
|
|
|
}
|
2020-08-18 09:51:32 +02:00
|
|
|
|
|
|
|
export function getActiveVisitors(website_id) {
|
2020-08-19 03:33:59 +02:00
|
|
|
const date = subMinutes(new Date(), 5);
|
2020-09-26 07:31:18 +02:00
|
|
|
const params = [website_id, date];
|
2020-08-19 03:33:59 +02:00
|
|
|
|
2020-09-25 08:02:11 +02:00
|
|
|
return rawQuery(
|
|
|
|
`
|
2020-08-18 09:51:32 +02:00
|
|
|
select count(distinct session_id) x
|
|
|
|
from pageview
|
|
|
|
where website_id=$1
|
|
|
|
and created_at >= $2
|
|
|
|
`,
|
2020-09-26 07:31:18 +02:00
|
|
|
params,
|
2020-09-25 08:02:11 +02:00
|
|
|
);
|
2020-08-18 09:51:32 +02:00
|
|
|
}
|
2020-08-25 08:49:14 +02:00
|
|
|
|
2020-10-09 08:26:05 +02:00
|
|
|
export function getEventMetrics(
|
2020-09-29 05:23:42 +02:00
|
|
|
website_id,
|
|
|
|
start_at,
|
|
|
|
end_at,
|
|
|
|
timezone = 'utc',
|
|
|
|
unit = 'day',
|
|
|
|
filters = {},
|
|
|
|
) {
|
2020-09-26 07:31:18 +02:00
|
|
|
const params = [website_id, start_at, end_at];
|
2020-09-29 05:23:42 +02:00
|
|
|
const { url } = filters;
|
|
|
|
|
2020-09-26 07:31:18 +02:00
|
|
|
let urlFilter = '';
|
|
|
|
|
|
|
|
if (url) {
|
|
|
|
urlFilter = `and url=$${params.length + 1}`;
|
|
|
|
params.push(decodeURIComponent(url));
|
|
|
|
}
|
|
|
|
|
2020-09-25 08:02:11 +02:00
|
|
|
return rawQuery(
|
|
|
|
`
|
|
|
|
select
|
|
|
|
event_value x,
|
|
|
|
${getDateQuery('created_at', unit, timezone)} t,
|
|
|
|
count(*) y
|
|
|
|
from event
|
|
|
|
where website_id=$1
|
|
|
|
and created_at between $2 and $3
|
2020-09-26 07:31:18 +02:00
|
|
|
${urlFilter}
|
2020-09-25 08:02:11 +02:00
|
|
|
group by 1, 2
|
|
|
|
order by 2
|
|
|
|
`,
|
2020-09-26 07:31:18 +02:00
|
|
|
params,
|
2020-09-25 08:02:11 +02:00
|
|
|
);
|
2020-08-27 12:42:24 +02:00
|
|
|
}
|
2020-10-10 20:04:07 +02:00
|
|
|
|
|
|
|
export async function getRealtimeData(websites, time) {
|
|
|
|
const [pageviews, sessions, events] = await Promise.all([
|
|
|
|
getPageviews(websites, time),
|
|
|
|
getSessions(websites, time),
|
|
|
|
getEvents(websites, time),
|
|
|
|
]);
|
|
|
|
|
|
|
|
return {
|
|
|
|
pageviews: pageviews.map(({ view_id, ...props }) => ({
|
|
|
|
__id: `p${view_id}`,
|
|
|
|
view_id,
|
|
|
|
...props,
|
|
|
|
})),
|
|
|
|
sessions: sessions.map(({ session_id, ...props }) => ({
|
|
|
|
__id: `s${session_id}`,
|
|
|
|
session_id,
|
|
|
|
...props,
|
|
|
|
})),
|
|
|
|
events: events.map(({ event_id, ...props }) => ({
|
|
|
|
__id: `e${event_id}`,
|
|
|
|
event_id,
|
|
|
|
...props,
|
|
|
|
})),
|
|
|
|
timestamp: Date.now(),
|
|
|
|
};
|
|
|
|
}
|