From 0aaba8cbd1f74664e9b8e0ab52aae326ee146cc3 Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Tue, 15 Nov 2022 13:21:14 -0800 Subject: [PATCH 1/5] Initial Typescript models. --- db/clickhouse/schema.sql | 3 + db/postgresql/schema.prisma | 5 +- interface/api/auth.d.ts | 8 ++ interface/api/models.d.ts | 73 ++++++++++++ interface/api/nextApi.d.ts | 14 +++ interface/auth.d.ts | 5 - interface/base.d.ts | 22 ---- interface/enum.d.ts | 7 ++ interface/index.d.ts | 22 ---- lib/clickhouse.js | 8 +- lib/{prisma.js => prisma.ts} | 74 +++++------- pages/api/auth/{login.js => login.ts} | 20 +++- pages/api/auth/{logout.js => logout.ts} | 3 +- pages/api/auth/verify.js | 8 -- pages/api/auth/verify.ts | 10 ++ pages/api/{collect.js => collect.ts} | 18 ++- pages/api/{config.js => config.ts} | 11 +- pages/api/heartbeat.js | 5 - pages/api/heartbeat.ts | 6 + pages/api/realtime/{init.js => init.ts} | 11 +- pages/api/realtime/{update.js => update.ts} | 12 +- pages/api/share/{[id].js => [id].ts} | 20 +++- pages/api/users/[id]/{index.js => index.ts} | 19 +++- .../users/[id]/{password.js => password.ts} | 17 ++- pages/api/users/{index.js => index.ts} | 14 ++- .../websites/[id]/{active.js => active.ts} | 12 +- .../[id]/{eventdata.js => eventdata.ts} | 21 +++- .../websites/[id]/{events.js => events.ts} | 38 +++++-- .../api/websites/[id]/{index.js => index.ts} | 18 ++- .../websites/[id]/{metrics.js => metrics.ts} | 21 +++- .../[id]/{pageviews.js => pageviews.ts} | 39 +++++-- .../api/websites/[id]/{reset.js => reset.ts} | 11 +- .../api/websites/[id]/{stats.js => stats.ts} | 21 +++- pages/api/websites/{index.js => index.ts} | 17 ++- queries/admin/user/createUser.js | 7 -- queries/admin/user/createUser.ts | 20 ++++ .../user/{deleteUser.js => deleteUser.ts} | 5 +- queries/admin/user/getUser.js | 7 -- queries/admin/user/getUser.ts | 25 +++++ .../admin/user/{getUsers.js => getUsers.ts} | 3 +- queries/admin/user/updateUser.js | 8 -- queries/admin/user/updateUser.ts | 19 ++++ .../{createWebsite.js => createWebsite.ts} | 13 ++- .../{deleteWebsite.js => deleteWebsite.ts} | 13 ++- queries/admin/website/getAllWebsites.js | 23 ---- queries/admin/website/getAllWebsites.ts | 24 ++++ ...{getUserWebsites.js => getUserWebsites.ts} | 3 +- queries/admin/website/getWebsite.js | 7 -- queries/admin/website/getWebsite.ts | 8 ++ .../{resetWebsite.js => resetWebsite.ts} | 15 ++- queries/admin/website/updateWebsite.js | 10 -- queries/admin/website/updateWebsite.ts | 11 ++ .../{getEventData.js => getEventData.ts} | 55 +++++++-- queries/analytics/event/getEventMetrics.js | 68 ------------ queries/analytics/event/getEventMetrics.ts | 105 ++++++++++++++++++ queries/analytics/event/getEvents.js | 45 -------- queries/analytics/event/saveEvent.js | 62 ----------- queries/analytics/event/saveEvent.ts | 91 +++++++++++++++ .../analytics/pageview/getPageviewMetrics.js | 59 ---------- .../analytics/pageview/getPageviewMetrics.ts | 82 ++++++++++++++ .../analytics/pageview/getPageviewParams.js | 41 ------- .../analytics/pageview/getPageviewStats.js | 78 ------------- .../analytics/pageview/getPageviewStats.ts | 102 +++++++++++++++++ queries/analytics/pageview/getPageviews.js | 43 ------- .../{savePageView.js => savePageView.ts} | 32 +++++- .../{createSession.js => createSession.ts} | 21 +++- .../session/{getSession.js => getSession.ts} | 11 +- ...SessionMetrics.js => getSessionMetrics.ts} | 29 +++-- queries/analytics/session/getSessions.js | 52 --------- ...ActiveVisitors.js => getActiveVisitors.ts} | 6 +- queries/analytics/stats/getRealtimeData.js | 30 ----- queries/analytics/stats/getRealtimeData.ts | 3 + ...{getWebsiteStats.js => getWebsiteStats.ts} | 35 +++--- tsconfig.json | 28 +++++ 74 files changed, 1144 insertions(+), 768 deletions(-) create mode 100644 interface/api/auth.d.ts create mode 100644 interface/api/models.d.ts create mode 100644 interface/api/nextApi.d.ts delete mode 100644 interface/auth.d.ts delete mode 100644 interface/base.d.ts create mode 100644 interface/enum.d.ts rename lib/{prisma.js => prisma.ts} (70%) rename pages/api/auth/{login.js => login.ts} (65%) rename pages/api/auth/{logout.js => logout.ts} (74%) delete mode 100644 pages/api/auth/verify.js create mode 100644 pages/api/auth/verify.ts rename pages/api/{collect.js => collect.ts} (83%) rename pages/api/{config.js => config.ts} (57%) delete mode 100644 pages/api/heartbeat.js create mode 100644 pages/api/heartbeat.ts rename pages/api/realtime/{init.js => init.ts} (60%) rename pages/api/realtime/{update.js => update.ts} (64%) rename pages/api/share/{[id].js => [id].ts} (52%) rename pages/api/users/[id]/{index.js => index.ts} (77%) rename pages/api/users/[id]/{password.js => password.ts} (65%) rename pages/api/users/{index.js => index.ts} (70%) rename pages/api/websites/[id]/{active.js => active.ts} (62%) rename pages/api/websites/[id]/{eventdata.js => eventdata.ts} (61%) rename pages/api/websites/[id]/{events.js => events.ts} (53%) rename pages/api/websites/[id]/{index.js => index.ts} (72%) rename pages/api/websites/[id]/{metrics.js => metrics.ts} (85%) rename pages/api/websites/[id]/{pageviews.js => pageviews.ts} (67%) rename pages/api/websites/[id]/{reset.js => reset.ts} (66%) rename pages/api/websites/[id]/{stats.js => stats.ts} (76%) rename pages/api/websites/{index.js => index.ts} (68%) delete mode 100644 queries/admin/user/createUser.js create mode 100644 queries/admin/user/createUser.ts rename queries/admin/user/{deleteUser.js => deleteUser.ts} (83%) delete mode 100644 queries/admin/user/getUser.js create mode 100644 queries/admin/user/getUser.ts rename queries/admin/user/{getUsers.js => getUsers.ts} (76%) delete mode 100644 queries/admin/user/updateUser.js create mode 100644 queries/admin/user/updateUser.ts rename queries/admin/website/{createWebsite.js => createWebsite.ts} (63%) rename queries/admin/website/{deleteWebsite.js => deleteWebsite.ts} (52%) delete mode 100644 queries/admin/website/getAllWebsites.js create mode 100644 queries/admin/website/getAllWebsites.ts rename queries/admin/website/{getUserWebsites.js => getUserWebsites.ts} (59%) delete mode 100644 queries/admin/website/getWebsite.js create mode 100644 queries/admin/website/getWebsite.ts rename queries/admin/website/{resetWebsite.js => resetWebsite.ts} (51%) delete mode 100644 queries/admin/website/updateWebsite.js create mode 100644 queries/admin/website/updateWebsite.ts rename queries/analytics/event/{getEventData.js => getEventData.ts} (54%) delete mode 100644 queries/analytics/event/getEventMetrics.js create mode 100644 queries/analytics/event/getEventMetrics.ts delete mode 100644 queries/analytics/event/getEvents.js delete mode 100644 queries/analytics/event/saveEvent.js create mode 100644 queries/analytics/event/saveEvent.ts delete mode 100644 queries/analytics/pageview/getPageviewMetrics.js create mode 100644 queries/analytics/pageview/getPageviewMetrics.ts delete mode 100644 queries/analytics/pageview/getPageviewParams.js delete mode 100644 queries/analytics/pageview/getPageviewStats.js create mode 100644 queries/analytics/pageview/getPageviewStats.ts delete mode 100644 queries/analytics/pageview/getPageviews.js rename queries/analytics/pageview/{savePageView.js => savePageView.ts} (60%) rename queries/analytics/session/{createSession.js => createSession.ts} (63%) rename queries/analytics/session/{getSession.js => getSession.ts} (64%) rename queries/analytics/session/{getSessionMetrics.js => getSessionMetrics.ts} (63%) delete mode 100644 queries/analytics/session/getSessions.js rename queries/analytics/stats/{getActiveVisitors.js => getActiveVisitors.ts} (83%) delete mode 100644 queries/analytics/stats/getRealtimeData.js create mode 100644 queries/analytics/stats/getRealtimeData.ts rename queries/analytics/stats/{getWebsiteStats.js => getWebsiteStats.ts} (70%) create mode 100644 tsconfig.json diff --git a/db/clickhouse/schema.sql b/db/clickhouse/schema.sql index 0579d13d..64d31c04 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, event_data, created_at diff --git a/db/postgresql/schema.prisma b/db/postgresql/schema.prisma index 58b28a8b..b2cb4662 100644 --- a/db/postgresql/schema.prisma +++ b/db/postgresql/schema.prisma @@ -69,8 +69,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]) 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/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..870ff68a 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 UserReqeustQuery { + id: string; +} + +export interface UserReqeustBody { + 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..6b942287 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..834b1732 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 WebsiteReqeustQuery { + id: string; +} + +export interface WebsiteReqeustBody { + 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..f3bf38aa 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 WebsiteMetricsReqeustQuery { + 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..c85a1a97 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 WebsitePageviewReqeustQuery { + 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..6d2ffcea 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 WebsiteResetReqeustQuery { + 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 76% rename from pages/api/websites/[id]/stats.js rename to pages/api/websites/[id]/stats.ts index 2c5b0156..0ba65bf2 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 WebsiteStatsReqeustQuery { + 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/index.js b/pages/api/websites/index.ts similarity index 68% rename from pages/api/websites/index.js rename to pages/api/websites/index.ts index 3966ad70..1b0b0586 100644 --- a/pages/api/websites/index.js +++ b/pages/api/websites/index.ts @@ -2,8 +2,23 @@ import { createWebsite, getAllWebsites, getUserWebsites } from 'queries'; import { ok, methodNotAllowed, getRandomChars } from 'next-basics'; import { useAuth, useCors } from 'lib/middleware'; import { uuid } from 'lib/crypto'; +import { NextApiRequestQueryBody } from 'interface/api/nextApi'; +import { NextApiResponse } from 'next'; -export default async (req, res) => { +export interface WebsitesReqeustQuery { + include_all?: boolean; +} + +export interface WebsitesReqeustBody { + name: string; + domain: string; + enableShareUrl: boolean; +} + +export default async ( + req: NextApiRequestQueryBody, + res: NextApiResponse, +) => { await useCors(req, res); await useAuth(req, res); 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/createUser.ts b/queries/admin/user/createUser.ts new file mode 100644 index 00000000..c1604044 --- /dev/null +++ b/queries/admin/user/createUser.ts @@ -0,0 +1,20 @@ +import prisma from 'lib/prisma'; + +export async function createUser(data: { + id: string; + username: string; + password: string; +}): Promise<{ + id: string; + username: string; + isAdmin: boolean; +}> { + return prisma.client.user.create({ + data, + select: { + id: true, + username: true, + isAdmin: true, + }, + }); +} diff --git a/queries/admin/user/deleteUser.js b/queries/admin/user/deleteUser.ts similarity index 83% rename from queries/admin/user/deleteUser.js rename to queries/admin/user/deleteUser.ts index 5970d2a5..bb556225 100644 --- a/queries/admin/user/deleteUser.js +++ b/queries/admin/user/deleteUser.ts @@ -1,7 +1,10 @@ import prisma from 'lib/prisma'; import cache from 'lib/cache'; +import { Prisma, User } from '@prisma/client'; -export async function deleteUser(userId) { +export async function deleteUser( + userId: string, +): Promise<[Prisma.BatchPayload, Prisma.BatchPayload, Prisma.BatchPayload, User]> { const { client } = prisma; const websites = await client.website.findMany({ 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/getUser.ts b/queries/admin/user/getUser.ts new file mode 100644 index 00000000..c353886e --- /dev/null +++ b/queries/admin/user/getUser.ts @@ -0,0 +1,25 @@ +import prisma from 'lib/prisma'; +import { Prisma } from '@prisma/client'; + +export interface User { + id: string; + username: string; + isAdmin: boolean; + password?: string; + createdAt?: Date; +} + +export async function getUser( + where: Prisma.UserWhereUniqueInput, + includePassword = false, +): Promise { + return prisma.client.user.findUnique({ + where, + select: { + id: true, + username: true, + isAdmin: true, + password: includePassword, + }, + }); +} diff --git a/queries/admin/user/getUsers.js b/queries/admin/user/getUsers.ts similarity index 76% rename from queries/admin/user/getUsers.js rename to queries/admin/user/getUsers.ts index 2fc473cb..e196d232 100644 --- a/queries/admin/user/getUsers.js +++ b/queries/admin/user/getUsers.ts @@ -1,6 +1,7 @@ import prisma from 'lib/prisma'; +import { User } from './getUser'; -export async function getUsers() { +export async function getUsers(): Promise { return prisma.client.user.findMany({ orderBy: [ { isAdmin: 'desc' }, 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/user/updateUser.ts b/queries/admin/user/updateUser.ts new file mode 100644 index 00000000..f5a64e51 --- /dev/null +++ b/queries/admin/user/updateUser.ts @@ -0,0 +1,19 @@ +import { Prisma } from '@prisma/client'; +import prisma from 'lib/prisma'; +import { User } from './getUser'; + +export async function updateUser( + data: Prisma.UserUpdateArgs, + where: Prisma.UserWhereUniqueInput, +): Promise { + return prisma.client.user.update({ + where, + data, + select: { + id: true, + username: true, + isAdmin: true, + createdAt: true, + }, + }); +} diff --git a/queries/admin/website/createWebsite.js b/queries/admin/website/createWebsite.ts similarity index 63% rename from queries/admin/website/createWebsite.js rename to queries/admin/website/createWebsite.ts index 0afe0bea..51aa2e3f 100644 --- a/queries/admin/website/createWebsite.js +++ b/queries/admin/website/createWebsite.ts @@ -1,7 +1,16 @@ -import prisma from 'lib/prisma'; +import { Website } from '@prisma/client'; import cache from 'lib/cache'; +import prisma from 'lib/prisma'; -export async function createWebsite(userId, data) { +export async function createWebsite( + userId: string, + data: { + id: string; + name: string; + domain: string; + shareId?: string; + }, +): Promise { return prisma.client.website .create({ data: { diff --git a/queries/admin/website/deleteWebsite.js b/queries/admin/website/deleteWebsite.ts similarity index 52% rename from queries/admin/website/deleteWebsite.js rename to queries/admin/website/deleteWebsite.ts index 685cee8a..a3c57e67 100644 --- a/queries/admin/website/deleteWebsite.js +++ b/queries/admin/website/deleteWebsite.ts @@ -1,22 +1,25 @@ import prisma from 'lib/prisma'; import cache from 'lib/cache'; +import { Prisma, Website } from '@prisma/client'; -export async function deleteWebsite(id) { +export async function deleteWebsite( + websiteId: string, +): Promise<[Prisma.BatchPayload, Prisma.BatchPayload, Website]> { const { client, transaction } = prisma; return transaction([ client.websiteEvent.deleteMany({ - where: { websiteId: id }, + where: { websiteId }, }), client.session.deleteMany({ - where: { websiteId: id }, + where: { websiteId }, }), client.website.delete({ - where: { id }, + where: { id: websiteId }, }), ]).then(async data => { if (cache.enabled) { - await cache.deleteWebsite(id); + await cache.deleteWebsite(websiteId); } 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/getAllWebsites.ts b/queries/admin/website/getAllWebsites.ts new file mode 100644 index 00000000..1e68693a --- /dev/null +++ b/queries/admin/website/getAllWebsites.ts @@ -0,0 +1,24 @@ +import { Website } from '@prisma/client'; +import prisma from 'lib/prisma'; + +export async function getAllWebsites(): Promise<(Website & { user: string })[]> { + return await prisma.client.website + .findMany({ + orderBy: [ + { + userId: 'asc', + }, + { + name: 'asc', + }, + ], + include: { + user: { + select: { + username: true, + }, + }, + }, + }) + .then(data => data.map(i => ({ ...i, user: i.user.username }))); +} diff --git a/queries/admin/website/getUserWebsites.js b/queries/admin/website/getUserWebsites.ts similarity index 59% rename from queries/admin/website/getUserWebsites.js rename to queries/admin/website/getUserWebsites.ts index c1a9d559..f4fa27b9 100644 --- a/queries/admin/website/getUserWebsites.js +++ b/queries/admin/website/getUserWebsites.ts @@ -1,6 +1,7 @@ import prisma from 'lib/prisma'; +import { Website } from '@prisma/client'; -export async function getUserWebsites(userId) { +export async function getUserWebsites(userId): Promise { return prisma.client.website.findMany({ where: { userId, 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/getWebsite.ts b/queries/admin/website/getWebsite.ts new file mode 100644 index 00000000..9ec27cb9 --- /dev/null +++ b/queries/admin/website/getWebsite.ts @@ -0,0 +1,8 @@ +import prisma from 'lib/prisma'; +import { Prisma, Website } from '@prisma/client'; + +export async function getWebsite(where: Prisma.WebsiteWhereUniqueInput): Promise { + return prisma.client.website.findUnique({ + where, + }); +} diff --git a/queries/admin/website/resetWebsite.js b/queries/admin/website/resetWebsite.ts similarity index 51% rename from queries/admin/website/resetWebsite.js rename to queries/admin/website/resetWebsite.ts index f4d685cb..05116f8a 100644 --- a/queries/admin/website/resetWebsite.js +++ b/queries/admin/website/resetWebsite.ts @@ -1,20 +1,23 @@ +import { Prisma, Website } from '@prisma/client'; +import cache from 'lib/cache'; import prisma from 'lib/prisma'; import { getWebsite } from 'queries'; -import cache from 'lib/cache'; -export async function resetWebsite(id) { +export async function resetWebsite( + websiteId, +): Promise<[Prisma.BatchPayload, Prisma.BatchPayload, Website]> { const { client, transaction } = prisma; - const { revId } = await getWebsite({ id }); + const { revId } = await getWebsite({ id: websiteId }); return transaction([ client.websiteEvent.deleteMany({ - where: { websiteId: id }, + where: { websiteId }, }), client.session.deleteMany({ - where: { websiteId: id }, + where: { websiteId }, }), - client.website.update({ where: { id }, data: { revId: revId + 1 } }), + client.website.update({ where: { id: websiteId }, data: { revId: revId + 1 } }), ]).then(async data => { if (cache.enabled) { await cache.storeWebsite(data[2]); 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/admin/website/updateWebsite.ts b/queries/admin/website/updateWebsite.ts new file mode 100644 index 00000000..51787222 --- /dev/null +++ b/queries/admin/website/updateWebsite.ts @@ -0,0 +1,11 @@ +import { Prisma, Website } from '@prisma/client'; +import prisma from 'lib/prisma'; + +export async function updateWebsite(websiteId, data: Prisma.WebsiteUpdateInput): Promise { + return prisma.client.website.update({ + where: { + id: websiteId, + }, + 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/getEvents.js b/queries/analytics/event/getEvents.js deleted file mode 100644 index 81a187ce..00000000 --- a/queries/analytics/event/getEvents.js +++ /dev/null @@ -1,45 +0,0 @@ -import prisma from 'lib/prisma'; -import clickhouse from 'lib/clickhouse'; -import { runQuery, CLICKHOUSE, PRISMA } from 'lib/db'; - -export function getEvents(...args) { - return runQuery({ - [PRISMA]: () => relationalQuery(...args), - [CLICKHOUSE]: () => clickhouseQuery(...args), - }); -} - -function relationalQuery(websites, start_at) { - return prisma.client.event.findMany({ - where: { - websiteId: { - in: websites, - }, - createdAt: { - gte: start_at, - }, - }, - }); -} - -function clickhouseQuery(websites, start_at) { - const { rawQuery, getDateFormat, getCommaSeparatedStringFormat } = clickhouse; - - return rawQuery( - `select - event_id, - website_id, - session_id, - created_at, - url, - event_name - from event - where event_name != '' - and ${ - websites && websites.length > 0 - ? `website_id in (${getCommaSeparatedStringFormat(websites)})` - : '0 = 0' - } - and created_at >= ${getDateFormat(start_at)}`, - ); -} 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/getPageviews.js b/queries/analytics/pageview/getPageviews.js deleted file mode 100644 index 2bf41b0b..00000000 --- a/queries/analytics/pageview/getPageviews.js +++ /dev/null @@ -1,43 +0,0 @@ -import prisma from 'lib/prisma'; -import clickhouse from 'lib/clickhouse'; -import { runQuery, CLICKHOUSE, PRISMA } from 'lib/db'; - -export async function getPageviews(...args) { - return runQuery({ - [PRISMA]: () => relationalQuery(...args), - [CLICKHOUSE]: () => clickhouseQuery(...args), - }); -} - -async function relationalQuery(websites, start_at) { - return prisma.client.pageview.findMany({ - where: { - websiteId: { - in: websites, - }, - createdAt: { - gte: start_at, - }, - }, - }); -} - -async function clickhouseQuery(websites, start_at) { - const { rawQuery, getCommaSeparatedStringFormat } = clickhouse; - - return rawQuery( - `select - website_id, - session_id, - created_at, - url - from event - where event_name = '' - and ${ - websites && websites.length > 0 - ? `website_id in (${getCommaSeparatedStringFormat(websites)})` - : '0 = 0' - } - and created_at >= ${clickhouse.getDateFormat(start_at)}`, - ); -} 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 6e74e9b6..77bc5a63 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(*) 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/session/getSessions.js b/queries/analytics/session/getSessions.js deleted file mode 100644 index c5fed485..00000000 --- a/queries/analytics/session/getSessions.js +++ /dev/null @@ -1,52 +0,0 @@ -import prisma from 'lib/prisma'; -import clickhouse from 'lib/clickhouse'; -import { runQuery, PRISMA, CLICKHOUSE } from 'lib/db'; - -export async function getSessions(...args) { - return runQuery({ - [PRISMA]: () => relationalQuery(...args), - [CLICKHOUSE]: () => clickhouseQuery(...args), - }); -} - -async function relationalQuery(websites, start_at) { - return prisma.client.session.findMany({ - where: { - ...(websites && websites.length > 0 - ? { - websiteId: { - in: websites, - }, - } - : {}), - createdAt: { - gte: start_at, - }, - }, - }); -} - -async function clickhouseQuery(websites, start_at) { - const { rawQuery, getDateFormat, getCommaSeparatedStringFormat } = clickhouse; - - return rawQuery( - `select distinct - session_id, - website_id, - created_at, - hostname, - browser, - os, - device, - screen, - language, - country - from event - where ${ - websites && websites.length > 0 - ? `website_id in (${getCommaSeparatedStringFormat(websites)})` - : '0 = 0' - } - and created_at >= ${getDateFormat(start_at)}`, - ); -} 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.js deleted file mode 100644 index 659f6145..00000000 --- a/queries/analytics/stats/getRealtimeData.js +++ /dev/null @@ -1,30 +0,0 @@ -import { getPageviews } from '../pageview/getPageviews'; -import { getSessions } from '../session/getSessions'; -import { getEvents } from '../event/getEvents'; - -export async function getRealtimeData(websites, time) { - const [pageviews, sessions, events] = await Promise.all([ - getPageviews(websites, time), - getSessions(websites, time), - getEvents(websites, time), - ]); - - return { - pageviews: pageviews.map(({ id, ...props }) => ({ - __id: `p${id}`, - pageviewId: id, - ...props, - })), - sessions: sessions.map(({ id, ...props }) => ({ - __id: `s${id}`, - sessionId: id, - ...props, - })), - events: events.map(({ id, ...props }) => ({ - __id: `e${id}`, - eventId: id, - ...props, - })), - timestamp: Date.now(), - }; -} diff --git a/queries/analytics/stats/getRealtimeData.ts b/queries/analytics/stats/getRealtimeData.ts new file mode 100644 index 00000000..042bcc46 --- /dev/null +++ b/queries/analytics/stats/getRealtimeData.ts @@ -0,0 +1,3 @@ +export async function getRealtimeData() { + throw new Error('Not Implemented'); +} 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/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"] +} From f3726e5abf313b32d3a625591486a628b4588004 Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Wed, 16 Nov 2022 10:32:02 -0800 Subject: [PATCH 2/5] Re-add realtime data --- db/postgresql/schema.prisma | 7 +-- queries/analytics/event/getEvents.js | 45 +++++++++++++++++++ queries/analytics/pageview/getPageviews.js | 43 ++++++++++++++++++ queries/analytics/session/getSessions.js | 52 ++++++++++++++++++++++ queries/analytics/stats/getRealtimeData.ts | 31 ++++++++++++- 5 files changed, 170 insertions(+), 8 deletions(-) create mode 100644 queries/analytics/event/getEvents.js create mode 100644 queries/analytics/pageview/getPageviews.js create mode 100644 queries/analytics/session/getSessions.js diff --git a/db/postgresql/schema.prisma b/db/postgresql/schema.prisma index b2cb4662..b6a7610a 100644 --- a/db/postgresql/schema.prisma +++ b/db/postgresql/schema.prisma @@ -11,7 +11,6 @@ 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) groupRole GroupRole[] @@ -20,7 +19,6 @@ model User { teamWebsite TeamWebsite[] teamUser TeamUser[] userWebsite UserWebsite[] - website Website[] @@map("user") } @@ -44,7 +42,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 +49,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") @@ -117,7 +112,7 @@ 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") } diff --git a/queries/analytics/event/getEvents.js b/queries/analytics/event/getEvents.js new file mode 100644 index 00000000..81a187ce --- /dev/null +++ b/queries/analytics/event/getEvents.js @@ -0,0 +1,45 @@ +import prisma from 'lib/prisma'; +import clickhouse from 'lib/clickhouse'; +import { runQuery, CLICKHOUSE, PRISMA } from 'lib/db'; + +export function getEvents(...args) { + return runQuery({ + [PRISMA]: () => relationalQuery(...args), + [CLICKHOUSE]: () => clickhouseQuery(...args), + }); +} + +function relationalQuery(websites, start_at) { + return prisma.client.event.findMany({ + where: { + websiteId: { + in: websites, + }, + createdAt: { + gte: start_at, + }, + }, + }); +} + +function clickhouseQuery(websites, start_at) { + const { rawQuery, getDateFormat, getCommaSeparatedStringFormat } = clickhouse; + + return rawQuery( + `select + event_id, + website_id, + session_id, + created_at, + url, + event_name + from event + where event_name != '' + and ${ + websites && websites.length > 0 + ? `website_id in (${getCommaSeparatedStringFormat(websites)})` + : '0 = 0' + } + and created_at >= ${getDateFormat(start_at)}`, + ); +} diff --git a/queries/analytics/pageview/getPageviews.js b/queries/analytics/pageview/getPageviews.js new file mode 100644 index 00000000..2bf41b0b --- /dev/null +++ b/queries/analytics/pageview/getPageviews.js @@ -0,0 +1,43 @@ +import prisma from 'lib/prisma'; +import clickhouse from 'lib/clickhouse'; +import { runQuery, CLICKHOUSE, PRISMA } from 'lib/db'; + +export async function getPageviews(...args) { + return runQuery({ + [PRISMA]: () => relationalQuery(...args), + [CLICKHOUSE]: () => clickhouseQuery(...args), + }); +} + +async function relationalQuery(websites, start_at) { + return prisma.client.pageview.findMany({ + where: { + websiteId: { + in: websites, + }, + createdAt: { + gte: start_at, + }, + }, + }); +} + +async function clickhouseQuery(websites, start_at) { + const { rawQuery, getCommaSeparatedStringFormat } = clickhouse; + + return rawQuery( + `select + website_id, + session_id, + created_at, + url + from event + where event_name = '' + and ${ + websites && websites.length > 0 + ? `website_id in (${getCommaSeparatedStringFormat(websites)})` + : '0 = 0' + } + and created_at >= ${clickhouse.getDateFormat(start_at)}`, + ); +} diff --git a/queries/analytics/session/getSessions.js b/queries/analytics/session/getSessions.js new file mode 100644 index 00000000..c5fed485 --- /dev/null +++ b/queries/analytics/session/getSessions.js @@ -0,0 +1,52 @@ +import prisma from 'lib/prisma'; +import clickhouse from 'lib/clickhouse'; +import { runQuery, PRISMA, CLICKHOUSE } from 'lib/db'; + +export async function getSessions(...args) { + return runQuery({ + [PRISMA]: () => relationalQuery(...args), + [CLICKHOUSE]: () => clickhouseQuery(...args), + }); +} + +async function relationalQuery(websites, start_at) { + return prisma.client.session.findMany({ + where: { + ...(websites && websites.length > 0 + ? { + websiteId: { + in: websites, + }, + } + : {}), + createdAt: { + gte: start_at, + }, + }, + }); +} + +async function clickhouseQuery(websites, start_at) { + const { rawQuery, getDateFormat, getCommaSeparatedStringFormat } = clickhouse; + + return rawQuery( + `select distinct + session_id, + website_id, + created_at, + hostname, + browser, + os, + device, + screen, + language, + country + from event + where ${ + websites && websites.length > 0 + ? `website_id in (${getCommaSeparatedStringFormat(websites)})` + : '0 = 0' + } + and created_at >= ${getDateFormat(start_at)}`, + ); +} diff --git a/queries/analytics/stats/getRealtimeData.ts b/queries/analytics/stats/getRealtimeData.ts index 042bcc46..659f6145 100644 --- a/queries/analytics/stats/getRealtimeData.ts +++ b/queries/analytics/stats/getRealtimeData.ts @@ -1,3 +1,30 @@ -export async function getRealtimeData() { - throw new Error('Not Implemented'); +import { getPageviews } from '../pageview/getPageviews'; +import { getSessions } from '../session/getSessions'; +import { getEvents } from '../event/getEvents'; + +export async function getRealtimeData(websites, time) { + const [pageviews, sessions, events] = await Promise.all([ + getPageviews(websites, time), + getSessions(websites, time), + getEvents(websites, time), + ]); + + return { + pageviews: pageviews.map(({ id, ...props }) => ({ + __id: `p${id}`, + pageviewId: id, + ...props, + })), + sessions: sessions.map(({ id, ...props }) => ({ + __id: `s${id}`, + sessionId: id, + ...props, + })), + events: events.map(({ id, ...props }) => ({ + __id: `e${id}`, + eventId: id, + ...props, + })), + timestamp: Date.now(), + }; } From 5aa8187e422e345a1a5006f0775351e2a2b59310 Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Thu, 17 Nov 2022 22:27:33 -0800 Subject: [PATCH 3/5] Add queries for new schema. --- db/postgresql/schema.prisma | 39 +++-- pages/api/websites/[id]/stats.ts | 8 +- pages/api/websites/index.ts | 12 +- queries/admin/permission.ts | 41 ++++++ queries/admin/role.ts | 57 ++++++++ queries/admin/team.ts | 56 ++++++++ queries/admin/teamUser.ts | 41 ++++++ queries/admin/teamWebsite.ts | 43 ++++++ queries/admin/user.ts | 152 ++++++++++++++++++++ queries/admin/user/createUser.ts | 20 --- queries/admin/user/deleteUser.ts | 48 ------- queries/admin/user/getUser.ts | 25 ---- queries/admin/user/getUsers.ts | 19 --- queries/admin/user/updateUser.ts | 19 --- queries/admin/userRole.ts | 41 ++++++ queries/admin/userWebsite.ts | 43 ++++++ queries/admin/website.ts | 176 +++++++++++++++++++++++ queries/admin/website/createWebsite.ts | 32 ----- queries/admin/website/deleteWebsite.ts | 27 ---- queries/admin/website/getAllWebsites.ts | 24 ---- queries/admin/website/getUserWebsites.ts | 13 -- queries/admin/website/getWebsite.ts | 8 -- queries/admin/website/resetWebsite.ts | 28 ---- queries/admin/website/updateWebsite.ts | 11 -- queries/index.js | 22 ++- 25 files changed, 699 insertions(+), 306 deletions(-) create mode 100644 queries/admin/permission.ts create mode 100644 queries/admin/role.ts create mode 100644 queries/admin/team.ts create mode 100644 queries/admin/teamUser.ts create mode 100644 queries/admin/teamWebsite.ts create mode 100644 queries/admin/user.ts delete mode 100644 queries/admin/user/createUser.ts delete mode 100644 queries/admin/user/deleteUser.ts delete mode 100644 queries/admin/user/getUser.ts delete mode 100644 queries/admin/user/getUsers.ts delete mode 100644 queries/admin/user/updateUser.ts create mode 100644 queries/admin/userRole.ts create mode 100644 queries/admin/userWebsite.ts create mode 100644 queries/admin/website.ts delete mode 100644 queries/admin/website/createWebsite.ts delete mode 100644 queries/admin/website/deleteWebsite.ts delete mode 100644 queries/admin/website/getAllWebsites.ts delete mode 100644 queries/admin/website/getUserWebsites.ts delete mode 100644 queries/admin/website/getWebsite.ts delete mode 100644 queries/admin/website/resetWebsite.ts delete mode 100644 queries/admin/website/updateWebsite.ts diff --git a/db/postgresql/schema.prisma b/db/postgresql/schema.prisma index b6a7610a..538a1fa8 100644 --- a/db/postgresql/schema.prisma +++ b/db/postgresql/schema.prisma @@ -12,6 +12,7 @@ model User { username String @unique @db.VarChar(255) password String @db.VarChar(60) createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) + isDeleted Boolean @default(false) @map("is_deleted") groupRole GroupRole[] groupUser GroupUser[] @@ -118,11 +119,12 @@ model GroupUser { } 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") } @@ -134,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") } @@ -161,6 +179,7 @@ model Team { teamWebsites TeamWebsite[] teamUsers TeamUser[] + UserRole UserRole[] @@map("team") } @@ -184,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") @@ -198,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/pages/api/websites/[id]/stats.ts b/pages/api/websites/[id]/stats.ts index 0ba65bf2..d9a581a9 100644 --- a/pages/api/websites/[id]/stats.ts +++ b/pages/api/websites/[id]/stats.ts @@ -52,8 +52,8 @@ export default async ( const prevEndDate = new Date(+end_at - distance); const metrics = await getWebsiteStats(websiteId, { - start_at: startDate, - end_at: endDate, + startDate, + endDate, filters: { url, referrer, @@ -64,8 +64,8 @@ export default async ( }, }); 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.ts b/pages/api/websites/index.ts index 1b0b0586..c4f2b3ea 100644 --- a/pages/api/websites/index.ts +++ b/pages/api/websites/index.ts @@ -1,9 +1,9 @@ -import { createWebsite, getAllWebsites, getUserWebsites } from 'queries'; -import { ok, methodNotAllowed, getRandomChars } from 'next-basics'; -import { useAuth, useCors } from 'lib/middleware'; -import { uuid } from 'lib/crypto'; 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 WebsitesReqeustQuery { include_all?: boolean; @@ -30,7 +30,7 @@ export default async ( const { include_all } = req.query; const websites = - isAdmin && include_all ? await getAllWebsites() : await getUserWebsites(userId); + isAdmin && include_all ? await getAllWebsites() : await getWebsitesByUserId(userId); return ok(res, websites); } @@ -39,7 +39,7 @@ export default async ( const { name, domain, enableShareUrl } = req.body; const shareId = enableShareUrl ? getRandomChars(8) : null; - const website = await createWebsite(userId, { id: uuid(), name, domain, shareId }); + const website = await createWebsiteByUser(userId, { id: uuid(), name, domain, shareId }); return ok(res, website); } 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..71b5f807 --- /dev/null +++ b/queries/admin/team.ts @@ -0,0 +1,56 @@ +import { Prisma, Role, Team, TeamUser } from '@prisma/client'; +import prisma from 'lib/prisma'; + +export async function createTeam(data: Prisma.RoleCreateInput): Promise { + return prisma.client.role.create({ + data, + }); +} + +export async function getTeam(where: Prisma.RoleWhereUniqueInput): Promise { + return prisma.client.role.findUnique({ + where, + }); +} + +export async function getTeams(where: Prisma.RoleWhereInput): 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.RoleUpdateInput, + where: Prisma.RoleWhereUniqueInput, +): 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..e110efcf --- /dev/null +++ b/queries/admin/teamUser.ts @@ -0,0 +1,41 @@ +import { Prisma, TeamUser } from '@prisma/client'; +import prisma from 'lib/prisma'; + +export async function createTeamUser(data: Prisma.TeamUserCreateInput): 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..950a7026 --- /dev/null +++ b/queries/admin/teamWebsite.ts @@ -0,0 +1,43 @@ +import { Prisma, TeamWebsite } from '@prisma/client'; +import prisma from 'lib/prisma'; + +export async function createTeamWebsite(data: Prisma.TeamWebsiteCreateInput): 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.ts b/queries/admin/user/createUser.ts deleted file mode 100644 index c1604044..00000000 --- a/queries/admin/user/createUser.ts +++ /dev/null @@ -1,20 +0,0 @@ -import prisma from 'lib/prisma'; - -export async function createUser(data: { - id: string; - username: string; - password: string; -}): Promise<{ - id: string; - username: string; - isAdmin: boolean; -}> { - return prisma.client.user.create({ - data, - select: { - id: true, - username: true, - isAdmin: true, - }, - }); -} diff --git a/queries/admin/user/deleteUser.ts b/queries/admin/user/deleteUser.ts deleted file mode 100644 index bb556225..00000000 --- a/queries/admin/user/deleteUser.ts +++ /dev/null @@ -1,48 +0,0 @@ -import prisma from 'lib/prisma'; -import cache from 'lib/cache'; -import { Prisma, User } from '@prisma/client'; - -export async function deleteUser( - userId: string, -): Promise<[Prisma.BatchPayload, Prisma.BatchPayload, Prisma.BatchPayload, User]> { - 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.ts b/queries/admin/user/getUser.ts deleted file mode 100644 index c353886e..00000000 --- a/queries/admin/user/getUser.ts +++ /dev/null @@ -1,25 +0,0 @@ -import prisma from 'lib/prisma'; -import { Prisma } from '@prisma/client'; - -export interface User { - id: string; - username: string; - isAdmin: boolean; - password?: string; - createdAt?: Date; -} - -export async function getUser( - where: Prisma.UserWhereUniqueInput, - includePassword = false, -): Promise { - return prisma.client.user.findUnique({ - where, - select: { - id: true, - username: true, - isAdmin: true, - password: includePassword, - }, - }); -} diff --git a/queries/admin/user/getUsers.ts b/queries/admin/user/getUsers.ts deleted file mode 100644 index e196d232..00000000 --- a/queries/admin/user/getUsers.ts +++ /dev/null @@ -1,19 +0,0 @@ -import prisma from 'lib/prisma'; -import { User } from './getUser'; - -export async function getUsers(): Promise { - 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.ts b/queries/admin/user/updateUser.ts deleted file mode 100644 index f5a64e51..00000000 --- a/queries/admin/user/updateUser.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Prisma } from '@prisma/client'; -import prisma from 'lib/prisma'; -import { User } from './getUser'; - -export async function updateUser( - data: Prisma.UserUpdateArgs, - where: Prisma.UserWhereUniqueInput, -): Promise { - return prisma.client.user.update({ - where, - data, - select: { - id: true, - username: true, - isAdmin: true, - createdAt: true, - }, - }); -} diff --git a/queries/admin/userRole.ts b/queries/admin/userRole.ts new file mode 100644 index 00000000..c4e365ac --- /dev/null +++ b/queries/admin/userRole.ts @@ -0,0 +1,41 @@ +import { Prisma, UserRole } from '@prisma/client'; +import prisma from 'lib/prisma'; + +export async function createUserRole(data: Prisma.UserRoleCreateInput): 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..313d6cd4 --- /dev/null +++ b/queries/admin/userWebsite.ts @@ -0,0 +1,43 @@ +import { Prisma, UserWebsite } from '@prisma/client'; +import prisma from 'lib/prisma'; + +export async function createUserWebsite(data: Prisma.UserWebsiteCreateInput): 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.ts b/queries/admin/website/createWebsite.ts deleted file mode 100644 index 51aa2e3f..00000000 --- a/queries/admin/website/createWebsite.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { Website } from '@prisma/client'; -import cache from 'lib/cache'; -import prisma from 'lib/prisma'; - -export async function createWebsite( - userId: string, - data: { - id: string; - name: string; - domain: string; - shareId?: string; - }, -): Promise { - 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.ts b/queries/admin/website/deleteWebsite.ts deleted file mode 100644 index a3c57e67..00000000 --- a/queries/admin/website/deleteWebsite.ts +++ /dev/null @@ -1,27 +0,0 @@ -import prisma from 'lib/prisma'; -import cache from 'lib/cache'; -import { Prisma, Website } from '@prisma/client'; - -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.delete({ - where: { id: websiteId }, - }), - ]).then(async data => { - if (cache.enabled) { - await cache.deleteWebsite(websiteId); - } - - return data; - }); -} diff --git a/queries/admin/website/getAllWebsites.ts b/queries/admin/website/getAllWebsites.ts deleted file mode 100644 index 1e68693a..00000000 --- a/queries/admin/website/getAllWebsites.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Website } from '@prisma/client'; -import prisma from 'lib/prisma'; - -export async function getAllWebsites(): Promise<(Website & { user: string })[]> { - return await prisma.client.website - .findMany({ - orderBy: [ - { - userId: 'asc', - }, - { - name: 'asc', - }, - ], - include: { - user: { - select: { - username: true, - }, - }, - }, - }) - .then(data => data.map(i => ({ ...i, user: i.user.username }))); -} diff --git a/queries/admin/website/getUserWebsites.ts b/queries/admin/website/getUserWebsites.ts deleted file mode 100644 index f4fa27b9..00000000 --- a/queries/admin/website/getUserWebsites.ts +++ /dev/null @@ -1,13 +0,0 @@ -import prisma from 'lib/prisma'; -import { Website } from '@prisma/client'; - -export async function getUserWebsites(userId): Promise { - return prisma.client.website.findMany({ - where: { - userId, - }, - orderBy: { - name: 'asc', - }, - }); -} diff --git a/queries/admin/website/getWebsite.ts b/queries/admin/website/getWebsite.ts deleted file mode 100644 index 9ec27cb9..00000000 --- a/queries/admin/website/getWebsite.ts +++ /dev/null @@ -1,8 +0,0 @@ -import prisma from 'lib/prisma'; -import { Prisma, Website } from '@prisma/client'; - -export async function getWebsite(where: Prisma.WebsiteWhereUniqueInput): Promise { - return prisma.client.website.findUnique({ - where, - }); -} diff --git a/queries/admin/website/resetWebsite.ts b/queries/admin/website/resetWebsite.ts deleted file mode 100644 index 05116f8a..00000000 --- a/queries/admin/website/resetWebsite.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { Prisma, Website } from '@prisma/client'; -import cache from 'lib/cache'; -import prisma from 'lib/prisma'; -import { getWebsite } from 'queries'; - -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; - }); -} diff --git a/queries/admin/website/updateWebsite.ts b/queries/admin/website/updateWebsite.ts deleted file mode 100644 index 51787222..00000000 --- a/queries/admin/website/updateWebsite.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Prisma, Website } from '@prisma/client'; -import prisma from 'lib/prisma'; - -export async function updateWebsite(websiteId, data: Prisma.WebsiteUpdateInput): Promise { - return prisma.client.website.update({ - where: { - id: websiteId, - }, - data, - }); -} 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'; From f5eb974d8d958c800596ff5ae6dfffa8d6a1e3e8 Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Thu, 17 Nov 2022 22:46:05 -0800 Subject: [PATCH 4/5] Fix Typo. --- pages/api/teams/[id]/index.ts | 87 ++++++++++++++++++++++++++++ pages/api/teams/index.ts | 0 pages/api/users/[id]/index.ts | 6 +- pages/api/websites/[id]/index.ts | 6 +- pages/api/websites/[id]/metrics.ts | 4 +- pages/api/websites/[id]/pageviews.ts | 4 +- pages/api/websites/[id]/reset.ts | 4 +- pages/api/websites/[id]/stats.ts | 4 +- pages/api/websites/index.ts | 6 +- 9 files changed, 104 insertions(+), 17 deletions(-) create mode 100644 pages/api/teams/[id]/index.ts create mode 100644 pages/api/teams/index.ts diff --git a/pages/api/teams/[id]/index.ts b/pages/api/teams/[id]/index.ts new file mode 100644 index 00000000..26440c8b --- /dev/null +++ b/pages/api/teams/[id]/index.ts @@ -0,0 +1,87 @@ +import { badRequest, hashPassword, methodNotAllowed, ok, unauthorized } from 'next-basics'; +import { getTeam, deleteTeam, updateTeam } from 'queries'; +import { useAuth } from 'lib/middleware'; +import { NextApiResponse } from 'next'; +import { NextApiRequestQueryBody } from 'interface/api/nextApi'; +import { Team } from '@prisma/client'; + +export interface TeamRequestQuery { + id: string; +} + +export interface TeamRequestBody { + username: string; + password: string; +} + +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 { username, password } = req.body; + + if (id !== userId && !isAdmin) { + return unauthorized(res); + } + + const user = await getTeam({ id }); + + const data: any = {}; + + if (password) { + data.password = hashPassword(password); + } + + // Only admin can change these fields + if (isAdmin) { + data.username = username; + } + + // Check when username changes + if (data.username && user.username !== data.username) { + const userByTeamname = await getTeam({ username }); + + if (userByTeamname) { + return badRequest(res, 'Team already exists'); + } + } + + const updated = await updateTeam(data, { 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/index.ts b/pages/api/teams/index.ts new file mode 100644 index 00000000..e69de29b diff --git a/pages/api/users/[id]/index.ts b/pages/api/users/[id]/index.ts index 870ff68a..80b6f8b2 100644 --- a/pages/api/users/[id]/index.ts +++ b/pages/api/users/[id]/index.ts @@ -5,17 +5,17 @@ import { NextApiResponse } from 'next'; import { NextApiRequestQueryBody } from 'interface/api/nextApi'; import { User } from 'interface/api/models'; -export interface UserReqeustQuery { +export interface UserRequestQuery { id: string; } -export interface UserReqeustBody { +export interface UserRequestBody { username: string; password: string; } export default async ( - req: NextApiRequestQueryBody, + req: NextApiRequestQueryBody, res: NextApiResponse, ) => { await useAuth(req, res); diff --git a/pages/api/websites/[id]/index.ts b/pages/api/websites/[id]/index.ts index 834b1732..2ec1ee14 100644 --- a/pages/api/websites/[id]/index.ts +++ b/pages/api/websites/[id]/index.ts @@ -7,18 +7,18 @@ import { NextApiRequestQueryBody } from 'interface/api/nextApi'; import { NextApiResponse } from 'next'; import { Website } from 'interface/api/models'; -export interface WebsiteReqeustQuery { +export interface WebsiteRequestQuery { id: string; } -export interface WebsiteReqeustBody { +export interface WebsiteRequestBody { name: string; domain: string; shareId: string; } export default async ( - req: NextApiRequestQueryBody, + req: NextApiRequestQueryBody, res: NextApiResponse, ) => { await useCors(req, res); diff --git a/pages/api/websites/[id]/metrics.ts b/pages/api/websites/[id]/metrics.ts index f3bf38aa..69c3c79d 100644 --- a/pages/api/websites/[id]/metrics.ts +++ b/pages/api/websites/[id]/metrics.ts @@ -36,7 +36,7 @@ function getColumn(type) { return type; } -export interface WebsiteMetricsReqeustQuery { +export interface WebsiteMetricsRequestQuery { id: string; type: string; start_at: number; @@ -50,7 +50,7 @@ export interface WebsiteMetricsReqeustQuery { } export default async ( - req: NextApiRequestQueryBody, + req: NextApiRequestQueryBody, res: NextApiResponse, ) => { await useCors(req, res); diff --git a/pages/api/websites/[id]/pageviews.ts b/pages/api/websites/[id]/pageviews.ts index c85a1a97..5bbf067b 100644 --- a/pages/api/websites/[id]/pageviews.ts +++ b/pages/api/websites/[id]/pageviews.ts @@ -10,7 +10,7 @@ import { getPageviewStats } from 'queries'; const unitTypes = ['year', 'month', 'hour', 'day']; -export interface WebsitePageviewReqeustQuery { +export interface WebsitePageviewRequestQuery { id: string; websiteId: string; start_at: number; @@ -26,7 +26,7 @@ export interface WebsitePageviewReqeustQuery { } export default async ( - req: NextApiRequestQueryBody, + req: NextApiRequestQueryBody, res: NextApiResponse, ) => { await useCors(req, res); diff --git a/pages/api/websites/[id]/reset.ts b/pages/api/websites/[id]/reset.ts index 6d2ffcea..ff141398 100644 --- a/pages/api/websites/[id]/reset.ts +++ b/pages/api/websites/[id]/reset.ts @@ -6,12 +6,12 @@ import { TYPE_WEBSITE } from 'lib/constants'; import { NextApiRequestQueryBody } from 'interface/api/nextApi'; import { NextApiResponse } from 'next'; -export interface WebsiteResetReqeustQuery { +export interface WebsiteResetRequestQuery { id: string; } export default async ( - req: NextApiRequestQueryBody, + req: NextApiRequestQueryBody, res: NextApiResponse, ) => { await useCors(req, res); diff --git a/pages/api/websites/[id]/stats.ts b/pages/api/websites/[id]/stats.ts index d9a581a9..497c3ff2 100644 --- a/pages/api/websites/[id]/stats.ts +++ b/pages/api/websites/[id]/stats.ts @@ -7,7 +7,7 @@ import { WebsiteStats } from 'interface/api/models'; import { NextApiRequestQueryBody } from 'interface/api/nextApi'; import { NextApiResponse } from 'next'; -export interface WebsiteStatsReqeustQuery { +export interface WebsiteStatsRequestQuery { id: string; type: string; start_at: number; @@ -21,7 +21,7 @@ export interface WebsiteStatsReqeustQuery { } export default async ( - req: NextApiRequestQueryBody, + req: NextApiRequestQueryBody, res: NextApiResponse, ) => { await useCors(req, res); diff --git a/pages/api/websites/index.ts b/pages/api/websites/index.ts index c4f2b3ea..4c5ad07c 100644 --- a/pages/api/websites/index.ts +++ b/pages/api/websites/index.ts @@ -5,18 +5,18 @@ import { NextApiResponse } from 'next'; import { getRandomChars, methodNotAllowed, ok } from 'next-basics'; import { createWebsiteByUser, getAllWebsites, getWebsitesByUserId } from 'queries'; -export interface WebsitesReqeustQuery { +export interface WebsitesRequestQuery { include_all?: boolean; } -export interface WebsitesReqeustBody { +export interface WebsitesRequestBody { name: string; domain: string; enableShareUrl: boolean; } export default async ( - req: NextApiRequestQueryBody, + req: NextApiRequestQueryBody, res: NextApiResponse, ) => { await useCors(req, res); From 25279271ce84c3193739c0f297f984961cef0af6 Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Fri, 18 Nov 2022 00:27:42 -0800 Subject: [PATCH 5/5] Add some api/team endpoints. --- pages/api/teams/[id]/index.ts | 38 ++++++-------------------- pages/api/teams/[id]/user.ts | 48 +++++++++++++++++++++++++++++++++ pages/api/teams/[id]/website.ts | 48 +++++++++++++++++++++++++++++++++ pages/api/teams/index.ts | 47 ++++++++++++++++++++++++++++++++ pages/api/users/index.ts | 2 +- queries/admin/team.ts | 16 +++++------ queries/admin/teamUser.ts | 4 ++- queries/admin/teamWebsite.ts | 4 ++- queries/admin/userRole.ts | 4 ++- queries/admin/userWebsite.ts | 4 ++- 10 files changed, 172 insertions(+), 43 deletions(-) create mode 100644 pages/api/teams/[id]/user.ts create mode 100644 pages/api/teams/[id]/website.ts diff --git a/pages/api/teams/[id]/index.ts b/pages/api/teams/[id]/index.ts index 26440c8b..14710a54 100644 --- a/pages/api/teams/[id]/index.ts +++ b/pages/api/teams/[id]/index.ts @@ -1,17 +1,17 @@ -import { badRequest, hashPassword, methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getTeam, deleteTeam, updateTeam } from 'queries'; +import { Team } from '@prisma/client'; +import { NextApiRequestQueryBody } from 'interface/api/nextApi'; import { useAuth } from 'lib/middleware'; import { NextApiResponse } from 'next'; -import { NextApiRequestQueryBody } from 'interface/api/nextApi'; -import { Team } from '@prisma/client'; +import { badRequest, methodNotAllowed, ok, unauthorized } from 'next-basics'; +import { deleteTeam, getTeam, updateTeam } from 'queries'; export interface TeamRequestQuery { id: string; } export interface TeamRequestBody { - username: string; - password: string; + name?: string; + is_deleted?: boolean; } export default async ( @@ -36,35 +36,13 @@ export default async ( } if (req.method === 'POST') { - const { username, password } = req.body; + const { name, is_deleted: isDeleted } = req.body; if (id !== userId && !isAdmin) { return unauthorized(res); } - const user = await getTeam({ id }); - - const data: any = {}; - - if (password) { - data.password = hashPassword(password); - } - - // Only admin can change these fields - if (isAdmin) { - data.username = username; - } - - // Check when username changes - if (data.username && user.username !== data.username) { - const userByTeamname = await getTeam({ username }); - - if (userByTeamname) { - return badRequest(res, 'Team already exists'); - } - } - - const updated = await updateTeam(data, { id }); + const updated = await updateTeam({ name, isDeleted }, { id }); return ok(res, updated); } 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 index e69de29b..8173c3b2 100644 --- a/pages/api/teams/index.ts +++ 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/index.ts b/pages/api/users/index.ts index 6b942287..07007546 100644 --- a/pages/api/users/index.ts +++ b/pages/api/users/index.ts @@ -13,7 +13,7 @@ export interface UsersRequestBody { } export default async ( - req: NextApiRequestQueryBody, + req: NextApiRequestQueryBody, res: NextApiResponse, ) => { await useAuth(req, res); diff --git a/queries/admin/team.ts b/queries/admin/team.ts index 71b5f807..8687fb64 100644 --- a/queries/admin/team.ts +++ b/queries/admin/team.ts @@ -1,19 +1,19 @@ -import { Prisma, Role, Team, TeamUser } from '@prisma/client'; +import { Prisma, Team, TeamUser } from '@prisma/client'; import prisma from 'lib/prisma'; -export async function createTeam(data: Prisma.RoleCreateInput): Promise { +export async function createTeam(data: Prisma.TeamCreateInput): Promise { return prisma.client.role.create({ data, }); } -export async function getTeam(where: Prisma.RoleWhereUniqueInput): Promise { +export async function getTeam(where: Prisma.TeamWhereUniqueInput): Promise { return prisma.client.role.findUnique({ where, }); } -export async function getTeams(where: Prisma.RoleWhereInput): Promise { +export async function getTeams(where: Prisma.TeamWhereInput): Promise { return prisma.client.role.findMany({ where, }); @@ -35,16 +35,16 @@ export async function getTeamsByUserId(userId: string): Promise< } export async function updateTeam( - data: Prisma.RoleUpdateInput, - where: Prisma.RoleWhereUniqueInput, -): Promise { + data: Prisma.TeamUpdateInput, + where: Prisma.TeamWhereUniqueInput, +): Promise { return prisma.client.role.update({ data, where, }); } -export async function deleteTeam(teamId: string): Promise { +export async function deleteTeam(teamId: string): Promise { return prisma.client.role.update({ data: { isDeleted: true, diff --git a/queries/admin/teamUser.ts b/queries/admin/teamUser.ts index e110efcf..b2f7bbf2 100644 --- a/queries/admin/teamUser.ts +++ b/queries/admin/teamUser.ts @@ -1,7 +1,9 @@ import { Prisma, TeamUser } from '@prisma/client'; import prisma from 'lib/prisma'; -export async function createTeamUser(data: Prisma.TeamUserCreateInput): Promise { +export async function createTeamUser( + data: Prisma.TeamUserCreateInput | Prisma.TeamUserUncheckedCreateInput, +): Promise { return prisma.client.teamUser.create({ data, }); diff --git a/queries/admin/teamWebsite.ts b/queries/admin/teamWebsite.ts index 950a7026..6b485da0 100644 --- a/queries/admin/teamWebsite.ts +++ b/queries/admin/teamWebsite.ts @@ -1,7 +1,9 @@ import { Prisma, TeamWebsite } from '@prisma/client'; import prisma from 'lib/prisma'; -export async function createTeamWebsite(data: Prisma.TeamWebsiteCreateInput): Promise { +export async function createTeamWebsite( + data: Prisma.TeamWebsiteCreateInput | Prisma.TeamWebsiteUncheckedCreateInput, +): Promise { return prisma.client.teamWebsite.create({ data, }); diff --git a/queries/admin/userRole.ts b/queries/admin/userRole.ts index c4e365ac..22893412 100644 --- a/queries/admin/userRole.ts +++ b/queries/admin/userRole.ts @@ -1,7 +1,9 @@ import { Prisma, UserRole } from '@prisma/client'; import prisma from 'lib/prisma'; -export async function createUserRole(data: Prisma.UserRoleCreateInput): Promise { +export async function createUserRole( + data: Prisma.UserRoleCreateInput | Prisma.UserRoleUncheckedCreateInput, +): Promise { return prisma.client.userRole.create({ data, }); diff --git a/queries/admin/userWebsite.ts b/queries/admin/userWebsite.ts index 313d6cd4..90039b8f 100644 --- a/queries/admin/userWebsite.ts +++ b/queries/admin/userWebsite.ts @@ -1,7 +1,9 @@ import { Prisma, UserWebsite } from '@prisma/client'; import prisma from 'lib/prisma'; -export async function createUserWebsite(data: Prisma.UserWebsiteCreateInput): Promise { +export async function createUserWebsite( + data: Prisma.UserWebsiteCreateInput | Prisma.UserWebsiteUncheckedCreateInput, +): Promise { return prisma.client.userWebsite.create({ data, });