diff --git a/db/clickhouse/schema.sql b/db/clickhouse/schema.sql index 256b713d..209e6d5e 100644 --- a/db/clickhouse/schema.sql +++ b/db/clickhouse/schema.sql @@ -19,6 +19,7 @@ CREATE TABLE event url String, referrer String, --event + event_type UInt32, event_name String, event_data JSON, created_at DateTime('UTC') @@ -41,6 +42,7 @@ CREATE TABLE event_queue ( screen LowCardinality(String), language LowCardinality(String), country LowCardinality(String), + event_type UInt32, event_name String, event_data String, created_at DateTime('UTC') @@ -67,6 +69,7 @@ SELECT website_id, screen, language, country, + event_type, event_name, if((empty(event_data) = 0) AND startsWith(event_data, '"'), concat('{', event_data, ': true}'), event_data) AS event_data, created_at diff --git a/db/postgresql/schema.prisma b/db/postgresql/schema.prisma index 58b28a8b..538a1fa8 100644 --- a/db/postgresql/schema.prisma +++ b/db/postgresql/schema.prisma @@ -11,8 +11,8 @@ model User { id String @id @unique @map("user_id") @db.Uuid username String @unique @db.VarChar(255) password String @db.VarChar(60) - isAdmin Boolean @default(false) @map("is_admin") createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) + isDeleted Boolean @default(false) @map("is_deleted") groupRole GroupRole[] groupUser GroupUser[] @@ -20,7 +20,6 @@ model User { teamWebsite TeamWebsite[] teamUser TeamUser[] userWebsite UserWebsite[] - website Website[] @@map("user") } @@ -44,7 +43,6 @@ model Session { model Website { id String @id @unique @map("website_id") @db.Uuid - userId String @map("user_id") @db.Uuid name String @db.VarChar(100) domain String? @db.VarChar(500) shareId String? @unique @map("share_id") @db.VarChar(64) @@ -52,11 +50,9 @@ model Website { createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) isDeleted Boolean @default(false) @map("is_deleted") - user User @relation(fields: [userId], references: [id]) teamWebsite TeamWebsite[] userWebsite UserWebsite[] - @@index([userId]) @@index([createdAt]) @@index([shareId]) @@map("website") @@ -69,8 +65,9 @@ model WebsiteEvent { createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) url String @db.VarChar(500) referrer String? @db.VarChar(500) - eventName String @map("event_name") @db.VarChar(50) - eventData Json @map("event_data") + eventType Int @default(1) @map("event_type") @db.Integer + eventName String? @map("event_name") @db.VarChar(50) + eventData Json? @map("event_data") @@index([createdAt]) @@index([sessionId]) @@ -116,17 +113,18 @@ model GroupUser { isDeleted Boolean @default(false) @map("is_deleted") group Group @relation(fields: [groupId], references: [id]) - User User @relation(fields: [userId], references: [id]) + user User @relation(fields: [userId], references: [id]) @@map("group_user") } model Permission { - id String @id() @unique() @map("permission_id") @db.Uuid - name String @unique() @db.VarChar(255) - description String? @db.VarChar(255) - createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) - isDeleted Boolean @default(false) @map("is_deleted") + id String @id() @unique() @map("permission_id") @db.Uuid + name String @unique() @db.VarChar(255) + description String? @db.VarChar(255) + createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) + isDeleted Boolean @default(false) @map("is_deleted") + RolePermission RolePermission[] @@map("permission") } @@ -138,21 +136,37 @@ model Role { createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) isDeleted Boolean @default(false) @map("is_deleted") - groupRoles GroupRole[] - userRoles UserRole[] + groupRoles GroupRole[] + userRoles UserRole[] + RolePermission RolePermission[] @@map("role") } +model RolePermission { + id String @id() @unique() @map("role_permission_id") @db.Uuid + roleId String @map("role_id") @db.Uuid + permissionId String @map("permission_id") @db.Uuid + createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) + isDeleted Boolean @default(false) @map("is_deleted") + + role Role @relation(fields: [roleId], references: [id]) + permission Permission @relation(fields: [permissionId], references: [id]) + + @@map("role_permission") +} + model UserRole { id String @id() @unique() @map("user_role_id") @db.Uuid roleId String @map("role_id") @db.Uuid userId String @map("user_id") @db.Uuid + teamId String? @map("team_id") @db.Uuid createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) isDeleted Boolean @default(false) @map("is_deleted") - role Role @relation(fields: [roleId], references: [id]) - user User @relation(fields: [userId], references: [id]) + role Role @relation(fields: [roleId], references: [id]) + user User @relation(fields: [userId], references: [id]) + team Team? @relation(fields: [teamId], references: [id]) @@map("user_role") } @@ -165,6 +179,7 @@ model Team { teamWebsites TeamWebsite[] teamUsers TeamUser[] + UserRole UserRole[] @@map("team") } @@ -188,6 +203,7 @@ model TeamUser { id String @id() @unique() @map("team_user_id") @db.Uuid teamId String @map("team_id") @db.Uuid userId String @map("user_id") @db.Uuid + isOwner Boolean @default(false) @map("is_owner") createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) isDeleted Boolean @default(false) @map("is_deleted") @@ -202,6 +218,7 @@ model UserWebsite { userId String @map("user_id") @db.Uuid websiteId String @map("website_id") @db.Uuid createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) + isDeleted Boolean @default(false) @map("is_deleted") website Website @relation(fields: [websiteId], references: [id]) user User @relation(fields: [userId], references: [id]) diff --git a/interface/api/auth.d.ts b/interface/api/auth.d.ts new file mode 100644 index 00000000..6e4e3494 --- /dev/null +++ b/interface/api/auth.d.ts @@ -0,0 +1,8 @@ +export interface Auth { + user: { + id: string; + username: string; + isAdmin: boolean; + }; + shareToken: string; +} diff --git a/interface/api/models.d.ts b/interface/api/models.d.ts new file mode 100644 index 00000000..6251a149 --- /dev/null +++ b/interface/api/models.d.ts @@ -0,0 +1,73 @@ +export interface User { + id: string; + username: string; + isAdmin: boolean; + createdAt: string; +} + +export interface Website { + id: string; + userId: string; + revId: number; + name: string; + domain: string; + shareId: string; + createdAt: Date; +} + +export interface Share { + id: string; + token: string; +} + +export interface Empty {} + +export interface WebsiteActive { + x: number; +} + +export interface WebsiteEventDataMetric { + [key: string]: number; +} + +export interface WebsiteMetric { + x: string; + y: number; +} + +export interface WebsiteEventMetric { + x: string; + t: string; + y: number; +} + +export interface WebsitePageviews { + pageviews: { + t: string; + y: number; + }; + sessions: { + t: string; + y: number; + }; +} + +export interface WebsiteStats { + pageviews: { value: number; change: number }; + uniques: { value: number; change: number }; + bounces: { value: number; change: number }; + totalTime: { value: number; change: number }; +} + +export interface RealtimeInit { + websites: Website[]; + token: string; + data: RealtimeUpdate; +} + +export interface RealtimeUpdate { + pageviews: any[]; + sessions: any[]; + events: any[]; + timestamp: number; +} diff --git a/interface/api/nextApi.d.ts b/interface/api/nextApi.d.ts new file mode 100644 index 00000000..c5a54515 --- /dev/null +++ b/interface/api/nextApi.d.ts @@ -0,0 +1,14 @@ +import { NextApiRequest } from 'next'; +import { Auth } from './auth'; + +export interface NextApiRequestQueryBody extends NextApiRequest { + auth?: Auth; + query: TQuery & { [key: string]: string | string[] }; + body: TBody; + headers: any; +} + +export interface NextApiRequestAuth extends NextApiRequest { + auth?: Auth; + headers: any; +} diff --git a/interface/auth.d.ts b/interface/auth.d.ts deleted file mode 100644 index 36d3041e..00000000 --- a/interface/auth.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface Auth { - id: number; - email?: string; - teams?: string[]; -} diff --git a/interface/base.d.ts b/interface/base.d.ts deleted file mode 100644 index 5d498ebf..00000000 --- a/interface/base.d.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { NextApiRequest } from 'next'; -import { Auth } from './auth'; - -export interface NextApiRequestQueryBody extends NextApiRequest { - auth: Auth; - query: TQuery; - body: TBody; -} - -export interface NextApiRequestQuery extends NextApiRequest { - auth: Auth; - query: TQuery; -} - -export interface NextApiRequestBody extends NextApiRequest { - auth: Auth; - body: TBody; -} - -export interface ObjectAny { - [key: string]: any; -} diff --git a/interface/enum.d.ts b/interface/enum.d.ts new file mode 100644 index 00000000..8b09039c --- /dev/null +++ b/interface/enum.d.ts @@ -0,0 +1,7 @@ +/* eslint-disable no-unused-vars */ +export namespace UmamiApi { + enum EventType { + Pageview = 1, + Event = 2, + } +} diff --git a/interface/index.d.ts b/interface/index.d.ts index 5d498ebf..e69de29b 100644 --- a/interface/index.d.ts +++ b/interface/index.d.ts @@ -1,22 +0,0 @@ -import { NextApiRequest } from 'next'; -import { Auth } from './auth'; - -export interface NextApiRequestQueryBody extends NextApiRequest { - auth: Auth; - query: TQuery; - body: TBody; -} - -export interface NextApiRequestQuery extends NextApiRequest { - auth: Auth; - query: TQuery; -} - -export interface NextApiRequestBody extends NextApiRequest { - auth: Auth; - body: TBody; -} - -export interface ObjectAny { - [key: string]: any; -} diff --git a/lib/clickhouse.js b/lib/clickhouse.js index 3d44daac..b28694b6 100644 --- a/lib/clickhouse.js +++ b/lib/clickhouse.js @@ -106,7 +106,7 @@ function getEventDataFilterQuery(column, filters) { return query.join('\nand '); } -function getFilterQuery(column, filters = {}, params = []) { +function getFilterQuery(filters = {}, params = []) { const query = Object.keys(filters).reduce((arr, key) => { const filter = filters[key]; @@ -146,7 +146,7 @@ function getFilterQuery(column, filters = {}, params = []) { return query.join('\n'); } -function parseFilters(column, filters = {}, params = []) { +function parseFilters(filters = {}, params = []) { const { domain, url, event_url, referrer, os, browser, device, country, event_name, query } = filters; @@ -159,9 +159,7 @@ function parseFilters(column, filters = {}, params = []) { sessionFilters, eventFilters, event: { event_name }, - pageviewQuery: getFilterQuery(column, pageviewFilters, params), - sessionQuery: getFilterQuery(column, sessionFilters, params), - eventQuery: getFilterQuery(column, eventFilters, params), + filterQuery: getFilterQuery(filters, params), }; } diff --git a/lib/prisma.js b/lib/prisma.ts similarity index 70% rename from lib/prisma.js rename to lib/prisma.ts index ab1e6ebf..08cfb0e0 100644 --- a/lib/prisma.js +++ b/lib/prisma.ts @@ -4,6 +4,7 @@ import moment from 'moment-timezone'; import debug from 'debug'; import { PRISMA, MYSQL, POSTGRESQL, getDatabaseType } from 'lib/db'; import { FILTER_IGNORED } from 'lib/constants'; +import { PrismaClientOptions } from '@prisma/client/runtime'; const MYSQL_DATE_FORMATS = { minute: '%Y-%m-%d %H:%i:00', @@ -37,7 +38,8 @@ function logQuery(e) { } function getClient(options) { - const prisma = new PrismaClient(options); + const prisma: PrismaClient = + new PrismaClient(options); if (process.env.LOG_QUERY) { prisma.$on('query', logQuery); @@ -52,7 +54,7 @@ function getClient(options) { return prisma; } -function getDateQuery(field, unit, timezone) { +function getDateQuery(field, unit, timezone?): string { const db = getDatabaseType(process.env.DATABASE_URL); if (db === POSTGRESQL) { @@ -73,7 +75,7 @@ function getDateQuery(field, unit, timezone) { } } -function getTimestampInterval(field) { +function getTimestampInterval(field): string { const db = getDatabaseType(process.env.DATABASE_URL); if (db === POSTGRESQL) { @@ -85,7 +87,7 @@ function getTimestampInterval(field) { } } -function getJsonField(column, property, isNumber) { +function getJsonField(column, property, isNumber): string { const db = getDatabaseType(process.env.DATABASE_URL); if (db === POSTGRESQL) { @@ -103,7 +105,7 @@ function getJsonField(column, property, isNumber) { } } -function getEventDataColumnsQuery(column, columns) { +function getEventDataColumnsQuery(column, columns): string { const query = Object.keys(columns).reduce((arr, key) => { const filter = columns[key]; @@ -121,7 +123,7 @@ function getEventDataColumnsQuery(column, columns) { return query.join(',\n'); } -function getEventDataFilterQuery(column, filters) { +function getEventDataFilterQuery(column, filters): string { const query = Object.keys(filters).reduce((arr, key) => { const filter = filters[key]; @@ -143,7 +145,7 @@ function getEventDataFilterQuery(column, filters) { return query.join('\nand '); } -function getFilterQuery(table, column, filters = {}, params = []) { +function getFilterQuery(filters = {}, params = []): string { const query = Object.keys(filters).reduce((arr, key) => { const filter = filters[key]; @@ -153,48 +155,25 @@ function getFilterQuery(table, column, filters = {}, params = []) { switch (key) { case 'url': - if (table === 'pageview' || table === 'event') { - arr.push(`and ${table}.${key}=$${params.length + 1}`); - params.push(decodeURIComponent(filter)); - } - break; - case 'os': case 'browser': case 'device': case 'country': - if (table === 'session') { - arr.push(`and ${table}.${key}=$${params.length + 1}`); - params.push(decodeURIComponent(filter)); - } - break; - case 'event_name': - if (table === 'event') { - arr.push(`and ${table}.${key}=$${params.length + 1}`); - params.push(decodeURIComponent(filter)); - } + arr.push(`and ${key}=$${params.length + 1}`); + params.push(decodeURIComponent(filter)); break; - case 'referrer': - if (table === 'pageview' || table === 'event') { - arr.push(`and ${table}.referrer like $${params.length + 1}`); - params.push(`%${decodeURIComponent(filter)}%`); - } + arr.push(`and referrer like $${params.length + 1}`); + params.push(`%${decodeURIComponent(filter)}%`); break; - case 'domain': - if (table === 'pageview') { - arr.push(`and ${table}.referrer not like $${params.length + 1}`); - arr.push(`and ${table}.referrer not like '/%'`); - params.push(`%://${filter}/%`); - } + arr.push(`and referrer not like $${params.length + 1}`); + arr.push(`and referrer not like '/%'`); + params.push(`%://${filter}/%`); break; - case 'query': - if (table === 'pageview') { - arr.push(`and ${table}.url like '%?%'`); - } + arr.push(`and url like '%?%'`); } return arr; @@ -203,7 +182,11 @@ function getFilterQuery(table, column, filters = {}, params = []) { return query.join('\n'); } -function parseFilters(table, column, filters = {}, params = [], sessionKey = 'session_id') { +function parseFilters( + filters: { [key: string]: any } = {}, + params = [], + sessionKey = 'session_id', +) { const { domain, url, event_url, referrer, os, browser, device, country, event_name, query } = filters; @@ -218,15 +201,13 @@ function parseFilters(table, column, filters = {}, params = [], sessionKey = 'se event: { event_name }, joinSession: os || browser || device || country - ? `inner join session on ${table}.${sessionKey} = session.${sessionKey}` + ? `inner join session on ${sessionKey} = session.${sessionKey}` : '', - pageviewQuery: getFilterQuery('pageview', column, pageviewFilters, params), - sessionQuery: getFilterQuery('session', column, sessionFilters, params), - eventQuery: getFilterQuery('event', column, eventFilters, params), + filterQuery: getFilterQuery(filters, params), }; } -async function rawQuery(query, params = []) { +async function rawQuery(query, params = []): Promise { const db = getDatabaseType(process.env.DATABASE_URL); if (db !== POSTGRESQL && db !== MYSQL) { @@ -238,12 +219,13 @@ async function rawQuery(query, params = []) { return prisma.$queryRawUnsafe.apply(prisma, [sql, ...params]); } -async function transaction(queries) { +async function transaction(queries): Promise { return prisma.$transaction(queries); } // Initialization -const prisma = global[PRISMA] || getClient(PRISMA_OPTIONS); +const prisma: PrismaClient = + global[PRISMA] || getClient(PRISMA_OPTIONS); export default { client: prisma, diff --git a/pages/api/auth/login.js b/pages/api/auth/login.ts similarity index 65% rename from pages/api/auth/login.js rename to pages/api/auth/login.ts index a54e8013..321fb3ab 100644 --- a/pages/api/auth/login.js +++ b/pages/api/auth/login.ts @@ -10,8 +10,24 @@ import { import { getUser } from 'queries'; import { secret } from 'lib/crypto'; import redis from 'lib/redis'; +import { NextApiRequestQueryBody } from 'interface/api/nextApi'; +import { NextApiResponse } from 'next'; +import { User } from 'interface/api/models'; -export default async (req, res) => { +export interface LoginRequestBody { + username: string; + password: string; +} + +export interface LoginResponse { + token: string; + user: User; +} + +export default async ( + req: NextApiRequestQueryBody, + res: NextApiResponse, +) => { if (req.method === 'POST') { const { username, password } = req.body; @@ -19,7 +35,7 @@ export default async (req, res) => { return badRequest(res); } - const user = await getUser({ username }); + const user = await getUser({ username }, true); if (user && checkPassword(password, user.password)) { if (redis.enabled) { diff --git a/pages/api/auth/logout.js b/pages/api/auth/logout.ts similarity index 74% rename from pages/api/auth/logout.js rename to pages/api/auth/logout.ts index 37f117be..c05a05de 100644 --- a/pages/api/auth/logout.js +++ b/pages/api/auth/logout.ts @@ -2,8 +2,9 @@ import { methodNotAllowed, ok } from 'next-basics'; import { useAuth } from 'lib/middleware'; import redis from 'lib/redis'; import { getAuthToken } from 'lib/auth'; +import { NextApiRequest, NextApiResponse } from 'next'; -export default async (req, res) => { +export default async (req: NextApiRequest, res: NextApiResponse) => { await useAuth(req, res); if (req.method === 'POST') { diff --git a/pages/api/auth/verify.js b/pages/api/auth/verify.js deleted file mode 100644 index 670480b1..00000000 --- a/pages/api/auth/verify.js +++ /dev/null @@ -1,8 +0,0 @@ -import { useAuth } from 'lib/middleware'; -import { ok } from 'next-basics'; - -export default async (req, res) => { - await useAuth(req, res); - - return ok(res, req.auth); -}; diff --git a/pages/api/auth/verify.ts b/pages/api/auth/verify.ts new file mode 100644 index 00000000..024cd96a --- /dev/null +++ b/pages/api/auth/verify.ts @@ -0,0 +1,10 @@ +import { NextApiRequestAuth } from 'interface/api/nextApi'; +import { useAuth } from 'lib/middleware'; +import { NextApiResponse } from 'next'; +import { ok } from 'next-basics'; + +export default async (req: NextApiRequestAuth, res: NextApiResponse) => { + await useAuth(req, res); + + return ok(res, req.auth); +}; diff --git a/pages/api/collect.js b/pages/api/collect.ts similarity index 83% rename from pages/api/collect.js rename to pages/api/collect.ts index 87e516cc..2aa2bde1 100644 --- a/pages/api/collect.js +++ b/pages/api/collect.ts @@ -6,8 +6,23 @@ import { savePageView, saveEvent } from 'queries'; import { useCors, useSession } from 'lib/middleware'; import { getJsonBody, getIpAddress } from 'lib/request'; import { secret } from 'lib/crypto'; +import { NextApiRequest, NextApiResponse } from 'next'; -export default async (req, res) => { +export interface NextApiRequestCollect extends NextApiRequest { + session: { + id: string; + websiteId: string; + hostname: string; + browser: string; + os: string; + device: string; + screen: string; + language: string; + country: string; + }; +} + +export default async (req: NextApiRequestCollect, res: NextApiResponse) => { await useCors(req, res); if (isbot(req.headers['user-agent']) && !process.env.DISABLE_BOT_CHECK) { @@ -74,6 +89,7 @@ export default async (req, res) => { await saveEvent({ ...session, url, + referrer, eventName, eventData, }); diff --git a/pages/api/config.js b/pages/api/config.ts similarity index 57% rename from pages/api/config.js rename to pages/api/config.ts index faf94c0b..e5ae318a 100644 --- a/pages/api/config.js +++ b/pages/api/config.ts @@ -1,6 +1,15 @@ +import { NextApiRequest, NextApiResponse } from 'next'; import { ok, methodNotAllowed } from 'next-basics'; -export default async (req, res) => { +export interface ConfigResponse { + basePath: string; + trackerScriptName: string; + updatesDisabled: boolean; + telemetryDisabled: boolean; + adminDisabled: boolean; +} + +export default async (req: NextApiRequest, res: NextApiResponse) => { if (req.method === 'GET') { return ok(res, { basePath: process.env.BASE_PATH || '', diff --git a/pages/api/heartbeat.js b/pages/api/heartbeat.js deleted file mode 100644 index a4ee5923..00000000 --- a/pages/api/heartbeat.js +++ /dev/null @@ -1,5 +0,0 @@ -import { ok } from 'next-basics'; - -export default async (req, res) => { - return ok(res); -}; diff --git a/pages/api/heartbeat.ts b/pages/api/heartbeat.ts new file mode 100644 index 00000000..1b515d39 --- /dev/null +++ b/pages/api/heartbeat.ts @@ -0,0 +1,6 @@ +import { NextApiRequest, NextApiResponse } from 'next'; +import { ok } from 'next-basics'; + +export default async (req: NextApiRequest, res: NextApiResponse) => { + return ok(res); +}; diff --git a/pages/api/realtime/init.js b/pages/api/realtime/init.ts similarity index 60% rename from pages/api/realtime/init.js rename to pages/api/realtime/init.ts index 16e7cad3..b1d1f32f 100644 --- a/pages/api/realtime/init.js +++ b/pages/api/realtime/init.ts @@ -1,10 +1,13 @@ import { subMinutes } from 'date-fns'; -import { ok, methodNotAllowed, createToken } from 'next-basics'; -import { useAuth } from 'lib/middleware'; -import { getUserWebsites, getRealtimeData } from 'queries'; +import { RealtimeInit } from 'interface/api/models'; +import { NextApiRequestAuth } from 'interface/api/nextApi'; import { secret } from 'lib/crypto'; +import { useAuth } from 'lib/middleware'; +import { NextApiResponse } from 'next'; +import { createToken, methodNotAllowed, ok } from 'next-basics'; +import { getRealtimeData, getUserWebsites } from 'queries'; -export default async (req, res) => { +export default async (req: NextApiRequestAuth, res: NextApiResponse) => { await useAuth(req, res); if (req.method === 'GET') { diff --git a/pages/api/realtime/update.js b/pages/api/realtime/update.ts similarity index 64% rename from pages/api/realtime/update.js rename to pages/api/realtime/update.ts index 9b91663d..239ab1e6 100644 --- a/pages/api/realtime/update.js +++ b/pages/api/realtime/update.ts @@ -3,8 +3,18 @@ import { useAuth } from 'lib/middleware'; import { getRealtimeData } from 'queries'; import { SHARE_TOKEN_HEADER } from 'lib/constants'; import { secret } from 'lib/crypto'; +import { NextApiRequestQueryBody } from 'interface/api/nextApi'; +import { NextApiResponse } from 'next'; +import { RealtimeUpdate } from 'interface/api/models'; -export default async (req, res) => { +export interface InitUpdateRequestQuery { + start_at: string; +} + +export default async ( + req: NextApiRequestQueryBody, + res: NextApiResponse, +) => { await useAuth(req, res); if (req.method === 'GET') { diff --git a/pages/api/share/[id].js b/pages/api/share/[id].ts similarity index 52% rename from pages/api/share/[id].js rename to pages/api/share/[id].ts index 6fb19739..51385bee 100644 --- a/pages/api/share/[id].js +++ b/pages/api/share/[id].ts @@ -1,8 +1,22 @@ -import { getWebsite } from 'queries'; -import { ok, notFound, methodNotAllowed, createToken } from 'next-basics'; +import { NextApiRequestQueryBody } from 'interface/api/nextApi'; import { secret } from 'lib/crypto'; +import { NextApiResponse } from 'next'; +import { createToken, methodNotAllowed, notFound, ok } from 'next-basics'; +import { getWebsite } from 'queries'; -export default async (req, res) => { +export interface ShareRequestQuery { + id: string; +} + +export interface ShareResponse { + id: string; + token: string; +} + +export default async ( + req: NextApiRequestQueryBody, + res: NextApiResponse, +) => { const { id: shareId } = req.query; if (req.method === 'GET') { diff --git a/pages/api/teams/[id]/index.ts b/pages/api/teams/[id]/index.ts new file mode 100644 index 00000000..14710a54 --- /dev/null +++ b/pages/api/teams/[id]/index.ts @@ -0,0 +1,65 @@ +import { Team } from '@prisma/client'; +import { NextApiRequestQueryBody } from 'interface/api/nextApi'; +import { useAuth } from 'lib/middleware'; +import { NextApiResponse } from 'next'; +import { badRequest, methodNotAllowed, ok, unauthorized } from 'next-basics'; +import { deleteTeam, getTeam, updateTeam } from 'queries'; + +export interface TeamRequestQuery { + id: string; +} + +export interface TeamRequestBody { + name?: string; + is_deleted?: boolean; +} + +export default async ( + req: NextApiRequestQueryBody, + res: NextApiResponse, +) => { + await useAuth(req, res); + + const { + user: { id: userId, isAdmin }, + } = req.auth; + const { id } = req.query; + + if (req.method === 'GET') { + if (id !== userId && !isAdmin) { + return unauthorized(res); + } + + const user = await getTeam({ id }); + + return ok(res, user); + } + + if (req.method === 'POST') { + const { name, is_deleted: isDeleted } = req.body; + + if (id !== userId && !isAdmin) { + return unauthorized(res); + } + + const updated = await updateTeam({ name, isDeleted }, { id }); + + return ok(res, updated); + } + + if (req.method === 'DELETE') { + if (id === userId) { + return badRequest(res, 'You cannot delete your own user.'); + } + + if (!isAdmin) { + return unauthorized(res); + } + + await deleteTeam(id); + + return ok(res); + } + + return methodNotAllowed(res); +}; diff --git a/pages/api/teams/[id]/user.ts b/pages/api/teams/[id]/user.ts new file mode 100644 index 00000000..9f1290e1 --- /dev/null +++ b/pages/api/teams/[id]/user.ts @@ -0,0 +1,48 @@ +import { NextApiRequestQueryBody } from 'interface/api/nextApi'; +import { uuid } from 'lib/crypto'; +import { useAuth } from 'lib/middleware'; +import { NextApiResponse } from 'next'; +import { methodNotAllowed, ok } from 'next-basics'; +import { createTeamUser, deleteTeamUser, getUsersByTeamId } from 'queries'; + +export interface TeamUserRequestQuery { + id: string; +} + +export interface TeamUserRequestBody { + user_id: string; + team_user_id?: string; +} + +export default async ( + req: NextApiRequestQueryBody, + res: NextApiResponse, +) => { + await useAuth(req, res); + + const { id: teamId } = req.query; + + if (req.method === 'GET') { + const user = await getUsersByTeamId({ teamId }); + + return ok(res, user); + } + + if (req.method === 'POST') { + const { user_id: userId } = req.body; + + const updated = await createTeamUser({ id: uuid(), userId, teamId }); + + return ok(res, updated); + } + + if (req.method === 'DELETE') { + const { team_user_id } = req.body; + + await deleteTeamUser(team_user_id); + + return ok(res); + } + + return methodNotAllowed(res); +}; diff --git a/pages/api/teams/[id]/website.ts b/pages/api/teams/[id]/website.ts new file mode 100644 index 00000000..35c2e36e --- /dev/null +++ b/pages/api/teams/[id]/website.ts @@ -0,0 +1,48 @@ +import { NextApiRequestQueryBody } from 'interface/api/nextApi'; +import { uuid } from 'lib/crypto'; +import { useAuth } from 'lib/middleware'; +import { NextApiResponse } from 'next'; +import { methodNotAllowed, ok } from 'next-basics'; +import { createTeamWebsite, deleteTeamWebsite, getWebsitesByTeamId } from 'queries'; + +export interface TeamWebsiteRequestQuery { + id: string; +} + +export interface TeamWebsiteRequestBody { + website_id: string; + team_website_id?: string; +} + +export default async ( + req: NextApiRequestQueryBody, + res: NextApiResponse, +) => { + await useAuth(req, res); + + const { id: teamId } = req.query; + + if (req.method === 'GET') { + const website = await getWebsitesByTeamId({ teamId }); + + return ok(res, website); + } + + if (req.method === 'POST') { + const { website_id: websiteId } = req.body; + + const updated = await createTeamWebsite({ id: uuid(), websiteId, teamId }); + + return ok(res, updated); + } + + if (req.method === 'DELETE') { + const { team_website_id } = req.body; + + await deleteTeamWebsite(team_website_id); + + return ok(res); + } + + return methodNotAllowed(res); +}; diff --git a/pages/api/teams/index.ts b/pages/api/teams/index.ts new file mode 100644 index 00000000..8173c3b2 --- /dev/null +++ b/pages/api/teams/index.ts @@ -0,0 +1,47 @@ +import { Team } from '@prisma/client'; +import { NextApiRequestQueryBody } from 'interface/api/nextApi'; +import { uuid } from 'lib/crypto'; +import { useAuth } from 'lib/middleware'; +import { NextApiResponse } from 'next'; +import { badRequest, methodNotAllowed, ok } from 'next-basics'; +import { createTeam, getTeam, getTeamsByUserId } from 'queries'; +export interface TeamsRequestBody { + name: string; + description: string; +} + +export default async ( + req: NextApiRequestQueryBody, + res: NextApiResponse, +) => { + await useAuth(req, res); + + const { + user: { id }, + } = req.auth; + + if (req.method === 'GET') { + const users = await getTeamsByUserId(id); + + return ok(res, users); + } + + if (req.method === 'POST') { + const { name } = req.body; + + const user = await getTeam({ name }); + + if (user) { + return badRequest(res, 'Team already exists'); + } + + const created = await createTeam({ + id: id || uuid(), + name, + }); + + return ok(res, created); + } + + return methodNotAllowed(res); +}; diff --git a/pages/api/users/[id]/index.js b/pages/api/users/[id]/index.ts similarity index 77% rename from pages/api/users/[id]/index.js rename to pages/api/users/[id]/index.ts index a373bbd1..80b6f8b2 100644 --- a/pages/api/users/[id]/index.js +++ b/pages/api/users/[id]/index.ts @@ -1,8 +1,23 @@ import { badRequest, hashPassword, methodNotAllowed, ok, unauthorized } from 'next-basics'; import { getUser, deleteUser, updateUser } from 'queries'; import { useAuth } from 'lib/middleware'; +import { NextApiResponse } from 'next'; +import { NextApiRequestQueryBody } from 'interface/api/nextApi'; +import { User } from 'interface/api/models'; -export default async (req, res) => { +export interface UserRequestQuery { + id: string; +} + +export interface UserRequestBody { + username: string; + password: string; +} + +export default async ( + req: NextApiRequestQueryBody, + res: NextApiResponse, +) => { await useAuth(req, res); const { @@ -29,7 +44,7 @@ export default async (req, res) => { const user = await getUser({ id }); - const data = {}; + const data: any = {}; if (password) { data.password = hashPassword(password); diff --git a/pages/api/users/[id]/password.js b/pages/api/users/[id]/password.ts similarity index 65% rename from pages/api/users/[id]/password.js rename to pages/api/users/[id]/password.ts index 6cad82ed..4b00d7d5 100644 --- a/pages/api/users/[id]/password.js +++ b/pages/api/users/[id]/password.ts @@ -10,8 +10,23 @@ import { } from 'next-basics'; import { allowQuery } from 'lib/auth'; import { TYPE_USER } from 'lib/constants'; +import { NextApiRequestQueryBody } from 'interface/api/nextApi'; +import { NextApiResponse } from 'next'; +import { User } from 'interface/api/models'; -export default async (req, res) => { +export interface UserPasswordRequestQuery { + id: string; +} + +export interface UserPasswordRequestBody { + current_password: string; + new_password: string; +} + +export default async ( + req: NextApiRequestQueryBody, + res: NextApiResponse, +) => { await useAuth(req, res); const { current_password, new_password } = req.body; diff --git a/pages/api/users/index.js b/pages/api/users/index.ts similarity index 70% rename from pages/api/users/index.js rename to pages/api/users/index.ts index f4a5010a..07007546 100644 --- a/pages/api/users/index.js +++ b/pages/api/users/index.ts @@ -2,8 +2,20 @@ import { ok, unauthorized, methodNotAllowed, badRequest, hashPassword } from 'ne import { useAuth } from 'lib/middleware'; import { uuid } from 'lib/crypto'; import { createUser, getUser, getUsers } from 'queries'; +import { NextApiRequestQueryBody } from 'interface/api/nextApi'; +import { NextApiResponse } from 'next'; +import { User } from 'interface/api/models'; -export default async (req, res) => { +export interface UsersRequestBody { + username: string; + password: string; + id: string; +} + +export default async ( + req: NextApiRequestQueryBody, + res: NextApiResponse, +) => { await useAuth(req, res); const { diff --git a/pages/api/websites/[id]/active.js b/pages/api/websites/[id]/active.ts similarity index 62% rename from pages/api/websites/[id]/active.js rename to pages/api/websites/[id]/active.ts index 59af938e..b50c29e7 100644 --- a/pages/api/websites/[id]/active.js +++ b/pages/api/websites/[id]/active.ts @@ -3,8 +3,18 @@ import { allowQuery } from 'lib/auth'; import { useAuth, useCors } from 'lib/middleware'; import { getActiveVisitors } from 'queries'; import { TYPE_WEBSITE } from 'lib/constants'; +import { WebsiteActive } from 'interface/api/models'; +import { NextApiRequestQueryBody } from 'interface/api/nextApi'; +import { NextApiResponse } from 'next'; -export default async (req, res) => { +export interface WebsiteActiveRequestQuery { + id: string; +} + +export default async ( + req: NextApiRequestQueryBody, + res: NextApiResponse, +) => { await useCors(req, res); await useAuth(req, res); diff --git a/pages/api/websites/[id]/eventdata.js b/pages/api/websites/[id]/eventdata.ts similarity index 61% rename from pages/api/websites/[id]/eventdata.js rename to pages/api/websites/[id]/eventdata.ts index 0e6ad2e9..646b2920 100644 --- a/pages/api/websites/[id]/eventdata.js +++ b/pages/api/websites/[id]/eventdata.ts @@ -4,8 +4,27 @@ import { ok, badRequest, methodNotAllowed, unauthorized } from 'next-basics'; import { allowQuery } from 'lib/auth'; import { useAuth, useCors } from 'lib/middleware'; import { TYPE_WEBSITE } from 'lib/constants'; +import { NextApiRequestQueryBody } from 'interface/api/nextApi'; +import { NextApiResponse } from 'next'; +import { WebsiteMetric } from 'interface/api/models'; -export default async (req, res) => { +export interface WebsiteEventDataRequestQuery { + id: string; +} + +export interface WebsiteEventDataRequestBody { + start_at: string; + end_at: string; + timezone: string; + event_name: string; + columns: { [key: string]: 'count' | 'max' | 'min' | 'avg' | 'sum' }; + filters?: { [key: string]: any }; +} + +export default async ( + req: NextApiRequestQueryBody, + res: NextApiResponse, +) => { await useCors(req, res); await useAuth(req, res); diff --git a/pages/api/websites/[id]/events.js b/pages/api/websites/[id]/events.ts similarity index 53% rename from pages/api/websites/[id]/events.js rename to pages/api/websites/[id]/events.ts index da88794e..efbf5a39 100644 --- a/pages/api/websites/[id]/events.js +++ b/pages/api/websites/[id]/events.ts @@ -1,13 +1,29 @@ -import moment from 'moment-timezone'; -import { getEventMetrics } from 'queries'; -import { ok, badRequest, methodNotAllowed, unauthorized } from 'next-basics'; +import { WebsiteMetric } from 'interface/api/models'; +import { NextApiRequestQueryBody } from 'interface/api/nextApi'; import { allowQuery } from 'lib/auth'; -import { useAuth, useCors } from 'lib/middleware'; import { TYPE_WEBSITE } from 'lib/constants'; +import { useAuth, useCors } from 'lib/middleware'; +import moment from 'moment-timezone'; +import { NextApiResponse } from 'next'; +import { badRequest, methodNotAllowed, ok, unauthorized } from 'next-basics'; +import { getEventMetrics } from 'queries'; const unitTypes = ['year', 'month', 'hour', 'day']; -export default async (req, res) => { +export interface WebsiteEventsRequestQuery { + id: string; + start_at: string; + end_at: string; + unit: string; + tz: string; + url: string; + event_name: string; +} + +export default async ( + req: NextApiRequestQueryBody, + res: NextApiResponse, +) => { await useCors(req, res); await useAuth(req, res); @@ -24,9 +40,15 @@ export default async (req, res) => { const startDate = new Date(+start_at); const endDate = new Date(+end_at); - const events = await getEventMetrics(websiteId, startDate, endDate, tz, unit, { - url, - eventName: event_name, + const events = await getEventMetrics(websiteId, { + startDate, + endDate, + timezone: tz, + unit, + filters: { + url, + eventName: event_name, + }, }); return ok(res, events); diff --git a/pages/api/websites/[id]/index.js b/pages/api/websites/[id]/index.ts similarity index 72% rename from pages/api/websites/[id]/index.js rename to pages/api/websites/[id]/index.ts index 09056865..2ec1ee14 100644 --- a/pages/api/websites/[id]/index.js +++ b/pages/api/websites/[id]/index.ts @@ -3,8 +3,24 @@ import { useAuth, useCors } from 'lib/middleware'; import { methodNotAllowed, ok, serverError, unauthorized } from 'next-basics'; import { deleteWebsite, getWebsite, updateWebsite } from 'queries'; import { TYPE_WEBSITE } from 'lib/constants'; +import { NextApiRequestQueryBody } from 'interface/api/nextApi'; +import { NextApiResponse } from 'next'; +import { Website } from 'interface/api/models'; -export default async (req, res) => { +export interface WebsiteRequestQuery { + id: string; +} + +export interface WebsiteRequestBody { + name: string; + domain: string; + shareId: string; +} + +export default async ( + req: NextApiRequestQueryBody, + res: NextApiResponse, +) => { await useCors(req, res); await useAuth(req, res); diff --git a/pages/api/websites/[id]/metrics.js b/pages/api/websites/[id]/metrics.ts similarity index 85% rename from pages/api/websites/[id]/metrics.js rename to pages/api/websites/[id]/metrics.ts index 97216273..69c3c79d 100644 --- a/pages/api/websites/[id]/metrics.js +++ b/pages/api/websites/[id]/metrics.ts @@ -1,6 +1,9 @@ +import { WebsiteMetric } from 'interface/api/models'; +import { NextApiRequestQueryBody } from 'interface/api/nextApi'; import { allowQuery } from 'lib/auth'; import { FILTER_IGNORED, TYPE_WEBSITE } from 'lib/constants'; import { useAuth, useCors } from 'lib/middleware'; +import { NextApiResponse } from 'next'; import { badRequest, methodNotAllowed, ok, unauthorized } from 'next-basics'; import { getPageviewMetrics, getSessionMetrics, getWebsite } from 'queries'; @@ -33,7 +36,23 @@ function getColumn(type) { return type; } -export default async (req, res) => { +export interface WebsiteMetricsRequestQuery { + id: string; + type: string; + start_at: number; + end_at: number; + url: string; + referrer: string; + os: string; + browser: string; + device: string; + country: string; +} + +export default async ( + req: NextApiRequestQueryBody, + res: NextApiResponse, +) => { await useCors(req, res); await useAuth(req, res); diff --git a/pages/api/websites/[id]/pageviews.js b/pages/api/websites/[id]/pageviews.ts similarity index 67% rename from pages/api/websites/[id]/pageviews.js rename to pages/api/websites/[id]/pageviews.ts index 5b628e3a..5bbf067b 100644 --- a/pages/api/websites/[id]/pageviews.js +++ b/pages/api/websites/[id]/pageviews.ts @@ -1,13 +1,34 @@ -import moment from 'moment-timezone'; -import { getPageviewStats } from 'queries'; -import { ok, badRequest, methodNotAllowed, unauthorized } from 'next-basics'; +import { WebsitePageviews } from 'interface/api/models'; +import { NextApiRequestQueryBody } from 'interface/api/nextApi'; import { allowQuery } from 'lib/auth'; -import { useAuth, useCors } from 'lib/middleware'; import { TYPE_WEBSITE } from 'lib/constants'; +import { useAuth, useCors } from 'lib/middleware'; +import moment from 'moment-timezone'; +import { NextApiResponse } from 'next'; +import { badRequest, methodNotAllowed, ok, unauthorized } from 'next-basics'; +import { getPageviewStats } from 'queries'; const unitTypes = ['year', 'month', 'hour', 'day']; -export default async (req, res) => { +export interface WebsitePageviewRequestQuery { + id: string; + websiteId: string; + start_at: number; + end_at: number; + unit: string; + tz: string; + url?: string; + referrer?: string; + os?: string; + browser?: string; + device?: string; + country?: string; +} + +export default async ( + req: NextApiRequestQueryBody, + res: NextApiResponse, +) => { await useCors(req, res); await useAuth(req, res); @@ -39,8 +60,8 @@ export default async (req, res) => { const [pageviews, sessions] = await Promise.all([ getPageviewStats(websiteId, { - start_at: startDate, - end_at: endDate, + startDate, + endDate, timezone: tz, unit, count: '*', @@ -54,8 +75,8 @@ export default async (req, res) => { }, }), getPageviewStats(websiteId, { - start_at: startDate, - end_at: endDate, + startDate, + endDate, timezone: tz, unit, count: 'distinct pageview.', diff --git a/pages/api/websites/[id]/reset.js b/pages/api/websites/[id]/reset.ts similarity index 66% rename from pages/api/websites/[id]/reset.js rename to pages/api/websites/[id]/reset.ts index 0dde02df..ff141398 100644 --- a/pages/api/websites/[id]/reset.js +++ b/pages/api/websites/[id]/reset.ts @@ -3,8 +3,17 @@ import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { allowQuery } from 'lib/auth'; import { useAuth, useCors } from 'lib/middleware'; import { TYPE_WEBSITE } from 'lib/constants'; +import { NextApiRequestQueryBody } from 'interface/api/nextApi'; +import { NextApiResponse } from 'next'; -export default async (req, res) => { +export interface WebsiteResetRequestQuery { + id: string; +} + +export default async ( + req: NextApiRequestQueryBody, + res: NextApiResponse, +) => { await useCors(req, res); await useAuth(req, res); diff --git a/pages/api/websites/[id]/stats.js b/pages/api/websites/[id]/stats.ts similarity index 71% rename from pages/api/websites/[id]/stats.js rename to pages/api/websites/[id]/stats.ts index 2c5b0156..497c3ff2 100644 --- a/pages/api/websites/[id]/stats.js +++ b/pages/api/websites/[id]/stats.ts @@ -3,8 +3,27 @@ import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { allowQuery } from 'lib/auth'; import { useAuth, useCors } from 'lib/middleware'; import { TYPE_WEBSITE } from 'lib/constants'; +import { WebsiteStats } from 'interface/api/models'; +import { NextApiRequestQueryBody } from 'interface/api/nextApi'; +import { NextApiResponse } from 'next'; -export default async (req, res) => { +export interface WebsiteStatsRequestQuery { + id: string; + type: string; + start_at: number; + end_at: number; + url: string; + referrer: string; + os: string; + browser: string; + device: string; + country: string; +} + +export default async ( + req: NextApiRequestQueryBody, + res: NextApiResponse, +) => { await useCors(req, res); await useAuth(req, res); @@ -33,8 +52,8 @@ export default async (req, res) => { const prevEndDate = new Date(+end_at - distance); const metrics = await getWebsiteStats(websiteId, { - start_at: startDate, - end_at: endDate, + startDate, + endDate, filters: { url, referrer, @@ -45,8 +64,8 @@ export default async (req, res) => { }, }); const prevPeriod = await getWebsiteStats(websiteId, { - start_at: prevStartDate, - end_at: prevEndDate, + startDate: prevStartDate, + endDate: prevEndDate, filters: { url, referrer, diff --git a/pages/api/websites/index.js b/pages/api/websites/index.js deleted file mode 100644 index 3966ad70..00000000 --- a/pages/api/websites/index.js +++ /dev/null @@ -1,33 +0,0 @@ -import { createWebsite, getAllWebsites, getUserWebsites } from 'queries'; -import { ok, methodNotAllowed, getRandomChars } from 'next-basics'; -import { useAuth, useCors } from 'lib/middleware'; -import { uuid } from 'lib/crypto'; - -export default async (req, res) => { - await useCors(req, res); - await useAuth(req, res); - - const { - user: { id: userId, isAdmin }, - } = req.auth; - - if (req.method === 'GET') { - const { include_all } = req.query; - - const websites = - isAdmin && include_all ? await getAllWebsites() : await getUserWebsites(userId); - - return ok(res, websites); - } - - if (req.method === 'POST') { - const { name, domain, enableShareUrl } = req.body; - - const shareId = enableShareUrl ? getRandomChars(8) : null; - const website = await createWebsite(userId, { id: uuid(), name, domain, shareId }); - - return ok(res, website); - } - - return methodNotAllowed(res); -}; diff --git a/pages/api/websites/index.ts b/pages/api/websites/index.ts new file mode 100644 index 00000000..4c5ad07c --- /dev/null +++ b/pages/api/websites/index.ts @@ -0,0 +1,48 @@ +import { NextApiRequestQueryBody } from 'interface/api/nextApi'; +import { uuid } from 'lib/crypto'; +import { useAuth, useCors } from 'lib/middleware'; +import { NextApiResponse } from 'next'; +import { getRandomChars, methodNotAllowed, ok } from 'next-basics'; +import { createWebsiteByUser, getAllWebsites, getWebsitesByUserId } from 'queries'; + +export interface WebsitesRequestQuery { + include_all?: boolean; +} + +export interface WebsitesRequestBody { + name: string; + domain: string; + enableShareUrl: boolean; +} + +export default async ( + req: NextApiRequestQueryBody, + res: NextApiResponse, +) => { + await useCors(req, res); + await useAuth(req, res); + + const { + user: { id: userId, isAdmin }, + } = req.auth; + + if (req.method === 'GET') { + const { include_all } = req.query; + + const websites = + isAdmin && include_all ? await getAllWebsites() : await getWebsitesByUserId(userId); + + return ok(res, websites); + } + + if (req.method === 'POST') { + const { name, domain, enableShareUrl } = req.body; + + const shareId = enableShareUrl ? getRandomChars(8) : null; + const website = await createWebsiteByUser(userId, { id: uuid(), name, domain, shareId }); + + return ok(res, website); + } + + return methodNotAllowed(res); +}; diff --git a/queries/admin/permission.ts b/queries/admin/permission.ts new file mode 100644 index 00000000..37d1647e --- /dev/null +++ b/queries/admin/permission.ts @@ -0,0 +1,41 @@ +import { Prisma, Permission } from '@prisma/client'; +import prisma from 'lib/prisma'; + +export async function createPermission(data: Prisma.PermissionCreateInput): Promise { + return prisma.client.permission.create({ + data, + }); +} + +export async function getPermission(where: Prisma.PermissionWhereUniqueInput): Promise { + return prisma.client.permission.findUnique({ + where, + }); +} + +export async function getPermissions(where: Prisma.PermissionWhereInput): Promise { + return prisma.client.permission.findMany({ + where, + }); +} + +export async function updatePermission( + data: Prisma.PermissionUpdateInput, + where: Prisma.PermissionWhereUniqueInput, +): Promise { + return prisma.client.permission.update({ + data, + where, + }); +} + +export async function deletePermission(permissionId: string): Promise { + return prisma.client.permission.update({ + data: { + isDeleted: true, + }, + where: { + id: permissionId, + }, + }); +} diff --git a/queries/admin/role.ts b/queries/admin/role.ts new file mode 100644 index 00000000..2bf39930 --- /dev/null +++ b/queries/admin/role.ts @@ -0,0 +1,57 @@ +import { Prisma, Role } from '@prisma/client'; +import prisma from 'lib/prisma'; + +export async function createRole(data: { + id: string; + name: string; + description: string; +}): Promise { + return prisma.client.role.create({ + data, + }); +} + +export async function getRole(where: Prisma.RoleWhereUniqueInput): Promise { + return prisma.client.role.findUnique({ + where, + }); +} + +export async function getRoles(where: Prisma.RoleWhereInput): Promise { + return prisma.client.role.findMany({ + where, + }); +} + +export async function getRolesByUserId(userId: string): Promise { + return prisma.client.role.findMany({ + where: { + userRoles: { + every: { + userId, + }, + }, + }, + }); +} + +export async function updateRole( + data: Prisma.RoleUpdateInput, + where: Prisma.RoleWhereUniqueInput, +): Promise { + return prisma.client.role.update({ + data, + where, + }); +} + +export async function deleteRole(roleId: string): Promise { + return prisma.client.role.update({ + data: { + isDeleted: true, + }, + where: { + id: roleId, + }, + }); +} diff --git a/queries/admin/team.ts b/queries/admin/team.ts new file mode 100644 index 00000000..8687fb64 --- /dev/null +++ b/queries/admin/team.ts @@ -0,0 +1,56 @@ +import { Prisma, Team, TeamUser } from '@prisma/client'; +import prisma from 'lib/prisma'; + +export async function createTeam(data: Prisma.TeamCreateInput): Promise { + return prisma.client.role.create({ + data, + }); +} + +export async function getTeam(where: Prisma.TeamWhereUniqueInput): Promise { + return prisma.client.role.findUnique({ + where, + }); +} + +export async function getTeams(where: Prisma.TeamWhereInput): Promise { + return prisma.client.role.findMany({ + where, + }); +} + +export async function getTeamsByUserId(userId: string): Promise< + (TeamUser & { + team: Team; + })[] +> { + return prisma.client.teamUser.findMany({ + where: { + userId, + }, + include: { + team: true, + }, + }); +} + +export async function updateTeam( + data: Prisma.TeamUpdateInput, + where: Prisma.TeamWhereUniqueInput, +): Promise { + return prisma.client.role.update({ + data, + where, + }); +} + +export async function deleteTeam(teamId: string): Promise { + return prisma.client.role.update({ + data: { + isDeleted: true, + }, + where: { + id: teamId, + }, + }); +} diff --git a/queries/admin/teamUser.ts b/queries/admin/teamUser.ts new file mode 100644 index 00000000..b2f7bbf2 --- /dev/null +++ b/queries/admin/teamUser.ts @@ -0,0 +1,43 @@ +import { Prisma, TeamUser } from '@prisma/client'; +import prisma from 'lib/prisma'; + +export async function createTeamUser( + data: Prisma.TeamUserCreateInput | Prisma.TeamUserUncheckedCreateInput, +): Promise { + return prisma.client.teamUser.create({ + data, + }); +} + +export async function getTeamUser(where: Prisma.TeamUserWhereUniqueInput): Promise { + return prisma.client.teamUser.findUnique({ + where, + }); +} + +export async function getTeamUsers(where: Prisma.TeamUserWhereInput): Promise { + return prisma.client.teamUser.findMany({ + where, + }); +} + +export async function updateTeamUser( + data: Prisma.TeamUserUpdateInput, + where: Prisma.TeamUserWhereUniqueInput, +): Promise { + return prisma.client.teamUser.update({ + data, + where, + }); +} + +export async function deleteTeamUser(teamUserId: string): Promise { + return prisma.client.teamUser.update({ + data: { + isDeleted: true, + }, + where: { + id: teamUserId, + }, + }); +} diff --git a/queries/admin/teamWebsite.ts b/queries/admin/teamWebsite.ts new file mode 100644 index 00000000..6b485da0 --- /dev/null +++ b/queries/admin/teamWebsite.ts @@ -0,0 +1,45 @@ +import { Prisma, TeamWebsite } from '@prisma/client'; +import prisma from 'lib/prisma'; + +export async function createTeamWebsite( + data: Prisma.TeamWebsiteCreateInput | Prisma.TeamWebsiteUncheckedCreateInput, +): Promise { + return prisma.client.teamWebsite.create({ + data, + }); +} + +export async function getTeamWebsite( + where: Prisma.TeamWebsiteWhereUniqueInput, +): Promise { + return prisma.client.teamWebsite.findUnique({ + where, + }); +} + +export async function getTeamWebsites(where: Prisma.TeamWebsiteWhereInput): Promise { + return prisma.client.teamWebsite.findMany({ + where, + }); +} + +export async function updateTeamWebsite( + data: Prisma.TeamWebsiteUpdateInput, + where: Prisma.TeamWebsiteWhereUniqueInput, +): Promise { + return prisma.client.teamWebsite.update({ + data, + where, + }); +} + +export async function deleteTeamWebsite(teamWebsiteId: string): Promise { + return prisma.client.teamWebsite.update({ + data: { + isDeleted: true, + }, + where: { + id: teamWebsiteId, + }, + }); +} diff --git a/queries/admin/user.ts b/queries/admin/user.ts new file mode 100644 index 00000000..98eadc1b --- /dev/null +++ b/queries/admin/user.ts @@ -0,0 +1,152 @@ +import { Prisma } from '@prisma/client'; +import { UmamiApi } from 'interface/enum'; +import cache from 'lib/cache'; +import prisma from 'lib/prisma'; + +export interface User { + id: string; + username: string; + password?: string; + createdAt?: Date; +} + +export async function createUser(data: { + id: string; + username: string; + password: string; +}): Promise<{ + id: string; + username: string; +}> { + return prisma.client.user.create({ + data, + select: { + id: true, + username: true, + }, + }); +} + +export async function getUser( + where: Prisma.UserWhereUniqueInput, + includePassword = false, +): Promise { + return prisma.client.user.findUnique({ + where, + select: { + id: true, + username: true, + userRole: { + include: { + role: true, + }, + }, + password: includePassword, + }, + }); +} + +export async function getUsers(): Promise { + return prisma.client.user.findMany({ + orderBy: [ + { + username: 'asc', + }, + ], + select: { + id: true, + username: true, + createdAt: true, + }, + }); +} + +export async function getUsersByTeamId(teamId): Promise { + return prisma.client.user.findMany({ + where: { + teamUser: { + every: { + teamId, + }, + }, + }, + select: { + id: true, + username: true, + createdAt: true, + }, + }); +} + +export async function updateUser( + data: Prisma.UserUpdateInput, + where: Prisma.UserWhereUniqueInput, +): Promise { + return prisma.client.user + .update({ + where, + data, + select: { + id: true, + username: true, + createdAt: true, + userRole: true, + }, + }) + .then(user => { + const { userRole, ...rest } = user; + + return { ...rest, isAdmin: userRole.some(a => a.roleId === UmamiApi.SystemRole.Admin) }; + }); +} + +export async function deleteUser( + userId: string, +): Promise<[Prisma.BatchPayload, Prisma.BatchPayload, Prisma.BatchPayload, User]> { + const { client } = prisma; + + const websites = await client.userWebsite.findMany({ + where: { userId }, + }); + + let websiteIds = []; + + if (websites.length > 0) { + websiteIds = websites.map(a => a.websiteId); + } + + return client + .$transaction([ + client.websiteEvent.deleteMany({ + where: { websiteId: { in: websiteIds } }, + }), + client.session.deleteMany({ + where: { websiteId: { in: websiteIds } }, + }), + client.website.updateMany({ + data: { + isDeleted: true, + }, + where: { id: { in: websiteIds } }, + }), + client.user.update({ + data: { + isDeleted: true, + }, + where: { + id: userId, + }, + }), + ]) + .then(async data => { + if (cache.enabled) { + const ids = websites.map(a => a.id); + + for (let i = 0; i < ids.length; i++) { + await cache.deleteWebsite(`website:${ids[i]}`); + } + } + + return data; + }); +} diff --git a/queries/admin/user/createUser.js b/queries/admin/user/createUser.js deleted file mode 100644 index 54e008fe..00000000 --- a/queries/admin/user/createUser.js +++ /dev/null @@ -1,7 +0,0 @@ -import prisma from 'lib/prisma'; - -export async function createUser(data) { - return prisma.client.user.create({ - data, - }); -} diff --git a/queries/admin/user/deleteUser.js b/queries/admin/user/deleteUser.js deleted file mode 100644 index 5970d2a5..00000000 --- a/queries/admin/user/deleteUser.js +++ /dev/null @@ -1,45 +0,0 @@ -import prisma from 'lib/prisma'; -import cache from 'lib/cache'; - -export async function deleteUser(userId) { - const { client } = prisma; - - const websites = await client.website.findMany({ - where: { userId }, - }); - - let websiteIds = []; - - if (websites.length > 0) { - websiteIds = websites.map(a => a.id); - } - - return client - .$transaction([ - client.websiteEvent.deleteMany({ - where: { websiteId: { in: websiteIds } }, - }), - client.session.deleteMany({ - where: { websiteId: { in: websiteIds } }, - }), - client.website.deleteMany({ - where: { userId }, - }), - client.user.delete({ - where: { - id: userId, - }, - }), - ]) - .then(async data => { - if (cache.enabled) { - const ids = websites.map(a => a.id); - - for (let i = 0; i < ids.length; i++) { - await cache.deleteWebsite(`website:${ids[i]}`); - } - } - - return data; - }); -} diff --git a/queries/admin/user/getUser.js b/queries/admin/user/getUser.js deleted file mode 100644 index 6c9ebd88..00000000 --- a/queries/admin/user/getUser.js +++ /dev/null @@ -1,7 +0,0 @@ -import prisma from 'lib/prisma'; - -export async function getUser(where) { - return prisma.client.user.findUnique({ - where, - }); -} diff --git a/queries/admin/user/getUsers.js b/queries/admin/user/getUsers.js deleted file mode 100644 index 2fc473cb..00000000 --- a/queries/admin/user/getUsers.js +++ /dev/null @@ -1,18 +0,0 @@ -import prisma from 'lib/prisma'; - -export async function getUsers() { - return prisma.client.user.findMany({ - orderBy: [ - { isAdmin: 'desc' }, - { - username: 'asc', - }, - ], - select: { - id: true, - username: true, - isAdmin: true, - createdAt: true, - }, - }); -} diff --git a/queries/admin/user/updateUser.js b/queries/admin/user/updateUser.js deleted file mode 100644 index ea80cf43..00000000 --- a/queries/admin/user/updateUser.js +++ /dev/null @@ -1,8 +0,0 @@ -import prisma from 'lib/prisma'; - -export async function updateUser(data, where) { - return prisma.client.user.update({ - where, - data, - }); -} diff --git a/queries/admin/userRole.ts b/queries/admin/userRole.ts new file mode 100644 index 00000000..22893412 --- /dev/null +++ b/queries/admin/userRole.ts @@ -0,0 +1,43 @@ +import { Prisma, UserRole } from '@prisma/client'; +import prisma from 'lib/prisma'; + +export async function createUserRole( + data: Prisma.UserRoleCreateInput | Prisma.UserRoleUncheckedCreateInput, +): Promise { + return prisma.client.userRole.create({ + data, + }); +} + +export async function getUserRole(where: Prisma.UserRoleWhereUniqueInput): Promise { + return prisma.client.userRole.findUnique({ + where, + }); +} + +export async function getUserRoles(where: Prisma.UserRoleWhereInput): Promise { + return prisma.client.userRole.findMany({ + where, + }); +} + +export async function updateUserRole( + data: Prisma.UserRoleUpdateInput, + where: Prisma.UserRoleWhereUniqueInput, +): Promise { + return prisma.client.userRole.update({ + data, + where, + }); +} + +export async function deleteUserRole(userRoleId: string): Promise { + return prisma.client.userRole.update({ + data: { + isDeleted: true, + }, + where: { + id: userRoleId, + }, + }); +} diff --git a/queries/admin/userWebsite.ts b/queries/admin/userWebsite.ts new file mode 100644 index 00000000..90039b8f --- /dev/null +++ b/queries/admin/userWebsite.ts @@ -0,0 +1,45 @@ +import { Prisma, UserWebsite } from '@prisma/client'; +import prisma from 'lib/prisma'; + +export async function createUserWebsite( + data: Prisma.UserWebsiteCreateInput | Prisma.UserWebsiteUncheckedCreateInput, +): Promise { + return prisma.client.userWebsite.create({ + data, + }); +} + +export async function getUserWebsite( + where: Prisma.UserWebsiteWhereUniqueInput, +): Promise { + return prisma.client.userWebsite.findUnique({ + where, + }); +} + +export async function getUserWebsites(where: Prisma.UserWebsiteWhereInput): Promise { + return prisma.client.userWebsite.findMany({ + where, + }); +} + +export async function updateUserWebsite( + data: Prisma.UserWebsiteUpdateInput, + where: Prisma.UserWebsiteWhereUniqueInput, +): Promise { + return prisma.client.userWebsite.update({ + data, + where, + }); +} + +export async function deleteUserWebsite(userWebsiteId: string): Promise { + return prisma.client.userWebsite.update({ + data: { + isDeleted: true, + }, + where: { + id: userWebsiteId, + }, + }); +} diff --git a/queries/admin/website.ts b/queries/admin/website.ts new file mode 100644 index 00000000..240798da --- /dev/null +++ b/queries/admin/website.ts @@ -0,0 +1,176 @@ +import { Prisma, Website } from '@prisma/client'; +import cache from 'lib/cache'; +import prisma from 'lib/prisma'; + +export async function createWebsiteByUser( + userId: string, + data: { + id: string; + name: string; + domain: string; + shareId?: string; + }, +): Promise { + return prisma.client.website + .create({ + data: { + userWebsite: { + connect: { + id: userId, + }, + }, + ...data, + }, + }) + .then(async data => { + if (cache.enabled) { + await cache.storeWebsite(data); + } + + return data; + }); +} + +export async function createWebsiteByTeam( + teamId: string, + data: { + id: string; + name: string; + domain: string; + shareId?: string; + }, +): Promise { + return prisma.client.website + .create({ + data: { + teamWebsite: { + connect: { + id: teamId, + }, + }, + ...data, + }, + }) + .then(async data => { + if (cache.enabled) { + await cache.storeWebsite(data); + } + + return data; + }); +} + +export async function updateWebsite(websiteId, data: Prisma.WebsiteUpdateInput): Promise { + return prisma.client.website.update({ + where: { + id: websiteId, + }, + data, + }); +} + +export async function resetWebsite( + websiteId, +): Promise<[Prisma.BatchPayload, Prisma.BatchPayload, Website]> { + const { client, transaction } = prisma; + + const { revId } = await getWebsite({ id: websiteId }); + + return transaction([ + client.websiteEvent.deleteMany({ + where: { websiteId }, + }), + client.session.deleteMany({ + where: { websiteId }, + }), + client.website.update({ where: { id: websiteId }, data: { revId: revId + 1 } }), + ]).then(async data => { + if (cache.enabled) { + await cache.storeWebsite(data[2]); + } + + return data; + }); +} + +export async function getWebsite(where: Prisma.WebsiteWhereUniqueInput): Promise { + return prisma.client.website.findUnique({ + where, + }); +} + +export async function getWebsitesByUserId(userId): Promise { + return prisma.client.website.findMany({ + where: { + userWebsite: { + every: { + userId, + }, + }, + }, + orderBy: { + name: 'asc', + }, + }); +} + +export async function getWebsitesByTeamId(teamId): Promise { + return prisma.client.website.findMany({ + where: { + teamWebsite: { + every: { + teamId, + }, + }, + }, + orderBy: { + name: 'asc', + }, + }); +} + +export async function getAllWebsites(): Promise<(Website & { user: string })[]> { + return await prisma.client.website + .findMany({ + orderBy: [ + { + name: 'asc', + }, + ], + include: { + userWebsite: { + include: { + user: true, + }, + }, + }, + }) + .then(data => data.map(i => ({ ...i, user: i.userWebsite[0]?.userId }))); +} + +export async function deleteWebsite( + websiteId: string, +): Promise<[Prisma.BatchPayload, Prisma.BatchPayload, Website]> { + const { client, transaction } = prisma; + + return transaction([ + client.websiteEvent.deleteMany({ + where: { websiteId }, + }), + client.session.deleteMany({ + where: { websiteId }, + }), + client.website.update({ + data: { + isDeleted: true, + }, + where: { id: websiteId }, + }), + ]).then(async data => { + if (cache.enabled) { + await cache.deleteWebsite(websiteId); + } + + return data; + }); +} diff --git a/queries/admin/website/createWebsite.js b/queries/admin/website/createWebsite.js deleted file mode 100644 index 0afe0bea..00000000 --- a/queries/admin/website/createWebsite.js +++ /dev/null @@ -1,23 +0,0 @@ -import prisma from 'lib/prisma'; -import cache from 'lib/cache'; - -export async function createWebsite(userId, data) { - return prisma.client.website - .create({ - data: { - user: { - connect: { - id: userId, - }, - }, - ...data, - }, - }) - .then(async data => { - if (cache.enabled) { - await cache.storeWebsite(data); - } - - return data; - }); -} diff --git a/queries/admin/website/deleteWebsite.js b/queries/admin/website/deleteWebsite.js deleted file mode 100644 index 685cee8a..00000000 --- a/queries/admin/website/deleteWebsite.js +++ /dev/null @@ -1,24 +0,0 @@ -import prisma from 'lib/prisma'; -import cache from 'lib/cache'; - -export async function deleteWebsite(id) { - const { client, transaction } = prisma; - - return transaction([ - client.websiteEvent.deleteMany({ - where: { websiteId: id }, - }), - client.session.deleteMany({ - where: { websiteId: id }, - }), - client.website.delete({ - where: { id }, - }), - ]).then(async data => { - if (cache.enabled) { - await cache.deleteWebsite(id); - } - - return data; - }); -} diff --git a/queries/admin/website/getAllWebsites.js b/queries/admin/website/getAllWebsites.js deleted file mode 100644 index f9ad262c..00000000 --- a/queries/admin/website/getAllWebsites.js +++ /dev/null @@ -1,23 +0,0 @@ -import prisma from 'lib/prisma'; - -export async function getAllWebsites() { - let data = await prisma.client.website.findMany({ - orderBy: [ - { - userId: 'asc', - }, - { - name: 'asc', - }, - ], - include: { - user: { - select: { - username: true, - }, - }, - }, - }); - - return data.map(i => ({ ...i, user: i.user.username })); -} diff --git a/queries/admin/website/getUserWebsites.js b/queries/admin/website/getUserWebsites.js deleted file mode 100644 index c1a9d559..00000000 --- a/queries/admin/website/getUserWebsites.js +++ /dev/null @@ -1,12 +0,0 @@ -import prisma from 'lib/prisma'; - -export async function getUserWebsites(userId) { - return prisma.client.website.findMany({ - where: { - userId, - }, - orderBy: { - name: 'asc', - }, - }); -} diff --git a/queries/admin/website/getWebsite.js b/queries/admin/website/getWebsite.js deleted file mode 100644 index 83c3e83a..00000000 --- a/queries/admin/website/getWebsite.js +++ /dev/null @@ -1,7 +0,0 @@ -import prisma from 'lib/prisma'; - -export async function getWebsite(where) { - return prisma.client.website.findUnique({ - where, - }); -} diff --git a/queries/admin/website/resetWebsite.js b/queries/admin/website/resetWebsite.js deleted file mode 100644 index f4d685cb..00000000 --- a/queries/admin/website/resetWebsite.js +++ /dev/null @@ -1,25 +0,0 @@ -import prisma from 'lib/prisma'; -import { getWebsite } from 'queries'; -import cache from 'lib/cache'; - -export async function resetWebsite(id) { - const { client, transaction } = prisma; - - const { revId } = await getWebsite({ id }); - - return transaction([ - client.websiteEvent.deleteMany({ - where: { websiteId: id }, - }), - client.session.deleteMany({ - where: { websiteId: id }, - }), - client.website.update({ where: { id }, data: { revId: revId + 1 } }), - ]).then(async data => { - if (cache.enabled) { - await cache.storeWebsite(data[2]); - } - - return data; - }); -} diff --git a/queries/admin/website/updateWebsite.js b/queries/admin/website/updateWebsite.js deleted file mode 100644 index 5ac70a61..00000000 --- a/queries/admin/website/updateWebsite.js +++ /dev/null @@ -1,10 +0,0 @@ -import prisma from 'lib/prisma'; - -export async function updateWebsite(id, data) { - return prisma.client.website.update({ - where: { - id, - }, - data, - }); -} diff --git a/queries/analytics/event/getEventData.js b/queries/analytics/event/getEventData.ts similarity index 54% rename from queries/analytics/event/getEventData.js rename to queries/analytics/event/getEventData.ts index f7d725c1..2e776f52 100644 --- a/queries/analytics/event/getEventData.js +++ b/queries/analytics/event/getEventData.ts @@ -2,8 +2,21 @@ import clickhouse from 'lib/clickhouse'; import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; import prisma from 'lib/prisma'; import cache from 'lib/cache'; +import { WebsiteMetric } from 'interface/api/models'; +import { UmamiApi } from 'interface/enum'; -export async function getEventData(...args) { +export async function getEventData( + ...args: [ + websiteId: string, + data: { + startDate: Date; + endDate: Date; + event_name: string; + columns: any; + filters: object; + }, + ] +): Promise { return runQuery({ [PRISMA]: () => relationalQuery(...args), [CLICKHOUSE]: () => clickhouseQuery(...args), @@ -14,31 +27,48 @@ export async function getEventData(...args) { }); } -async function relationalQuery(websiteId, { startDate, endDate, event_name, columns, filters }) { +async function relationalQuery( + websiteId: string, + data: { + startDate: Date; + endDate: Date; + event_name: string; + columns: any; + filters: object; + }, +) { + const { startDate, endDate, event_name, columns, filters } = data; const { rawQuery, getEventDataColumnsQuery, getEventDataFilterQuery } = prisma; const params = [startDate, endDate]; return rawQuery( `select - ${getEventDataColumnsQuery('event_data.event_data', columns)} - from event - join website - on event.website_id = website.website_id - join event_data - on event.event_id = event_data.event_id - where website.website_id ='${websiteId}' - and event.created_at between $1 and $2 + ${getEventDataColumnsQuery('event_data', columns)} + from website_event + where website_id ='${websiteId}' + and created_at between $1 and $2 + and event_type = ${UmamiApi.EventType.Event} ${event_name ? `and event_name = ${event_name}` : ''} ${ Object.keys(filters).length > 0 - ? `and ${getEventDataFilterQuery('event_data.event_data', filters)}` + ? `and ${getEventDataFilterQuery('event_data', filters)}` : '' }`, params, ); } -async function clickhouseQuery(websiteId, { startDate, endDate, event_name, columns, filters }) { +async function clickhouseQuery( + websiteId: string, + data: { + startDate: Date; + endDate: Date; + event_name: string; + columns: any; + filters: object; + }, +) { + const { startDate, endDate, event_name, columns, filters } = data; const { rawQuery, getBetweenDates, getEventDataColumnsQuery, getEventDataFilterQuery } = clickhouse; const website = await cache.fetchWebsite(websiteId); @@ -50,6 +80,7 @@ async function clickhouseQuery(websiteId, { startDate, endDate, event_name, colu from event where website_id = $1 and rev_id = $2 + and event_type = ${UmamiApi.EventType.Event} ${event_name ? `and event_name = ${event_name}` : ''} and ${getBetweenDates('created_at', startDate, endDate)} ${ diff --git a/queries/analytics/event/getEventMetrics.js b/queries/analytics/event/getEventMetrics.js deleted file mode 100644 index 27ee6d04..00000000 --- a/queries/analytics/event/getEventMetrics.js +++ /dev/null @@ -1,68 +0,0 @@ -import prisma from 'lib/prisma'; -import clickhouse from 'lib/clickhouse'; -import { runQuery, CLICKHOUSE, PRISMA } from 'lib/db'; -import cache from 'lib/cache'; - -export async function getEventMetrics(...args) { - return runQuery({ - [PRISMA]: () => relationalQuery(...args), - [CLICKHOUSE]: () => clickhouseQuery(...args), - }); -} - -async function relationalQuery( - websiteId, - start_at, - end_at, - timezone = 'utc', - unit = 'day', - filters = {}, -) { - const { rawQuery, getDateQuery, getFilterQuery } = prisma; - const params = [start_at, end_at]; - - return rawQuery( - `select - event_name x, - ${getDateQuery('event.created_at', unit, timezone)} t, - count(*) y - from event - join website - on event.website_id = website.website_id - where website.website_id='${websiteId}' - and event.created_at between $1 and $2 - ${getFilterQuery('event', filters, params)} - group by 1, 2 - order by 2`, - params, - ); -} - -async function clickhouseQuery( - websiteId, - start_at, - end_at, - timezone = 'UTC', - unit = 'day', - filters = {}, -) { - const { rawQuery, getDateQuery, getBetweenDates, getFilterQuery } = clickhouse; - const website = await cache.fetchWebsite(websiteId); - const params = [websiteId, website?.revId || 0]; - - return rawQuery( - `select - event_name x, - ${getDateQuery('created_at', unit, timezone)} t, - count(*) y - from event - where event_name != '' - and website_id = $1 - and rev_id = $2 - and ${getBetweenDates('created_at', start_at, end_at)} - ${getFilterQuery('event', filters, params)} - group by x, t - order by t`, - params, - ); -} diff --git a/queries/analytics/event/getEventMetrics.ts b/queries/analytics/event/getEventMetrics.ts new file mode 100644 index 00000000..c2e819c4 --- /dev/null +++ b/queries/analytics/event/getEventMetrics.ts @@ -0,0 +1,105 @@ +import prisma from 'lib/prisma'; +import clickhouse from 'lib/clickhouse'; +import { runQuery, CLICKHOUSE, PRISMA } from 'lib/db'; +import cache from 'lib/cache'; +import { WebsiteEventMetric } from 'interface/api/models'; +import { UmamiApi } from 'interface/enum'; + +export async function getEventMetrics( + ...args: [ + websiteId: string, + data: { + startDate: Date; + endDate: Date; + timezone: string; + unit: string; + filters: { + url: string; + eventName: string; + }; + }, + ] +): Promise { + return runQuery({ + [PRISMA]: () => relationalQuery(...args), + [CLICKHOUSE]: () => clickhouseQuery(...args), + }); +} + +async function relationalQuery( + websiteId: string, + { + startDate, + endDate, + timezone = 'utc', + unit = 'day', + filters, + }: { + startDate: Date; + endDate: Date; + timezone: string; + unit: string; + filters: { + url: string; + eventName: string; + }; + }, +) { + const { rawQuery, getDateQuery, getFilterQuery } = prisma; + const params = [startDate, endDate]; + + return rawQuery( + `select + event_name x, + ${getDateQuery('created_at', unit, timezone)} t, + count(*) y + from website_event + where website_id='${websiteId}' + and created_at between $1 and $2 + and event_type = ${UmamiApi.EventType.Event} + ${getFilterQuery(filters, params)} + group by 1, 2 + order by 2`, + params, + ); +} + +async function clickhouseQuery( + websiteId: string, + { + startDate, + endDate, + timezone = 'utc', + unit = 'day', + filters, + }: { + startDate: Date; + endDate: Date; + timezone: string; + unit: string; + filters: { + url: string; + eventName: string; + }; + }, +) { + const { rawQuery, getDateQuery, getBetweenDates, getFilterQuery } = clickhouse; + const website = await cache.fetchWebsite(websiteId); + const params = [websiteId, website?.revId || 0]; + + return rawQuery( + `select + event_name x, + ${getDateQuery('created_at', unit, timezone)} t, + count(*) y + from event + where website_id = $1 + and rev_id = $2 + and event_type = ${UmamiApi.EventType.Event} + and ${getBetweenDates('created_at', startDate, endDate)} + ${getFilterQuery(filters, params)} + group by x, t + order by t`, + params, + ); +} diff --git a/queries/analytics/event/saveEvent.js b/queries/analytics/event/saveEvent.js deleted file mode 100644 index 3bb3b0bf..00000000 --- a/queries/analytics/event/saveEvent.js +++ /dev/null @@ -1,62 +0,0 @@ -import { EVENT_NAME_LENGTH, URL_LENGTH } from 'lib/constants'; -import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; -import kafka from 'lib/kafka'; -import prisma from 'lib/prisma'; -import { uuid } from 'lib/crypto'; -import cache from 'lib/cache'; - -export async function saveEvent(...args) { - return runQuery({ - [PRISMA]: () => relationalQuery(...args), - [CLICKHOUSE]: () => clickhouseQuery(...args), - }); -} - -async function relationalQuery(data) { - const { websiteId, sessionId, url, eventName, eventData } = data; - const eventId = uuid(); - - const params = { - id: eventId, - websiteId, - sessionId, - url: url?.substring(0, URL_LENGTH), - eventName: eventName?.substring(0, EVENT_NAME_LENGTH), - }; - - if (eventData) { - params.eventData = { - create: { - id: eventId, - eventData: eventData, - }, - }; - } - - return prisma.client.event.create({ - data: params, - }); -} - -async function clickhouseQuery(data) { - const { websiteId, id: sessionId, url, eventName, eventData, country, ...args } = data; - const { getDateFormat, sendMessage } = kafka; - const website = await cache.fetchWebsite(websiteId); - - const params = { - website_id: websiteId, - session_id: sessionId, - event_id: uuid(), - url: url?.substring(0, URL_LENGTH), - event_name: eventName?.substring(0, EVENT_NAME_LENGTH), - event_data: eventData ? JSON.stringify(eventData) : null, - rev_id: website?.revId || 0, - created_at: getDateFormat(new Date()), - country: country ? country : null, - ...args, - }; - - await sendMessage(params, 'event'); - - return data; -} diff --git a/queries/analytics/event/saveEvent.ts b/queries/analytics/event/saveEvent.ts new file mode 100644 index 00000000..6bb44ef5 --- /dev/null +++ b/queries/analytics/event/saveEvent.ts @@ -0,0 +1,91 @@ +import { EVENT_NAME_LENGTH, URL_LENGTH } from 'lib/constants'; +import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; +import kafka from 'lib/kafka'; +import prisma from 'lib/prisma'; +import { uuid } from 'lib/crypto'; +import cache from 'lib/cache'; +import { UmamiApi } from 'interface/enum'; + +export async function saveEvent(args: { + id: string; + websiteId: string; + url: string; + referrer?: string; + eventName?: string; + eventData?: any; + hostname?: string; + browser?: string; + os?: string; + device?: string; + screen?: string; + language?: string; + country?: string; +}) { + return runQuery({ + [PRISMA]: () => relationalQuery(args), + [CLICKHOUSE]: () => clickhouseQuery(args), + }); +} + +async function relationalQuery(data: { + id: string; + websiteId: string; + url: string; + referrer?: string; + eventName?: string; + eventData?: any; +}) { + const { websiteId, id: sessionId, url, eventName, eventData, referrer } = data; + + const params = { + id: uuid(), + websiteId, + sessionId, + url: url?.substring(0, URL_LENGTH), + referrer: referrer?.substring(0, URL_LENGTH), + eventType: UmamiApi.EventType.Event, + eventName: eventName?.substring(0, EVENT_NAME_LENGTH), + eventData, + }; + + return prisma.client.websiteEvent.create({ + data: params, + }); +} + +async function clickhouseQuery(data: { + id: string; + websiteId: string; + url: string; + referrer?: string; + eventName?: string; + eventData?: any; + hostname?: string; + browser?: string; + os?: string; + device?: string; + screen?: string; + language?: string; + country?: string; +}) { + const { websiteId, id: sessionId, url, eventName, eventData, country, ...args } = data; + const { getDateFormat, sendMessage } = kafka; + const website = await cache.fetchWebsite(websiteId); + + const params = { + website_id: websiteId, + session_id: sessionId, + event_id: uuid(), + url: url?.substring(0, URL_LENGTH), + event_name: eventName?.substring(0, EVENT_NAME_LENGTH), + event_data: eventData ? JSON.stringify(eventData) : null, + rev_id: website?.revId || 0, + created_at: getDateFormat(new Date()), + country: country ? country : null, + ...args, + }; + + await sendMessage(params, 'event'); + + return data; +} diff --git a/queries/analytics/pageview/getPageviewMetrics.js b/queries/analytics/pageview/getPageviewMetrics.js deleted file mode 100644 index 8dfd6595..00000000 --- a/queries/analytics/pageview/getPageviewMetrics.js +++ /dev/null @@ -1,59 +0,0 @@ -import prisma from 'lib/prisma'; -import clickhouse from 'lib/clickhouse'; -import { runQuery, CLICKHOUSE, PRISMA } from 'lib/db'; -import cache from 'lib/cache'; - -export async function getPageviewMetrics(...args) { - return runQuery({ - [PRISMA]: () => relationalQuery(...args), - [CLICKHOUSE]: () => clickhouseQuery(...args), - }); -} - -async function relationalQuery(websiteId, { startDate, endDate, column, table, filters = {} }) { - const { rawQuery, parseFilters } = prisma; - const params = [startDate, endDate]; - const { pageviewQuery, sessionQuery, eventQuery, joinSession } = parseFilters( - table, - column, - filters, - params, - ); - - return rawQuery( - `select ${column} x, count(*) y - from ${table} - ${` join website on ${table}.website_id = website.website_id`} - ${joinSession} - where website.website_id='${websiteId}' - and ${table}.created_at between $1 and $2 - ${pageviewQuery} - ${joinSession && sessionQuery} - ${eventQuery} - group by 1 - order by 2 desc`, - params, - ); -} - -async function clickhouseQuery(websiteId, { startDate, endDate, column, filters = {} }) { - const { rawQuery, parseFilters, getBetweenDates } = clickhouse; - const website = await cache.fetchWebsite(websiteId); - const params = [websiteId, website?.revId || 0]; - const { pageviewQuery, sessionQuery, eventQuery } = parseFilters(column, filters, params); - - return rawQuery( - `select ${column} x, count(*) y - from event - where website_id = $1 - and rev_id = $2 - ${column !== 'event_name' ? `and event_name = ''` : `and event_name != ''`} - and ${getBetweenDates('created_at', startDate, endDate)} - ${pageviewQuery} - ${sessionQuery} - ${eventQuery} - group by x - order by y desc`, - params, - ); -} diff --git a/queries/analytics/pageview/getPageviewMetrics.ts b/queries/analytics/pageview/getPageviewMetrics.ts new file mode 100644 index 00000000..106d14a9 --- /dev/null +++ b/queries/analytics/pageview/getPageviewMetrics.ts @@ -0,0 +1,82 @@ +import prisma from 'lib/prisma'; +import clickhouse from 'lib/clickhouse'; +import { runQuery, CLICKHOUSE, PRISMA } from 'lib/db'; +import cache from 'lib/cache'; +import { Prisma } from '@prisma/client'; +import { UmamiApi } from 'interface/enum'; + +export async function getPageviewMetrics( + ...args: [ + websiteId: string, + data: { + startDate: Date; + endDate: Date; + column: Prisma.WebsiteEventScalarFieldEnum | Prisma.SessionScalarFieldEnum; + table: string; + filters: object; + }, + ] +) { + return runQuery({ + [PRISMA]: () => relationalQuery(...args), + [CLICKHOUSE]: () => clickhouseQuery(...args), + }); +} + +async function relationalQuery( + websiteId: string, + data: { + startDate: Date; + endDate: Date; + column: Prisma.WebsiteEventScalarFieldEnum | Prisma.SessionScalarFieldEnum; + filters: object; + }, +) { + const { startDate, endDate, column, filters = {} } = data; + const { rawQuery, parseFilters } = prisma; + const params = [startDate, endDate]; + const { filterQuery, joinSession } = parseFilters(filters, params); + + return rawQuery( + `select ${column} x, count(*) y + from website_event + ${joinSession} + where website_id='${websiteId}' + and website_event.created_at between $1 and $2 + and event_type = ${UmamiApi.EventType.Pageview} + ${filterQuery} + group by 1 + order by 2 desc`, + params, + ); +} + +async function clickhouseQuery( + websiteId: string, + data: { + startDate: Date; + endDate: Date; + column: Prisma.WebsiteEventScalarFieldEnum | Prisma.SessionScalarFieldEnum; + filters: object; + }, +) { + const { startDate, endDate, column, filters = {} } = data; + const { rawQuery, parseFilters, getBetweenDates } = clickhouse; + const website = await cache.fetchWebsite(websiteId); + const params = [websiteId, website?.revId || 0]; + const { filterQuery } = parseFilters(filters, params); + + return rawQuery( + `select ${column} x, count(*) y + from event + where website_id = $1 + and rev_id = $2 + and event_type = ${UmamiApi.EventType.Pageview} + ${column !== 'event_name' ? `and event_name = ''` : `and event_name != ''`} + and ${getBetweenDates('created_at', startDate, endDate)} + ${filterQuery} + group by x + order by y desc`, + params, + ); +} diff --git a/queries/analytics/pageview/getPageviewParams.js b/queries/analytics/pageview/getPageviewParams.js deleted file mode 100644 index ce96c25b..00000000 --- a/queries/analytics/pageview/getPageviewParams.js +++ /dev/null @@ -1,41 +0,0 @@ -import prisma from 'lib/prisma'; -import { runQuery, CLICKHOUSE, PRISMA } from 'lib/db'; - -export async function getPageviewParams(...args) { - return runQuery({ - [PRISMA]: () => relationalQuery(...args), - [CLICKHOUSE]: () => clickhouseQuery(...args), - }); -} - -async function relationalQuery(websiteId, start_at, end_at, column, table, filters = {}) { - const { parseFilters, rawQuery } = prisma; - const params = [start_at, end_at]; - const { pageviewQuery, sessionQuery, eventQuery, joinSession } = parseFilters( - table, - column, - filters, - params, - ); - - return rawQuery( - `select url x, - count(*) y - from ${table} - ${` join website on ${table}.website_id = website.website_id`} - ${joinSession} - where website.website_id='${websiteId}' - and ${table}.created_at between $1 and $2 - and ${table}.url like '%?%' - ${pageviewQuery} - ${joinSession && sessionQuery} - ${eventQuery} - group by 1 - order by 2 desc`, - params, - ); -} - -function clickhouseQuery() { - return Promise.reject(new Error('Not implemented.')); -} diff --git a/queries/analytics/pageview/getPageviewStats.js b/queries/analytics/pageview/getPageviewStats.js deleted file mode 100644 index c711d448..00000000 --- a/queries/analytics/pageview/getPageviewStats.js +++ /dev/null @@ -1,78 +0,0 @@ -import prisma from 'lib/prisma'; -import clickhouse from 'lib/clickhouse'; -import { runQuery, CLICKHOUSE, PRISMA } from 'lib/db'; -import cache from 'lib/cache'; - -export async function getPageviewStats(...args) { - return runQuery({ - [PRISMA]: () => relationalQuery(...args), - [CLICKHOUSE]: () => clickhouseQuery(...args), - }); -} - -async function relationalQuery( - websiteId, - { - start_at, - end_at, - timezone = 'utc', - unit = 'day', - count = '*', - filters = {}, - sessionKey = 'session_id', - }, -) { - const { getDateQuery, parseFilters, rawQuery } = prisma; - const params = [start_at, end_at]; - const { pageviewQuery, sessionQuery, joinSession } = parseFilters( - 'pageview', - null, - filters, - params, - ); - - return rawQuery( - `select ${getDateQuery('pageview.created_at', unit, timezone)} t, - count(${count !== '*' ? `${count}${sessionKey}` : count}) y - from pageview - join website - on pageview.website_id = website.website_id - ${joinSession} - where website.website_id='${websiteId}' - and pageview.created_at between $1 and $2 - ${pageviewQuery} - ${sessionQuery} - group by 1`, - params, - ); -} - -async function clickhouseQuery( - websiteId, - { start_at, end_at, timezone = 'UTC', unit = 'day', count = '*', filters = {} }, -) { - const { parseFilters, rawQuery, getDateStringQuery, getDateQuery, getBetweenDates } = clickhouse; - const website = await cache.fetchWebsite(websiteId); - const params = [websiteId, website?.revId || 0]; - const { pageviewQuery, sessionQuery } = parseFilters(null, filters, params); - - return rawQuery( - `select - ${getDateStringQuery('g.t', unit)} as t, - g.y as y - from - (select - ${getDateQuery('created_at', unit, timezone)} t, - count(${count !== '*' ? 'distinct session_id' : count}) y - from event - where event_name = '' - and website_id = $1 - and rev_id = $2 - and ${getBetweenDates('created_at', start_at, end_at)} - ${pageviewQuery} - ${sessionQuery} - group by t) g - order by t`, - params, - ); -} diff --git a/queries/analytics/pageview/getPageviewStats.ts b/queries/analytics/pageview/getPageviewStats.ts new file mode 100644 index 00000000..4a7a7782 --- /dev/null +++ b/queries/analytics/pageview/getPageviewStats.ts @@ -0,0 +1,102 @@ +import cache from 'lib/cache'; +import clickhouse from 'lib/clickhouse'; +import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; +import prisma from 'lib/prisma'; +import { UmamiApi } from 'interface/enum'; + +export async function getPageviewStats( + ...args: [ + websiteId: string, + data: { + startDate: Date; + endDate: Date; + timezone?: string; + unit?: string; + count?: string; + filters: object; + sessionKey?: string; + }, + ] +) { + return runQuery({ + [PRISMA]: () => relationalQuery(...args), + [CLICKHOUSE]: () => clickhouseQuery(...args), + }); +} + +async function relationalQuery( + websiteId: string, + data: { + startDate: Date; + endDate: Date; + timezone?: string; + unit?: string; + count?: string; + filters: object; + sessionKey?: string; + }, +) { + const { + startDate, + endDate, + timezone = 'utc', + unit = 'day', + count = '*', + filters = {}, + sessionKey = 'session_id', + } = data; + const { getDateQuery, parseFilters, rawQuery } = prisma; + const params = [startDate, endDate]; + const { filterQuery, joinSession } = parseFilters(filters, params); + + return rawQuery( + `select ${getDateQuery('website_event.created_at', unit, timezone)} t, + count(${count !== '*' ? `${count}${sessionKey}` : count}) y + from website_event + ${joinSession} + where website.website_id='${websiteId}' + and pageview.created_at between $1 and $2 + and event_type = ${UmamiApi.EventType.Pageview} + ${filterQuery} + group by 1`, + params, + ); +} + +async function clickhouseQuery( + websiteId: string, + data: { + startDate: Date; + endDate: Date; + timezone?: string; + unit?: string; + count?: string; + filters: object; + sessionKey?: string; + }, +) { + const { startDate, endDate, timezone = 'UTC', unit = 'day', count = '*', filters = {} } = data; + const { parseFilters, rawQuery, getDateStringQuery, getDateQuery, getBetweenDates } = clickhouse; + const website = await cache.fetchWebsite(websiteId); + const params = [websiteId, website?.revId || 0]; + const { filterQuery } = parseFilters(filters, params); + + return rawQuery( + `select + ${getDateStringQuery('g.t', unit)} as t, + g.y as y + from + (select + ${getDateQuery('created_at', unit, timezone)} t, + count(${count !== '*' ? 'distinct session_id' : count}) y + from event + where website_id = $1 + and rev_id = $2 + and event_type = ${UmamiApi.EventType.Pageview} + and ${getBetweenDates('created_at', startDate, endDate)} + ${filterQuery} + group by t) g + order by t`, + params, + ); +} diff --git a/queries/analytics/pageview/savePageView.js b/queries/analytics/pageview/savePageView.ts similarity index 60% rename from queries/analytics/pageview/savePageView.js rename to queries/analytics/pageview/savePageView.ts index adcb4b3f..134dfd09 100644 --- a/queries/analytics/pageview/savePageView.js +++ b/queries/analytics/pageview/savePageView.ts @@ -4,23 +4,43 @@ import kafka from 'lib/kafka'; import prisma from 'lib/prisma'; import cache from 'lib/cache'; import { uuid } from 'lib/crypto'; +import { UmamiApi } from 'interface/enum'; -export async function savePageView(...args) { +export async function savePageView(args: { + id: string; + websiteId: string; + url: string; + referrer?: string; + hostname?: string; + browser?: string; + os?: string; + device?: string; + screen?: string; + language?: string; + country?: string; +}) { return runQuery({ - [PRISMA]: () => relationalQuery(...args), - [CLICKHOUSE]: () => clickhouseQuery(...args), + [PRISMA]: () => relationalQuery(args), + [CLICKHOUSE]: () => clickhouseQuery(args), }); } -async function relationalQuery(data) { - const { websiteId, sessionId, url, referrer } = data; - return prisma.client.pageview.create({ +async function relationalQuery(data: { + id: string; + websiteId: string; + url: string; + referrer?: string; +}) { + const { websiteId, id: sessionId, url, referrer } = data; + + return prisma.client.websiteEvent.create({ data: { id: uuid(), websiteId, sessionId, url: url?.substring(0, URL_LENGTH), referrer: referrer?.substring(0, URL_LENGTH), + eventType: UmamiApi.EventType.Pageview, }, }); } diff --git a/queries/analytics/session/createSession.js b/queries/analytics/session/createSession.ts similarity index 63% rename from queries/analytics/session/createSession.js rename to queries/analytics/session/createSession.ts index f401a20f..fe15f11c 100644 --- a/queries/analytics/session/createSession.js +++ b/queries/analytics/session/createSession.ts @@ -2,11 +2,12 @@ import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; import kafka from 'lib/kafka'; import prisma from 'lib/prisma'; import cache from 'lib/cache'; +import { Prisma } from '@prisma/client'; -export async function createSession(...args) { +export async function createSession(args: Prisma.SessionCreateInput) { return runQuery({ - [PRISMA]: () => relationalQuery(...args), - [CLICKHOUSE]: () => clickhouseQuery(...args), + [PRISMA]: () => relationalQuery(args), + [CLICKHOUSE]: () => clickhouseQuery(args), }).then(async data => { if (cache.enabled) { await cache.storeSession(data); @@ -16,11 +17,21 @@ export async function createSession(...args) { }); } -async function relationalQuery(data) { +async function relationalQuery(data: Prisma.SessionCreateInput) { return prisma.client.session.create({ data }); } -async function clickhouseQuery(data) { +async function clickhouseQuery(data: { + id: string; + websiteId: string; + hostname?: string; + browser?: string; + os?: string; + device?: string; + screen?: string; + language?: string; + country?: string; +}) { const { id, websiteId, hostname, browser, os, device, screen, language, country } = data; const { getDateFormat, sendMessage } = kafka; const website = await cache.fetchWebsite(websiteId); diff --git a/queries/analytics/session/getSession.js b/queries/analytics/session/getSession.ts similarity index 64% rename from queries/analytics/session/getSession.js rename to queries/analytics/session/getSession.ts index adc9acd8..19875117 100644 --- a/queries/analytics/session/getSession.js +++ b/queries/analytics/session/getSession.ts @@ -1,21 +1,22 @@ import clickhouse from 'lib/clickhouse'; import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; import prisma from 'lib/prisma'; +import { Prisma } from '@prisma/client'; -export async function getSession(...args) { +export async function getSession(args: { id: string }) { return runQuery({ - [PRISMA]: () => relationalQuery(...args), - [CLICKHOUSE]: () => clickhouseQuery(...args), + [PRISMA]: () => relationalQuery(args), + [CLICKHOUSE]: () => clickhouseQuery(args), }); } -async function relationalQuery(where) { +async function relationalQuery(where: Prisma.SessionWhereUniqueInput) { return prisma.client.session.findUnique({ where, }); } -async function clickhouseQuery({ id: sessionId }) { +async function clickhouseQuery({ id: sessionId }: { id: string }) { const { rawQuery, findFirst } = clickhouse; const params = [sessionId]; diff --git a/queries/analytics/session/getSessionMetrics.js b/queries/analytics/session/getSessionMetrics.ts similarity index 63% rename from queries/analytics/session/getSessionMetrics.js rename to queries/analytics/session/getSessionMetrics.ts index 7db0f9b5..04e1801a 100644 --- a/queries/analytics/session/getSessionMetrics.js +++ b/queries/analytics/session/getSessionMetrics.ts @@ -3,17 +3,26 @@ import clickhouse from 'lib/clickhouse'; import { runQuery, CLICKHOUSE, PRISMA } from 'lib/db'; import cache from 'lib/cache'; -export async function getSessionMetrics(...args) { +export async function getSessionMetrics( + ...args: [ + websiteId: string, + data: { startDate: Date; endDate: Date; field: string; filters: object }, + ] +) { return runQuery({ [PRISMA]: () => relationalQuery(...args), [CLICKHOUSE]: () => clickhouseQuery(...args), }); } -async function relationalQuery(websiteId, { startDate, endDate, field, filters = {} }) { +async function relationalQuery( + websiteId: string, + data: { startDate: Date; endDate: Date; field: string; filters: object }, +) { + const { startDate, endDate, field, filters = {} } = data; const { parseFilters, rawQuery } = prisma; const params = [startDate, endDate]; - const { pageviewQuery, sessionQuery, joinSession } = parseFilters(null, filters, params); + const { filterQuery, joinSession } = parseFilters(filters, params); return rawQuery( `select ${field} x, count(*) y @@ -26,8 +35,7 @@ async function relationalQuery(websiteId, { startDate, endDate, field, filters = ${joinSession} where website.website_id='${websiteId}' and pageview.created_at between $1 and $2 - ${pageviewQuery} - ${sessionQuery} + ${filterQuery} ) group by 1 order by 2 desc`, @@ -35,11 +43,15 @@ async function relationalQuery(websiteId, { startDate, endDate, field, filters = ); } -async function clickhouseQuery(websiteId, { startDate, endDate, field, filters = {} }) { +async function clickhouseQuery( + websiteId: string, + data: { startDate: Date; endDate: Date; field: string; filters: object }, +) { + const { startDate, endDate, field, filters = {} } = data; const { parseFilters, getBetweenDates, rawQuery } = clickhouse; const website = await cache.fetchWebsite(websiteId); const params = [websiteId, website?.revId || 0]; - const { pageviewQuery, sessionQuery } = parseFilters(null, filters, params); + const { filterQuery } = parseFilters(filters, params); return rawQuery( `select ${field} x, count(distinct session_id) y @@ -48,8 +60,7 @@ async function clickhouseQuery(websiteId, { startDate, endDate, field, filters = and rev_id = $2 and event_name = '' and ${getBetweenDates('created_at', startDate, endDate)} - ${pageviewQuery} - ${sessionQuery} + ${filterQuery} group by x order by y desc`, params, diff --git a/queries/analytics/stats/getActiveVisitors.js b/queries/analytics/stats/getActiveVisitors.ts similarity index 83% rename from queries/analytics/stats/getActiveVisitors.js rename to queries/analytics/stats/getActiveVisitors.ts index c7592e5b..0b07574d 100644 --- a/queries/analytics/stats/getActiveVisitors.js +++ b/queries/analytics/stats/getActiveVisitors.ts @@ -3,14 +3,14 @@ import prisma from 'lib/prisma'; import clickhouse from 'lib/clickhouse'; import { runQuery, CLICKHOUSE, PRISMA } from 'lib/db'; -export async function getActiveVisitors(...args) { +export async function getActiveVisitors(...args: [websiteId: string]) { return runQuery({ [PRISMA]: () => relationalQuery(...args), [CLICKHOUSE]: () => clickhouseQuery(...args), }); } -async function relationalQuery(websiteId) { +async function relationalQuery(websiteId: string) { const date = subMinutes(new Date(), 5); const params = [date]; @@ -25,7 +25,7 @@ async function relationalQuery(websiteId) { ); } -async function clickhouseQuery(websiteId) { +async function clickhouseQuery(websiteId: string) { const { rawQuery, getDateFormat } = clickhouse; const params = [websiteId]; diff --git a/queries/analytics/stats/getRealtimeData.js b/queries/analytics/stats/getRealtimeData.ts similarity index 100% rename from queries/analytics/stats/getRealtimeData.js rename to queries/analytics/stats/getRealtimeData.ts diff --git a/queries/analytics/stats/getWebsiteStats.js b/queries/analytics/stats/getWebsiteStats.ts similarity index 70% rename from queries/analytics/stats/getWebsiteStats.js rename to queries/analytics/stats/getWebsiteStats.ts index 002d8a9c..bf5cdd96 100644 --- a/queries/analytics/stats/getWebsiteStats.js +++ b/queries/analytics/stats/getWebsiteStats.ts @@ -3,22 +3,23 @@ import clickhouse from 'lib/clickhouse'; import { runQuery, CLICKHOUSE, PRISMA } from 'lib/db'; import cache from 'lib/cache'; -export async function getWebsiteStats(...args) { +export async function getWebsiteStats( + ...args: [websiteId: string, data: { startDate: Date; endDate: Date; filters: object }] +) { return runQuery({ [PRISMA]: () => relationalQuery(...args), [CLICKHOUSE]: () => clickhouseQuery(...args), }); } -async function relationalQuery(websiteId, { start_at, end_at, filters = {} }) { +async function relationalQuery( + websiteId: string, + data: { startDate: Date; endDate: Date; filters: object }, +) { + const { startDate, endDate, filters = {} } = data; const { getDateQuery, getTimestampInterval, parseFilters, rawQuery } = prisma; - const params = [start_at, end_at]; - const { pageviewQuery, sessionQuery, joinSession } = parseFilters( - 'pageview', - null, - filters, - params, - ); + const params = [startDate, endDate]; + const { filterQuery, joinSession } = parseFilters(filters, params); return rawQuery( `select sum(t.c) as "pageviews", @@ -36,19 +37,22 @@ async function relationalQuery(websiteId, { start_at, end_at, filters = {} }) { ${joinSession} where website.website_id='${websiteId}' and pageview.created_at between $1 and $2 - ${pageviewQuery} - ${sessionQuery} + ${filterQuery} group by 1, 2 ) t`, params, ); } -async function clickhouseQuery(websiteId, { start_at, end_at, filters = {} }) { +async function clickhouseQuery( + websiteId: string, + data: { startDate: Date; endDate: Date; filters: object }, +) { + const { startDate, endDate, filters = {} } = data; const { rawQuery, getDateQuery, getBetweenDates, parseFilters } = clickhouse; const website = await cache.fetchWebsite(websiteId); const params = [websiteId, website?.revId || 0]; - const { pageviewQuery, sessionQuery } = parseFilters(null, filters, params); + const { filterQuery } = parseFilters(filters, params); return rawQuery( `select @@ -66,9 +70,8 @@ async function clickhouseQuery(websiteId, { start_at, end_at, filters = {} }) { where event_name = '' and website_id = $1 and rev_id = $2 - and ${getBetweenDates('created_at', start_at, end_at)} - ${pageviewQuery} - ${sessionQuery} + and ${getBetweenDates('created_at', startDate, endDate)} + ${filterQuery} group by session_id, time_series ) t;`, params, diff --git a/queries/index.js b/queries/index.js index 4cdcedd9..e14c6d84 100644 --- a/queries/index.js +++ b/queries/index.js @@ -1,21 +1,17 @@ -export * from './admin/user/createUser'; -export * from './admin/user/deleteUser'; -export * from './admin/user/getUser'; -export * from './admin/user/getUsers'; -export * from './admin/user/updateUser'; -export * from './admin/website/createWebsite'; -export * from './admin/website/deleteWebsite'; -export * from './admin/website/getAllWebsites'; -export * from './admin/website/getUserWebsites'; -export * from './admin/website/getWebsite'; -export * from './admin/website/resetWebsite'; -export * from './admin/website/updateWebsite'; +export * from './admin/permission'; +export * from './admin/role'; +export * from './admin/team'; +export * from './admin/teamUser'; +export * from './admin/teamWebsite'; +export * from './admin/user'; +export * from './admin/userRole'; +export * from './admin/userWebsite'; +export * from './admin/website'; export * from './analytics/event/getEventMetrics'; export * from './analytics/event/getEvents'; export * from './analytics/event/getEventData'; export * from './analytics/event/saveEvent'; export * from './analytics/pageview/getPageviewMetrics'; -export * from './analytics/pageview/getPageviewParams'; export * from './analytics/pageview/getPageviews'; export * from './analytics/pageview/getPageviewStats'; export * from './analytics/pageview/savePageView'; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..cbb5413f --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "target": "es5", + "outDir": "./build", + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "incremental": true, + "lib": ["dom", "dom.iterable", "esnext"], + "skipLibCheck": true, + "esModuleInterop": true, + "noImplicitAny": false, + "preserveConstEnums": true, + "removeComments": true, + "sourceMap": true, + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "allowJs": true, + "strict": true, + "baseUrl": ".", + "strictNullChecks": false, + "noEmit": true, + "jsx": "preserve" + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "queries/admin/website/getAllWebsites.ts"], + "exclude": ["node_modules"] +}