refactor packages
This commit is contained in:
101
packages/constants/index.ts
Normal file
101
packages/constants/index.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
export const NOT_SET_VALUE = '(not set)';
|
||||
|
||||
export const operators = {
|
||||
is: 'Is',
|
||||
isNot: 'Is not',
|
||||
contains: 'Contains',
|
||||
doesNotContain: 'Not contains',
|
||||
} as const;
|
||||
|
||||
export const chartTypes = {
|
||||
linear: 'Linear',
|
||||
bar: 'Bar',
|
||||
histogram: 'Histogram',
|
||||
pie: 'Pie',
|
||||
metric: 'Metric',
|
||||
area: 'Area',
|
||||
map: 'Map',
|
||||
} as const;
|
||||
|
||||
export const lineTypes = {
|
||||
monotone: 'Monotone',
|
||||
monotoneX: 'Monotone X',
|
||||
monotoneY: 'Monotone Y',
|
||||
linear: 'Linear',
|
||||
natural: 'Natural',
|
||||
basis: 'Basis',
|
||||
step: 'Step',
|
||||
stepBefore: 'Step before',
|
||||
stepAfter: 'Step after',
|
||||
basisClosed: 'Basis closed',
|
||||
basisOpen: 'Basis open',
|
||||
bumpX: 'Bump X',
|
||||
bumpY: 'Bump Y',
|
||||
bump: 'Bump',
|
||||
linearClosed: 'Linear closed',
|
||||
} as const;
|
||||
|
||||
export const intervals = {
|
||||
minute: 'minute',
|
||||
day: 'day',
|
||||
hour: 'hour',
|
||||
month: 'month',
|
||||
} as const;
|
||||
|
||||
export const alphabetIds = [
|
||||
'A',
|
||||
'B',
|
||||
'C',
|
||||
'D',
|
||||
'E',
|
||||
'F',
|
||||
'G',
|
||||
'H',
|
||||
'I',
|
||||
'J',
|
||||
] as const;
|
||||
|
||||
export const timeRanges = {
|
||||
'30min': '30min',
|
||||
'1h': '1h',
|
||||
today: 'today',
|
||||
'24h': '24h',
|
||||
'7d': '7d',
|
||||
'14d': '14d',
|
||||
'1m': '1m',
|
||||
'3m': '3m',
|
||||
'6m': '6m',
|
||||
'1y': '1y',
|
||||
} as const;
|
||||
|
||||
export const metrics = {
|
||||
sum: 'sum',
|
||||
average: 'average',
|
||||
min: 'min',
|
||||
max: 'max',
|
||||
} as const;
|
||||
|
||||
export function isMinuteIntervalEnabledByRange(range: keyof typeof timeRanges) {
|
||||
return range === '30min' || range === '1h';
|
||||
}
|
||||
|
||||
export function isHourIntervalEnabledByRange(range: keyof typeof timeRanges) {
|
||||
return (
|
||||
isMinuteIntervalEnabledByRange(range) ||
|
||||
range === 'today' ||
|
||||
range === '24h'
|
||||
);
|
||||
}
|
||||
|
||||
export function getDefaultIntervalByRange(
|
||||
range: keyof typeof timeRanges
|
||||
): keyof typeof intervals {
|
||||
if (range === '30min' || range === '1h') {
|
||||
return 'minute';
|
||||
} else if (range === 'today' || range === '24h') {
|
||||
return 'hour';
|
||||
} else if (range === '7d' || range === '14d' || range === '1m') {
|
||||
return 'day';
|
||||
}
|
||||
return 'month';
|
||||
}
|
||||
26
packages/constants/package.json
Normal file
26
packages/constants/package.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "@mixan/constants",
|
||||
"version": "0.0.1",
|
||||
"main": "index.ts",
|
||||
"scripts": {
|
||||
"lint": "eslint .",
|
||||
"format": "prettier --check \"**/*.{mjs,ts,md,json}\"",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@mixan/eslint-config": "workspace:*",
|
||||
"@mixan/prettier-config": "workspace:*",
|
||||
"@mixan/tsconfig": "workspace:*",
|
||||
"eslint": "^8.48.0",
|
||||
"prettier": "^3.0.3",
|
||||
"prisma": "^5.1.1",
|
||||
"typescript": "^5.2.2"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"root": true,
|
||||
"extends": [
|
||||
"@mixan/eslint-config/base"
|
||||
]
|
||||
},
|
||||
"prettier": "@mixan/prettier-config"
|
||||
}
|
||||
12
packages/constants/tsconfig.json
Normal file
12
packages/constants/tsconfig.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"extends": "@mixan/tsconfig/base.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
|
||||
},
|
||||
"include": ["."],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
@@ -1,8 +1,14 @@
|
||||
export * from './src/prisma-client';
|
||||
export * from './src/prisma-types';
|
||||
export * from './src/clickhouse-client';
|
||||
export * from './src/sql-builder';
|
||||
export * from './src/services/salt';
|
||||
export * from './src/services/event.service';
|
||||
export * from './src/services/share.service';
|
||||
export * from './src/services/chart.service';
|
||||
export * from './src/services/clients.service';
|
||||
export * from './src/services/dashboard.service';
|
||||
export * from './src/services/event.service';
|
||||
export * from './src/services/organization.service';
|
||||
export * from './src/services/profile.service';
|
||||
export * from './src/services/project.service';
|
||||
export * from './src/services/reports.service';
|
||||
export * from './src/services/salt.service';
|
||||
export * from './src/services/share.service';
|
||||
export * from './src/services/user.service';
|
||||
|
||||
@@ -12,9 +12,12 @@
|
||||
"with-env": "dotenv -e ../../.env -c --"
|
||||
},
|
||||
"dependencies": {
|
||||
"@clerk/nextjs": "^4.29.7",
|
||||
"@clickhouse/client": "^0.2.9",
|
||||
"@mixan/common": "workspace:*",
|
||||
"@mixan/constants": "workspace:*",
|
||||
"@mixan/redis": "workspace:*",
|
||||
"@mixan/validation": "workspace:*",
|
||||
"@prisma/client": "^5.1.1",
|
||||
"ramda": "^0.29.1",
|
||||
"uuid": "^9.0.1"
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterEnum
|
||||
ALTER TYPE "ChartType" ADD VALUE 'map';
|
||||
@@ -97,6 +97,7 @@ enum ChartType {
|
||||
pie
|
||||
metric
|
||||
area
|
||||
map
|
||||
}
|
||||
|
||||
model Dashboard {
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
/* eslint-disable */
|
||||
|
||||
import { Profile } from '@prisma/client';
|
||||
|
||||
export type IDBEvent = {
|
||||
id: string;
|
||||
name: string;
|
||||
profile_id?: string;
|
||||
project_id: string;
|
||||
properties: Record<string, string>;
|
||||
created_at: string;
|
||||
};
|
||||
|
||||
export type IDBProfile = Omit<Profile, 'properties'> & {
|
||||
properties: Record<string, unknown>;
|
||||
};
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { IChartEventFilter, IGetChartDataInput } from '@mixan/validation';
|
||||
|
||||
import { formatClickhouseDate } from '../clickhouse-client';
|
||||
import type { SqlBuilderObject } from '../sql-builder';
|
||||
import { createSqlBuilder } from '../sql-builder';
|
||||
@@ -8,8 +10,6 @@ function log(sql: string) {
|
||||
return sql;
|
||||
}
|
||||
|
||||
type IGetChartDataInput = any;
|
||||
|
||||
export function getChartSql({
|
||||
event,
|
||||
breakdowns,
|
||||
@@ -107,7 +107,7 @@ export function getChartSql({
|
||||
|
||||
export function getEventFiltersWhereClause(
|
||||
sb: SqlBuilderObject,
|
||||
filters: any[]
|
||||
filters: IChartEventFilter[]
|
||||
) {
|
||||
filters.forEach((filter, index) => {
|
||||
const id = `f${index}`;
|
||||
|
||||
19
packages/db/src/services/clients.service.ts
Normal file
19
packages/db/src/services/clients.service.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import type { Client } from '../prisma-client';
|
||||
import { db } from '../prisma-client';
|
||||
import type { IServiceProject } from './project.service';
|
||||
|
||||
export type IServiceClient = Client;
|
||||
export type IServiceClientWithProject = Client & {
|
||||
project: Exclude<IServiceProject, null>;
|
||||
};
|
||||
|
||||
export function getClientsByOrganizationId(organizationId: string) {
|
||||
return db.client.findMany({
|
||||
where: {
|
||||
organization_slug: organizationId,
|
||||
},
|
||||
include: {
|
||||
project: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
50
packages/db/src/services/dashboard.service.ts
Normal file
50
packages/db/src/services/dashboard.service.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { db } from '../prisma-client';
|
||||
|
||||
export type IServiceDashboard = Awaited<ReturnType<typeof getDashboardById>>;
|
||||
export type IServiceDashboards = Awaited<
|
||||
ReturnType<typeof getDashboardsByProjectId>
|
||||
>;
|
||||
|
||||
export async function getDashboardById(id: string) {
|
||||
const dashboard = await db.dashboard.findUnique({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
include: {
|
||||
project: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!dashboard) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return dashboard;
|
||||
}
|
||||
|
||||
export async function getDashboardsByOrganization(organizationSlug: string) {
|
||||
return db.dashboard.findMany({
|
||||
where: {
|
||||
organization_slug: organizationSlug,
|
||||
},
|
||||
include: {
|
||||
project: true,
|
||||
},
|
||||
orderBy: {
|
||||
reports: {
|
||||
_count: 'desc',
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function getDashboardsByProjectId(projectId: string) {
|
||||
return db.dashboard.findMany({
|
||||
where: {
|
||||
project_id: projectId,
|
||||
},
|
||||
include: {
|
||||
project: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { v4 as uuid } from 'uuid';
|
||||
|
||||
import { randomSplitName, toDots } from '@mixan/common';
|
||||
import { redis, redisPub } from '@mixan/redis';
|
||||
import type { IChartEventFilter } from '@mixan/validation';
|
||||
|
||||
import {
|
||||
ch,
|
||||
@@ -12,9 +13,9 @@ import {
|
||||
} from '../clickhouse-client';
|
||||
import type { EventMeta, Prisma } from '../prisma-client';
|
||||
import { db } from '../prisma-client';
|
||||
import type { IDBProfile } from '../prisma-types';
|
||||
import { createSqlBuilder } from '../sql-builder';
|
||||
import { getEventFiltersWhereClause } from './chart.service';
|
||||
import type { IServiceProfile } from './profile.service';
|
||||
|
||||
export interface IClickhouseEvent {
|
||||
id: string;
|
||||
@@ -40,7 +41,7 @@ export interface IClickhouseEvent {
|
||||
model: string;
|
||||
|
||||
// They do not exist here. Just make ts happy for now
|
||||
profile?: IDBProfile;
|
||||
profile?: IServiceProfile;
|
||||
meta?: EventMeta;
|
||||
}
|
||||
|
||||
@@ -100,7 +101,7 @@ export interface IServiceCreateEventPayload {
|
||||
referrer: string | undefined;
|
||||
referrerName: string | undefined;
|
||||
referrerType: string | undefined;
|
||||
profile: IDBProfile | undefined;
|
||||
profile: IServiceProfile | undefined;
|
||||
meta: EventMeta | undefined;
|
||||
}
|
||||
|
||||
@@ -131,9 +132,7 @@ export async function getEvents(
|
||||
});
|
||||
|
||||
for (const event of events) {
|
||||
event.profile = profiles.find((p) => p.id === event.profile_id) as
|
||||
| IDBProfile
|
||||
| undefined;
|
||||
event.profile = profiles.find((p) => p.id === event.profile_id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -251,7 +250,8 @@ interface GetEventListOptions {
|
||||
profileId?: string;
|
||||
take: number;
|
||||
cursor?: number;
|
||||
filters: any[];
|
||||
events?: string[] | null;
|
||||
filters?: IChartEventFilter[];
|
||||
}
|
||||
|
||||
export async function getEventList({
|
||||
@@ -259,18 +259,29 @@ export async function getEventList({
|
||||
take,
|
||||
projectId,
|
||||
profileId,
|
||||
events,
|
||||
filters,
|
||||
}: GetEventListOptions) {
|
||||
const { sb, getSql } = createSqlBuilder();
|
||||
const { sb, getSql, join } = createSqlBuilder();
|
||||
|
||||
sb.limit = take;
|
||||
sb.offset = (cursor ?? 0) * take;
|
||||
sb.where.projectId = `project_id = '${projectId}'`;
|
||||
|
||||
if (profileId) {
|
||||
sb.where.profileId = `profile_id = '${profileId}'`;
|
||||
}
|
||||
|
||||
getEventFiltersWhereClause(sb, filters);
|
||||
if (events && events.length > 0) {
|
||||
sb.where.events = `name IN (${join(
|
||||
events.map((n) => `'${n}'`),
|
||||
','
|
||||
)})`;
|
||||
}
|
||||
|
||||
if (filters) {
|
||||
getEventFiltersWhereClause(sb, filters);
|
||||
}
|
||||
|
||||
// if (cursor) {
|
||||
// sb.where.cursor = `created_at <= '${formatClickhouseDate(cursor)}'`;
|
||||
@@ -284,15 +295,25 @@ export async function getEventList({
|
||||
export async function getEventsCount({
|
||||
projectId,
|
||||
profileId,
|
||||
events,
|
||||
filters,
|
||||
}: Omit<GetEventListOptions, 'cursor' | 'take'>) {
|
||||
const { sb, getSql } = createSqlBuilder();
|
||||
const { sb, getSql, join } = createSqlBuilder();
|
||||
sb.where.projectId = `project_id = '${projectId}'`;
|
||||
if (profileId) {
|
||||
sb.where.profileId = `profile_id = '${profileId}'`;
|
||||
}
|
||||
|
||||
getEventFiltersWhereClause(sb, filters);
|
||||
if (events && events.length > 0) {
|
||||
sb.where.events = `name IN (${join(
|
||||
events.map((n) => `'${n}'`),
|
||||
','
|
||||
)})`;
|
||||
}
|
||||
|
||||
if (filters) {
|
||||
getEventFiltersWhereClause(sb, filters);
|
||||
}
|
||||
|
||||
const res = await chQuery<{ count: number }>(
|
||||
getSql().replace('*', 'count(*) as count')
|
||||
|
||||
67
packages/db/src/services/organization.service.ts
Normal file
67
packages/db/src/services/organization.service.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { auth, clerkClient } from '@clerk/nextjs';
|
||||
import type {
|
||||
Organization,
|
||||
OrganizationInvitation,
|
||||
} from '@clerk/nextjs/dist/types/server';
|
||||
|
||||
import { db } from '../prisma-client';
|
||||
|
||||
export type IServiceOrganization = Awaited<
|
||||
ReturnType<typeof getCurrentOrganizations>
|
||||
>[number];
|
||||
|
||||
export type IServiceInvites = Awaited<ReturnType<typeof getInvites>>;
|
||||
|
||||
function transformOrganization(org: Organization) {
|
||||
return {
|
||||
id: org.id,
|
||||
name: org.name,
|
||||
slug: org.slug,
|
||||
};
|
||||
}
|
||||
|
||||
export async function getCurrentOrganizations() {
|
||||
const session = auth();
|
||||
const organizations = await clerkClient.users.getOrganizationMembershipList({
|
||||
userId: session.userId!,
|
||||
});
|
||||
return organizations.map((item) => transformOrganization(item.organization));
|
||||
}
|
||||
|
||||
export function getOrganizationBySlug(slug: string) {
|
||||
return clerkClient.organizations
|
||||
.getOrganization({ slug })
|
||||
.then(transformOrganization)
|
||||
.catch(() => null);
|
||||
}
|
||||
|
||||
export async function getOrganizationByProjectId(projectId: string) {
|
||||
const project = await db.project.findUniqueOrThrow({
|
||||
where: {
|
||||
id: projectId,
|
||||
},
|
||||
});
|
||||
|
||||
return clerkClient.organizations.getOrganization({
|
||||
slug: project.organization_slug,
|
||||
});
|
||||
}
|
||||
|
||||
export function transformInvite(invite: OrganizationInvitation) {
|
||||
return {
|
||||
id: invite.id,
|
||||
email: invite.emailAddress,
|
||||
role: invite.role,
|
||||
status: invite.status,
|
||||
createdAt: invite.createdAt,
|
||||
updatedAt: invite.updatedAt,
|
||||
};
|
||||
}
|
||||
|
||||
export async function getInvites(organizationId: string) {
|
||||
return await clerkClient.organizations
|
||||
.getOrganizationInvitationList({
|
||||
organizationId,
|
||||
})
|
||||
.then((invites) => invites.map(transformInvite));
|
||||
}
|
||||
35
packages/db/src/services/profile.service.ts
Normal file
35
packages/db/src/services/profile.service.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { db } from '../prisma-client';
|
||||
|
||||
export type IServiceProfile = Awaited<ReturnType<typeof getProfileById>>;
|
||||
|
||||
export function getProfileById(id: string) {
|
||||
return db.profile.findUniqueOrThrow({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function getProfilesByExternalId(
|
||||
externalId: string | null,
|
||||
projectId: string
|
||||
) {
|
||||
if (externalId === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return db.profile.findMany({
|
||||
where: {
|
||||
external_id: externalId,
|
||||
project_id: projectId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function getProfile(id: string) {
|
||||
return db.profile.findUniqueOrThrow({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
}
|
||||
30
packages/db/src/services/project.service.ts
Normal file
30
packages/db/src/services/project.service.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { db } from '../prisma-client';
|
||||
|
||||
export type IServiceProject = Awaited<ReturnType<typeof getProjectById>>;
|
||||
|
||||
export function getProjectById(id: string) {
|
||||
return db.project.findUnique({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function getProjectsByOrganizationSlug(slug: string) {
|
||||
return db.project.findMany({
|
||||
where: {
|
||||
organization_slug: slug,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function getProjectWithMostEvents(slug: string) {
|
||||
return db.project.findFirst({
|
||||
where: {
|
||||
organization_slug: slug,
|
||||
},
|
||||
orderBy: {
|
||||
eventsCount: 'desc',
|
||||
},
|
||||
});
|
||||
}
|
||||
85
packages/db/src/services/reports.service.ts
Normal file
85
packages/db/src/services/reports.service.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import { alphabetIds, lineTypes, timeRanges } from '@mixan/constants';
|
||||
import type {
|
||||
IChartBreakdown,
|
||||
IChartEvent,
|
||||
IChartEventFilter,
|
||||
IChartInput,
|
||||
IChartLineType,
|
||||
IChartRange,
|
||||
} from '@mixan/validation';
|
||||
|
||||
import { db } from '../prisma-client';
|
||||
import type { Report as DbReport } from '../prisma-client';
|
||||
|
||||
export type IServiceReport = Awaited<ReturnType<typeof getReportById>>;
|
||||
|
||||
export function transformFilter(
|
||||
filter: Partial<IChartEventFilter>,
|
||||
index: number
|
||||
): IChartEventFilter {
|
||||
return {
|
||||
id: filter.id ?? alphabetIds[index] ?? 'A',
|
||||
name: filter.name ?? 'Unknown Filter',
|
||||
operator: filter.operator ?? 'is',
|
||||
value:
|
||||
typeof filter.value === 'string' ? [filter.value] : filter.value ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
export function transformReportEvent(
|
||||
event: Partial<IChartEvent>,
|
||||
index: number
|
||||
): IChartEvent {
|
||||
return {
|
||||
segment: event.segment ?? 'event',
|
||||
filters: (event.filters ?? []).map(transformFilter),
|
||||
id: event.id ?? alphabetIds[index]!,
|
||||
name: event.name || 'unknown_event',
|
||||
displayName: event.displayName,
|
||||
property: event.property,
|
||||
};
|
||||
}
|
||||
|
||||
export function transformReport(
|
||||
report: DbReport
|
||||
): IChartInput & { id: string } {
|
||||
return {
|
||||
id: report.id,
|
||||
projectId: report.project_id,
|
||||
events: (report.events as IChartEvent[]).map(transformReportEvent),
|
||||
breakdowns: report.breakdowns as IChartBreakdown[],
|
||||
chartType: report.chart_type,
|
||||
lineType: (report.line_type as IChartLineType) ?? lineTypes.monotone,
|
||||
interval: report.interval,
|
||||
name: report.name || 'Untitled',
|
||||
range: (report.range as IChartRange) ?? timeRanges['1m'],
|
||||
previous: report.previous ?? false,
|
||||
formula: report.formula ?? undefined,
|
||||
metric: report.metric ?? 'sum',
|
||||
unit: report.unit ?? undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export function getReportsByDashboardId(dashboardId: string) {
|
||||
return db.report
|
||||
.findMany({
|
||||
where: {
|
||||
dashboard_id: dashboardId,
|
||||
},
|
||||
})
|
||||
.then((reports) => reports.map(transformReport));
|
||||
}
|
||||
|
||||
export async function getReportById(id: string) {
|
||||
const report = await db.report.findUnique({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
|
||||
if (!report) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return transformReport(report);
|
||||
}
|
||||
24
packages/db/src/services/user.service.ts
Normal file
24
packages/db/src/services/user.service.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { auth, clerkClient } from '@clerk/nextjs';
|
||||
import type { User } from '@clerk/nextjs/dist/types/server';
|
||||
|
||||
export function transformUser(user: User) {
|
||||
return {
|
||||
name: `${user.firstName} ${user.lastName}`,
|
||||
email: user.emailAddresses[0]?.emailAddress ?? '',
|
||||
id: user.id,
|
||||
lastName: user.lastName ?? '',
|
||||
firstName: user.firstName ?? '',
|
||||
};
|
||||
}
|
||||
|
||||
export async function getCurrentUser() {
|
||||
const session = auth();
|
||||
if (!session.userId) {
|
||||
return null;
|
||||
}
|
||||
return getUserById(session.userId);
|
||||
}
|
||||
|
||||
export async function getUserById(id: string) {
|
||||
return clerkClient.users.getUser(id).then(transformUser);
|
||||
}
|
||||
@@ -1,168 +1 @@
|
||||
export type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
|
||||
export type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
|
||||
|
||||
export type MixanJson = Record<string, any>;
|
||||
|
||||
// Deprecated
|
||||
export interface EventPayload {
|
||||
name: string;
|
||||
time: string;
|
||||
profileId: string | null;
|
||||
properties: MixanJson;
|
||||
}
|
||||
|
||||
// Deprecated
|
||||
export interface ProfilePayload {
|
||||
first_name?: string;
|
||||
last_name?: string;
|
||||
email?: string;
|
||||
avatar?: string;
|
||||
id?: string;
|
||||
properties?: MixanJson;
|
||||
}
|
||||
|
||||
export type BatchPayload =
|
||||
| {
|
||||
type: 'increment';
|
||||
payload: BatchProfileIncrementPayload;
|
||||
}
|
||||
| {
|
||||
type: 'decrement';
|
||||
payload: BatchProfileDecrementPayload;
|
||||
}
|
||||
| {
|
||||
type: 'event';
|
||||
payload: BatchEventPayload;
|
||||
}
|
||||
| {
|
||||
type: 'create_profile';
|
||||
payload: BatchCreateProfilePayload;
|
||||
}
|
||||
| {
|
||||
type: 'update_profile';
|
||||
payload: BatchUpdateProfilePayload;
|
||||
}
|
||||
| {
|
||||
type: 'update_session';
|
||||
payload: BatchUpdateSessionPayload;
|
||||
}
|
||||
| {
|
||||
type: 'set_profile_property';
|
||||
payload: BatchSetProfilePropertyPayload;
|
||||
};
|
||||
|
||||
export interface BatchSetProfilePropertyPayload {
|
||||
profileId: string;
|
||||
name: string;
|
||||
value: any;
|
||||
update: boolean;
|
||||
}
|
||||
|
||||
export interface CreateProfileResponse {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface BatchCreateProfilePayload {
|
||||
profileId: string;
|
||||
properties?: MixanJson;
|
||||
}
|
||||
|
||||
export interface BatchUpdateSessionPayload {
|
||||
profileId: string;
|
||||
properties?: MixanJson;
|
||||
}
|
||||
|
||||
export interface BatchEventPayload {
|
||||
name: string;
|
||||
time: string;
|
||||
profileId: string;
|
||||
properties: MixanJson;
|
||||
}
|
||||
|
||||
export interface BatchUpdateProfilePayload {
|
||||
first_name?: string;
|
||||
last_name?: string;
|
||||
email?: string;
|
||||
avatar?: string;
|
||||
id?: string;
|
||||
properties?: MixanJson;
|
||||
profileId: string;
|
||||
}
|
||||
|
||||
export interface ProfileIncrementPayload {
|
||||
name: string;
|
||||
value: number;
|
||||
profileId: string;
|
||||
}
|
||||
|
||||
export interface ProfileDecrementPayload {
|
||||
name: string;
|
||||
value: number;
|
||||
profileId: string;
|
||||
}
|
||||
|
||||
export interface BatchProfileIncrementPayload {
|
||||
name: string;
|
||||
value: number;
|
||||
profileId: string;
|
||||
}
|
||||
|
||||
export interface BatchProfileDecrementPayload {
|
||||
name: string;
|
||||
value: number;
|
||||
profileId: string;
|
||||
}
|
||||
|
||||
export interface MixanIssue {
|
||||
field: string;
|
||||
message: string;
|
||||
value: any;
|
||||
}
|
||||
|
||||
export interface MixanErrorResponse {
|
||||
status: 'error';
|
||||
code: number;
|
||||
message: string;
|
||||
issues?: MixanIssue[] | undefined;
|
||||
stack?: string | undefined;
|
||||
}
|
||||
|
||||
export interface MixanResponse<T> {
|
||||
result: T;
|
||||
status: 'ok';
|
||||
}
|
||||
|
||||
// NEW
|
||||
|
||||
export interface PostEventPayload {
|
||||
name: string;
|
||||
timestamp: string;
|
||||
profileId?: string;
|
||||
properties?: Record<string, unknown> & {
|
||||
title?: string | undefined;
|
||||
referrer?: string | undefined;
|
||||
path?: string | undefined;
|
||||
};
|
||||
}
|
||||
|
||||
export interface UpdateProfilePayload {
|
||||
profileId?: string;
|
||||
id?: string;
|
||||
first_name?: string;
|
||||
last_name?: string;
|
||||
email?: string;
|
||||
avatar?: string;
|
||||
properties?: MixanJson;
|
||||
}
|
||||
|
||||
export interface IncrementProfilePayload {
|
||||
profileId?: string;
|
||||
property: string;
|
||||
value: number;
|
||||
}
|
||||
|
||||
export interface DecrementProfilePayload {
|
||||
profileId?: string;
|
||||
property: string;
|
||||
value: number;
|
||||
}
|
||||
export * from './src/sdk.types';
|
||||
|
||||
168
packages/types/src/sdk.types.ts
Normal file
168
packages/types/src/sdk.types.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
export type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
|
||||
export type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
|
||||
|
||||
export type MixanJson = Record<string, any>;
|
||||
|
||||
// Deprecated
|
||||
export interface EventPayload {
|
||||
name: string;
|
||||
time: string;
|
||||
profileId: string | null;
|
||||
properties: MixanJson;
|
||||
}
|
||||
|
||||
// Deprecated
|
||||
export interface ProfilePayload {
|
||||
first_name?: string;
|
||||
last_name?: string;
|
||||
email?: string;
|
||||
avatar?: string;
|
||||
id?: string;
|
||||
properties?: MixanJson;
|
||||
}
|
||||
|
||||
export type BatchPayload =
|
||||
| {
|
||||
type: 'increment';
|
||||
payload: BatchProfileIncrementPayload;
|
||||
}
|
||||
| {
|
||||
type: 'decrement';
|
||||
payload: BatchProfileDecrementPayload;
|
||||
}
|
||||
| {
|
||||
type: 'event';
|
||||
payload: BatchEventPayload;
|
||||
}
|
||||
| {
|
||||
type: 'create_profile';
|
||||
payload: BatchCreateProfilePayload;
|
||||
}
|
||||
| {
|
||||
type: 'update_profile';
|
||||
payload: BatchUpdateProfilePayload;
|
||||
}
|
||||
| {
|
||||
type: 'update_session';
|
||||
payload: BatchUpdateSessionPayload;
|
||||
}
|
||||
| {
|
||||
type: 'set_profile_property';
|
||||
payload: BatchSetProfilePropertyPayload;
|
||||
};
|
||||
|
||||
export interface BatchSetProfilePropertyPayload {
|
||||
profileId: string;
|
||||
name: string;
|
||||
value: any;
|
||||
update: boolean;
|
||||
}
|
||||
|
||||
export interface CreateProfileResponse {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface BatchCreateProfilePayload {
|
||||
profileId: string;
|
||||
properties?: MixanJson;
|
||||
}
|
||||
|
||||
export interface BatchUpdateSessionPayload {
|
||||
profileId: string;
|
||||
properties?: MixanJson;
|
||||
}
|
||||
|
||||
export interface BatchEventPayload {
|
||||
name: string;
|
||||
time: string;
|
||||
profileId: string;
|
||||
properties: MixanJson;
|
||||
}
|
||||
|
||||
export interface BatchUpdateProfilePayload {
|
||||
first_name?: string;
|
||||
last_name?: string;
|
||||
email?: string;
|
||||
avatar?: string;
|
||||
id?: string;
|
||||
properties?: MixanJson;
|
||||
profileId: string;
|
||||
}
|
||||
|
||||
export interface ProfileIncrementPayload {
|
||||
name: string;
|
||||
value: number;
|
||||
profileId: string;
|
||||
}
|
||||
|
||||
export interface ProfileDecrementPayload {
|
||||
name: string;
|
||||
value: number;
|
||||
profileId: string;
|
||||
}
|
||||
|
||||
export interface BatchProfileIncrementPayload {
|
||||
name: string;
|
||||
value: number;
|
||||
profileId: string;
|
||||
}
|
||||
|
||||
export interface BatchProfileDecrementPayload {
|
||||
name: string;
|
||||
value: number;
|
||||
profileId: string;
|
||||
}
|
||||
|
||||
export interface MixanIssue {
|
||||
field: string;
|
||||
message: string;
|
||||
value: any;
|
||||
}
|
||||
|
||||
export interface MixanErrorResponse {
|
||||
status: 'error';
|
||||
code: number;
|
||||
message: string;
|
||||
issues?: MixanIssue[] | undefined;
|
||||
stack?: string | undefined;
|
||||
}
|
||||
|
||||
export interface MixanResponse<T> {
|
||||
result: T;
|
||||
status: 'ok';
|
||||
}
|
||||
|
||||
// NEW
|
||||
|
||||
export interface PostEventPayload {
|
||||
name: string;
|
||||
timestamp: string;
|
||||
profileId?: string;
|
||||
properties?: Record<string, unknown> & {
|
||||
title?: string | undefined;
|
||||
referrer?: string | undefined;
|
||||
path?: string | undefined;
|
||||
};
|
||||
}
|
||||
|
||||
export interface UpdateProfilePayload {
|
||||
profileId?: string;
|
||||
id?: string;
|
||||
first_name?: string;
|
||||
last_name?: string;
|
||||
email?: string;
|
||||
avatar?: string;
|
||||
properties?: MixanJson;
|
||||
}
|
||||
|
||||
export interface IncrementProfilePayload {
|
||||
profileId?: string;
|
||||
property: string;
|
||||
value: number;
|
||||
}
|
||||
|
||||
export interface DecrementProfilePayload {
|
||||
profileId?: string;
|
||||
property: string;
|
||||
value: number;
|
||||
}
|
||||
2
packages/validation/index.ts
Normal file
2
packages/validation/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './src/index';
|
||||
export * from './src/types.validation';
|
||||
32
packages/validation/package.json
Normal file
32
packages/validation/package.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "@mixan/validation",
|
||||
"version": "0.0.1",
|
||||
"main": "index.ts",
|
||||
"scripts": {
|
||||
"lint": "eslint .",
|
||||
"format": "prettier --check \"**/*.{mjs,ts,md,json}\"",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mixan/constants": "workspace:*",
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@mixan/eslint-config": "workspace:*",
|
||||
"@mixan/prettier-config": "workspace:*",
|
||||
"@mixan/tsconfig": "workspace:*",
|
||||
"@mixan/types": "workspace:*",
|
||||
"@types/node": "^18.16.0",
|
||||
"eslint": "^8.48.0",
|
||||
"prettier": "^3.0.3",
|
||||
"prisma": "^5.1.1",
|
||||
"typescript": "^5.2.2"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"root": true,
|
||||
"extends": [
|
||||
"@mixan/eslint-config/base"
|
||||
]
|
||||
},
|
||||
"prettier": "@mixan/prettier-config"
|
||||
}
|
||||
88
packages/validation/src/index.ts
Normal file
88
packages/validation/src/index.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import {
|
||||
chartTypes,
|
||||
intervals,
|
||||
lineTypes,
|
||||
metrics,
|
||||
operators,
|
||||
timeRanges,
|
||||
} from '@mixan/constants';
|
||||
|
||||
export function objectToZodEnums<K extends string>(
|
||||
obj: Record<K, any>
|
||||
): [K, ...K[]] {
|
||||
const [firstKey, ...otherKeys] = Object.keys(obj) as K[];
|
||||
return [firstKey!, ...otherKeys];
|
||||
}
|
||||
|
||||
export const mapKeys = objectToZodEnums;
|
||||
|
||||
export const zChartEvent = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
displayName: z.string().optional(),
|
||||
property: z.string().optional(),
|
||||
segment: z.enum([
|
||||
'event',
|
||||
'user',
|
||||
'user_average',
|
||||
'one_event_per_user',
|
||||
'property_sum',
|
||||
'property_average',
|
||||
]),
|
||||
filters: z.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
operator: z.enum(objectToZodEnums(operators)),
|
||||
value: z.array(z.string().or(z.number()).or(z.boolean()).or(z.null())),
|
||||
})
|
||||
),
|
||||
});
|
||||
export const zChartBreakdown = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
});
|
||||
|
||||
export const zChartEvents = z.array(zChartEvent);
|
||||
export const zChartBreakdowns = z.array(zChartBreakdown);
|
||||
|
||||
export const zChartType = z.enum(objectToZodEnums(chartTypes));
|
||||
|
||||
export const zLineType = z.enum(objectToZodEnums(lineTypes));
|
||||
|
||||
export const zTimeInterval = z.enum(objectToZodEnums(intervals));
|
||||
|
||||
export const zMetric = z.enum(objectToZodEnums(metrics));
|
||||
|
||||
export const zChartInput = z.object({
|
||||
name: z.string(),
|
||||
chartType: zChartType,
|
||||
lineType: zLineType,
|
||||
interval: zTimeInterval,
|
||||
events: zChartEvents,
|
||||
breakdowns: zChartBreakdowns,
|
||||
range: z.enum(objectToZodEnums(timeRanges)),
|
||||
previous: z.boolean(),
|
||||
formula: z.string().optional(),
|
||||
metric: zMetric,
|
||||
unit: z.string().optional(),
|
||||
previousIndicatorInverted: z.boolean().optional(),
|
||||
projectId: z.string(),
|
||||
startDate: z.string().nullish(),
|
||||
endDate: z.string().nullish(),
|
||||
});
|
||||
|
||||
export const zInviteUser = z.object({
|
||||
email: z.string().email(),
|
||||
organizationSlug: z.string(),
|
||||
role: z.enum(['admin', 'org:member']),
|
||||
});
|
||||
|
||||
export const zShareOverview = z.object({
|
||||
organizationId: z.string(),
|
||||
projectId: z.string(),
|
||||
password: z.string().nullable(),
|
||||
public: z.boolean(),
|
||||
});
|
||||
33
packages/validation/src/types.validation.ts
Normal file
33
packages/validation/src/types.validation.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import type { z } from 'zod';
|
||||
|
||||
import type { timeRanges } from '@mixan/constants';
|
||||
|
||||
import type {
|
||||
zChartBreakdown,
|
||||
zChartEvent,
|
||||
zChartInput,
|
||||
zChartType,
|
||||
zLineType,
|
||||
zMetric,
|
||||
zTimeInterval,
|
||||
} from './index';
|
||||
|
||||
export type IChartInput = z.infer<typeof zChartInput>;
|
||||
export type IChartEvent = z.infer<typeof zChartEvent>;
|
||||
export type IChartEventFilter = IChartEvent['filters'][number];
|
||||
export type IChartEventFilterValue =
|
||||
IChartEvent['filters'][number]['value'][number];
|
||||
export type IChartEventFilterOperator =
|
||||
IChartEvent['filters'][number]['operator'];
|
||||
export type IChartBreakdown = z.infer<typeof zChartBreakdown>;
|
||||
export type IInterval = z.infer<typeof zTimeInterval>;
|
||||
export type IChartType = z.infer<typeof zChartType>;
|
||||
export type IChartMetric = z.infer<typeof zMetric>;
|
||||
export type IChartLineType = z.infer<typeof zLineType>;
|
||||
export type IChartRange = keyof typeof timeRanges;
|
||||
export type IGetChartDataInput = {
|
||||
event: IChartEvent;
|
||||
projectId: string;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
} & Omit<IChartInput, 'events' | 'name' | 'startDate' | 'endDate' | 'range'>;
|
||||
12
packages/validation/tsconfig.json
Normal file
12
packages/validation/tsconfig.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"extends": "@mixan/tsconfig/base.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
|
||||
},
|
||||
"include": ["."],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
Reference in New Issue
Block a user