a lot
This commit is contained in:
51
packages/db/src/clickhouse-client.ts
Normal file
51
packages/db/src/clickhouse-client.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { createClient } from '@clickhouse/client';
|
||||
|
||||
export const ch = createClient({
|
||||
host: process.env.CLICKHOUSE_URL,
|
||||
username: process.env.CLICKHOUSE_USER,
|
||||
password: process.env.CLICKHOUSE_PASSWORD,
|
||||
database: process.env.CLICKHOUSE_DB,
|
||||
});
|
||||
|
||||
interface ClickhouseJsonResponse<T> {
|
||||
data: T[];
|
||||
rows: number;
|
||||
statistics: { elapsed: number; rows_read: number; bytes_read: number };
|
||||
meta: { name: string; type: string }[];
|
||||
}
|
||||
|
||||
export async function chQueryAll<T extends Record<string, any>>(
|
||||
query: string
|
||||
): Promise<ClickhouseJsonResponse<T>> {
|
||||
const res = await ch.query({
|
||||
query,
|
||||
});
|
||||
const json = await res.json<ClickhouseJsonResponse<T>>();
|
||||
return {
|
||||
...json,
|
||||
data: json.data.map((item) => {
|
||||
const keys = Object.keys(item);
|
||||
return keys.reduce((acc, key) => {
|
||||
const meta = json.meta.find((m) => m.name === key);
|
||||
return {
|
||||
...acc,
|
||||
[key]:
|
||||
item[key] && meta?.type.includes('Int')
|
||||
? parseFloat(item[key] as string)
|
||||
: item[key],
|
||||
};
|
||||
}, {} as T);
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
export async function chQuery<T extends Record<string, any>>(
|
||||
query: string
|
||||
): Promise<T[]> {
|
||||
return (await chQueryAll<T>(query)).data;
|
||||
}
|
||||
|
||||
export function formatClickhouseDate(_date: Date | string) {
|
||||
const date = typeof _date === 'string' ? new Date(_date) : _date;
|
||||
return date.toISOString().replace('T', ' ').replace(/Z+$/, '');
|
||||
}
|
||||
15
packages/db/src/prisma-client.ts
Normal file
15
packages/db/src/prisma-client.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
export * from '@prisma/client';
|
||||
|
||||
const globalForPrisma = globalThis as unknown as {
|
||||
prisma: PrismaClient | undefined;
|
||||
};
|
||||
|
||||
export const db =
|
||||
globalForPrisma.prisma ??
|
||||
new PrismaClient({
|
||||
log: ['error'],
|
||||
});
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = db;
|
||||
22
packages/db/src/prisma-types.ts
Normal file
22
packages/db/src/prisma-types.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
/* eslint-disable */
|
||||
|
||||
export type IDBEvent = {
|
||||
id: string;
|
||||
name: string;
|
||||
profile_id?: string;
|
||||
project_id: string;
|
||||
properties: Record<string, string>;
|
||||
created_at: string;
|
||||
};
|
||||
|
||||
export type IDBProfile = {
|
||||
id: string;
|
||||
external_id?: string;
|
||||
first_name?: string;
|
||||
last_name?: string;
|
||||
email?: string;
|
||||
avatar?: string;
|
||||
properties: Record<string, string>;
|
||||
project_id: String;
|
||||
created_at: string;
|
||||
};
|
||||
113
packages/db/src/services/event.service.ts
Normal file
113
packages/db/src/services/event.service.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import { omit } from 'ramda';
|
||||
|
||||
import { toDots } from '@mixan/common';
|
||||
|
||||
import { ch, chQuery, formatClickhouseDate } from '../clickhouse-client';
|
||||
|
||||
export interface IClickhouseEvent {
|
||||
name: string;
|
||||
profile_id: string;
|
||||
project_id: string;
|
||||
path: string;
|
||||
referrer: string;
|
||||
referrer_name: string;
|
||||
duration: number;
|
||||
properties: Record<string, string>;
|
||||
created_at: string;
|
||||
country: string;
|
||||
city: string;
|
||||
region: string;
|
||||
os: string;
|
||||
os_version: string;
|
||||
browser: string;
|
||||
browser_version: string;
|
||||
device: string;
|
||||
brand: string;
|
||||
model: string;
|
||||
}
|
||||
|
||||
export function transformEvent(
|
||||
event: IClickhouseEvent
|
||||
): IServiceCreateEventPayload {
|
||||
return {
|
||||
name: event.name,
|
||||
profileId: event.profile_id,
|
||||
projectId: event.project_id,
|
||||
properties: event.properties,
|
||||
createdAt: event.created_at,
|
||||
country: event.country,
|
||||
city: event.city,
|
||||
region: event.region,
|
||||
os: event.os,
|
||||
osVersion: event.os_version,
|
||||
browser: event.browser,
|
||||
browserVersion: event.browser_version,
|
||||
device: event.device,
|
||||
brand: event.brand,
|
||||
model: event.model,
|
||||
duration: event.duration,
|
||||
path: event.path,
|
||||
referrer: event.referrer,
|
||||
referrerName: event.referrer_name,
|
||||
};
|
||||
}
|
||||
|
||||
export interface IServiceCreateEventPayload {
|
||||
name: string;
|
||||
profileId: string;
|
||||
projectId: string;
|
||||
properties: Record<string, unknown>;
|
||||
createdAt: string;
|
||||
country?: string | undefined;
|
||||
city?: string | undefined;
|
||||
region?: string | undefined;
|
||||
continent?: string | undefined;
|
||||
os?: string | undefined;
|
||||
osVersion?: string | undefined;
|
||||
browser?: string | undefined;
|
||||
browserVersion?: string | undefined;
|
||||
device?: string | undefined;
|
||||
brand?: string | undefined;
|
||||
model?: string | undefined;
|
||||
duration: number;
|
||||
path: string;
|
||||
referrer: string | undefined;
|
||||
referrerName: string | undefined;
|
||||
}
|
||||
|
||||
export function getEvents(sql: string) {
|
||||
return chQuery<IClickhouseEvent>(sql).then((events) =>
|
||||
events.map(transformEvent)
|
||||
);
|
||||
}
|
||||
|
||||
export async function createEvent(payload: IServiceCreateEventPayload) {
|
||||
console.log(`create event ${payload.name} for ${payload.profileId}`);
|
||||
|
||||
return ch.insert({
|
||||
table: 'events',
|
||||
values: [
|
||||
{
|
||||
name: payload.name,
|
||||
profile_id: payload.profileId,
|
||||
project_id: payload.projectId,
|
||||
properties: toDots(omit(['_path'], payload.properties)),
|
||||
path: payload.path ?? '',
|
||||
created_at: formatClickhouseDate(payload.createdAt),
|
||||
country: payload.country ?? '',
|
||||
city: payload.city ?? '',
|
||||
region: payload.region ?? '',
|
||||
os: payload.os ?? '',
|
||||
os_version: payload.osVersion ?? '',
|
||||
browser: payload.browser ?? '',
|
||||
browser_version: payload.browserVersion ?? '',
|
||||
device: payload.device ?? '',
|
||||
brand: payload.brand ?? '',
|
||||
model: payload.model ?? '',
|
||||
duration: payload.duration,
|
||||
referrer: payload.referrer ?? '',
|
||||
},
|
||||
],
|
||||
format: 'JSONEachRow',
|
||||
});
|
||||
}
|
||||
37
packages/db/src/services/salt.ts
Normal file
37
packages/db/src/services/salt.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { db } from '../prisma-client';
|
||||
|
||||
export async function getCurrentSalt() {
|
||||
const salt = await db.salt.findFirst({
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
});
|
||||
|
||||
if (!salt) {
|
||||
throw new Error('No salt found');
|
||||
}
|
||||
|
||||
return salt.salt;
|
||||
}
|
||||
|
||||
export async function getSalts() {
|
||||
const [curr, prev] = await db.salt.findMany({
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
take: 2,
|
||||
});
|
||||
|
||||
if (!curr) {
|
||||
throw new Error('No salt found');
|
||||
}
|
||||
|
||||
if (!prev) {
|
||||
throw new Error('No previous salt found');
|
||||
}
|
||||
|
||||
return {
|
||||
current: curr.salt,
|
||||
previous: prev.salt,
|
||||
};
|
||||
}
|
||||
56
packages/db/src/sql-builder.ts
Normal file
56
packages/db/src/sql-builder.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
export function createSqlBuilder() {
|
||||
const join = (obj: Record<string, string> | string[], joiner: string) =>
|
||||
Object.values(obj).filter(Boolean).join(joiner);
|
||||
|
||||
const sb: {
|
||||
where: Record<string, string>;
|
||||
select: Record<string, string>;
|
||||
groupBy: Record<string, string>;
|
||||
orderBy: Record<string, string>;
|
||||
from: string;
|
||||
limit: number | undefined;
|
||||
offset: number | undefined;
|
||||
} = {
|
||||
where: {},
|
||||
from: 'events',
|
||||
select: {},
|
||||
groupBy: {},
|
||||
orderBy: {},
|
||||
limit: undefined,
|
||||
offset: undefined,
|
||||
};
|
||||
|
||||
const getWhere = () =>
|
||||
Object.keys(sb.where).length ? 'WHERE ' + join(sb.where, ' AND ') : '';
|
||||
const getFrom = () => `FROM ${sb.from}`;
|
||||
const getSelect = () =>
|
||||
'SELECT ' + (Object.keys(sb.select).length ? join(sb.select, ', ') : '*');
|
||||
const getGroupBy = () =>
|
||||
Object.keys(sb.groupBy).length ? 'GROUP BY ' + join(sb.groupBy, ', ') : '';
|
||||
const getOrderBy = () =>
|
||||
Object.keys(sb.orderBy).length ? 'ORDER BY ' + join(sb.orderBy, ', ') : '';
|
||||
const getLimit = () => (sb.limit ? `LIMIT ${sb.limit}` : '');
|
||||
const getOffset = () => (sb.offset ? `OFFSET ${sb.offset}` : '');
|
||||
|
||||
return {
|
||||
sb,
|
||||
join,
|
||||
getWhere,
|
||||
getFrom,
|
||||
getSelect,
|
||||
getGroupBy,
|
||||
getOrderBy,
|
||||
getSql: () =>
|
||||
[
|
||||
getSelect(),
|
||||
getFrom(),
|
||||
getWhere(),
|
||||
getGroupBy(),
|
||||
getOrderBy(),
|
||||
getLimit(),
|
||||
getOffset(),
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' '),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user