migrate to app dir and ssr

This commit is contained in:
Carl-Gerhard Lindesvärd
2024-01-20 22:54:38 +01:00
parent 719a82f1c4
commit 308ae98472
194 changed files with 4706 additions and 2194 deletions

View File

@@ -4,29 +4,28 @@ import { getChartSql } from '@/server/chart-sql/getChartSql';
import { isJsonPath, selectJsonPath } from '@/server/chart-sql/helpers';
import { db } from '@/server/db';
import { getUniqueEvents } from '@/server/services/event.service';
import { getProjectBySlug } from '@/server/services/project.service';
import type {
IChartEvent,
IChartRange,
IGetChartDataInput,
IInterval,
} from '@/types';
import { alphabetIds } from '@/utils/constants';
import { getDaysOldDate } from '@/utils/date';
import { average, isFloat, round, sum } from '@/utils/math';
import { average, round, sum } from '@/utils/math';
import { toDots } from '@/utils/object';
import { zChartInputWithDates } from '@/utils/validation';
import { last, pipe, sort, uniq } from 'ramda';
import { pipe, sort, uniq } from 'ramda';
import { z } from 'zod';
export const chartRouter = createTRPCRouter({
events: protectedProcedure
.input(z.object({ projectSlug: z.string() }))
.query(async ({ input: { projectSlug } }) => {
const project = await getProjectBySlug(projectSlug);
.input(z.object({ projectId: z.string() }))
.query(async ({ input: { projectId } }) => {
const events = await cache.getOr(
`events_${project.id}`,
`events_${projectId}`,
1000 * 60 * 60 * 24,
() => getUniqueEvents({ projectId: project.id })
() => getUniqueEvents({ projectId: projectId })
);
return [
@@ -38,17 +37,16 @@ export const chartRouter = createTRPCRouter({
}),
properties: protectedProcedure
.input(z.object({ event: z.string().optional(), projectSlug: z.string() }))
.query(async ({ input: { projectSlug, event } }) => {
const project = await getProjectBySlug(projectSlug);
.input(z.object({ event: z.string().optional(), projectId: z.string() }))
.query(async ({ input: { projectId, event } }) => {
const events = await cache.getOr(
`events_${project.id}_${event ?? 'all'}`,
`events_${projectId}_${event ?? 'all'}`,
1000 * 60 * 60,
() =>
db.event.findMany({
take: 500,
where: {
project_id: project.id,
project_id: projectId,
...(event
? {
name: event,
@@ -78,19 +76,16 @@ export const chartRouter = createTRPCRouter({
z.object({
event: z.string(),
property: z.string(),
projectSlug: z.string(),
projectId: z.string(),
})
)
.query(async ({ input: { event, property, projectSlug } }) => {
.query(async ({ input: { event, property, projectId } }) => {
const intervalInDays = 180;
const project = await getProjectBySlug(projectSlug);
if (isJsonPath(property)) {
const events = await db.$queryRawUnsafe<{ value: string }[]>(
`SELECT ${selectJsonPath(
property
)} AS value from events WHERE project_id = '${
project.id
}' AND name = '${event}' AND "createdAt" >= NOW() - INTERVAL '${intervalInDays} days'`
)} AS value from events WHERE project_id = '${projectId}' AND name = '${event}' AND "createdAt" >= NOW() - INTERVAL '${intervalInDays} days'`
);
return {
@@ -99,7 +94,7 @@ export const chartRouter = createTRPCRouter({
} else {
const events = await db.event.findMany({
where: {
project_id: project.id,
project_id: projectId,
name: event,
[property]: {
not: null,
@@ -123,8 +118,8 @@ export const chartRouter = createTRPCRouter({
}),
chart: protectedProcedure
.input(zChartInputWithDates.merge(z.object({ projectSlug: z.string() })))
.query(async ({ input: { projectSlug, events, ...input } }) => {
.input(zChartInputWithDates.merge(z.object({ projectId: z.string() })))
.query(async ({ input: { projectId, events, ...input } }) => {
const { startDate, endDate } =
input.startDate && input.endDate
? {
@@ -132,18 +127,16 @@ export const chartRouter = createTRPCRouter({
endDate: input.endDate,
}
: getDatesFromRange(input.range);
const project = await getProjectBySlug(projectSlug);
const series: Awaited<ReturnType<typeof getChartData>> = [];
for (const event of events) {
series.push(
...(await getChartData({
...input,
startDate,
endDate,
event,
projectId: project.id,
}))
);
const result = await getChartData({
...input,
startDate,
endDate,
event,
projectId: projectId,
});
series.push(...result);
}
const sorted = [...series].sort((a, b) => {
@@ -152,13 +145,18 @@ export const chartRouter = createTRPCRouter({
const sumB = b.data.reduce((acc, item) => acc + item.count, 0);
return sumB - sumA;
} else {
return b.metrics.total - a.metrics.total;
return b.metrics.sum - a.metrics.sum;
}
});
const meta = {
highest: sorted[0]?.metrics.total ?? 0,
lowest: last(sorted)?.metrics.total ?? 0,
const metrics = {
max: Math.max(...sorted.map((item) => item.metrics.max)),
min: Math.min(...sorted.map((item) => item.metrics.min)),
sum: sum(sorted.map((item) => item.metrics.sum, 0)),
averge: round(
average(sorted.map((item) => item.metrics.average, 0)),
2
),
};
return {
@@ -166,9 +164,9 @@ export const chartRouter = createTRPCRouter({
series.reduce(
(acc, item) => {
if (acc[item.event.id]) {
acc[item.event.id] += item.metrics.total;
acc[item.event.id] += item.metrics.sum;
} else {
acc[item.event.id] = item.metrics.total;
acc[item.event.id] = item.metrics.sum;
}
return acc;
},
@@ -180,8 +178,12 @@ export const chartRouter = createTRPCRouter({
})),
series: sorted.map((item) => ({
...item,
meta,
metrics: {
...item.metrics,
totalMetrics: metrics,
},
})),
metrics,
};
}),
});
@@ -282,36 +284,47 @@ async function getChartData(payload: IGetChartDataInput) {
);
return Object.keys(series).map((key) => {
const legend = payload.breakdowns.length
? key
: getEventLegend(payload.event);
const data = series[key] ?? [];
// If we have breakdowns, we want to use the breakdown key as the legend
// But only if it successfully broke it down, otherwise we use the getEventLabel
const legend =
payload.breakdowns.length && !alphabetIds.includes(key as 'A')
? key
: getEventLegend(payload.event);
const data =
payload.chartType === 'area' ||
payload.chartType === 'linear' ||
payload.chartType === 'histogram' ||
payload.chartType === 'metric'
? fillEmptySpotsInTimeline(
series[key] ?? [],
payload.interval,
payload.startDate,
payload.endDate
).map((item) => {
return {
label: legend,
count: round(item.count),
date: new Date(item.date).toISOString(),
};
})
: (series[key] ?? []).map((item) => ({
label: item.label,
count: round(item.count),
date: new Date(item.date).toISOString(),
}));
const counts = data.map((item) => item.count);
return {
name: legend,
event: {
id: payload.event.id,
name: payload.event.name,
},
event: payload.event,
metrics: {
total: sum(data.map((item) => item.count)),
average: round(average(data.map((item) => item.count))),
sum: sum(counts),
average: round(average(counts)),
max: Math.max(...counts),
min: Math.min(...counts),
},
data:
payload.chartType === 'linear' || payload.chartType === 'histogram'
? fillEmptySpotsInTimeline(
data,
payload.interval,
payload.startDate,
payload.endDate
).map((item) => {
return {
label: legend,
count: round(item.count),
date: new Date(item.date).toISOString(),
};
})
: [],
data,
};
});
}

View File

@@ -2,21 +2,19 @@ import { randomUUID } from 'crypto';
import { createTRPCRouter, protectedProcedure } from '@/server/api/trpc';
import { db } from '@/server/db';
import { hashPassword } from '@/server/services/hash.service';
import { getOrganizationBySlug } from '@/server/services/organization.service';
import { z } from 'zod';
export const clientRouter = createTRPCRouter({
list: protectedProcedure
.input(
z.object({
organizationSlug: z.string(),
organizationId: z.string(),
})
)
.query(async ({ input }) => {
const organization = await getOrganizationBySlug(input.organizationSlug);
.query(async ({ input: { organizationId } }) => {
return db.client.findMany({
where: {
organization_id: organization.id,
organization_id: organizationId,
},
include: {
project: true,
@@ -60,16 +58,15 @@ export const clientRouter = createTRPCRouter({
z.object({
name: z.string(),
projectId: z.string(),
organizationSlug: z.string(),
organizationId: z.string(),
withCors: z.boolean().default(true),
})
)
.mutation(async ({ input }) => {
const organization = await getOrganizationBySlug(input.organizationSlug);
const secret = randomUUID();
const client = await db.client.create({
data: {
organization_id: organization.id,
organization_id: input.organizationId,
project_id: input.projectId,
name: input.name,
secret: input.withCors ? null : await hashPassword(secret),

View File

@@ -1,75 +1,72 @@
import { createTRPCRouter, protectedProcedure } from '@/server/api/trpc';
import { db } from '@/server/db';
import { getDashboardBySlug } from '@/server/services/dashboard.service';
import { getProjectBySlug } from '@/server/services/project.service';
import { slug } from '@/utils/slug';
import { PrismaError } from 'prisma-error-enum';
import { z } from 'zod';
import { Prisma } from '@mixan/db';
import type { Prisma } from '@mixan/db';
export const dashboardRouter = createTRPCRouter({
get: protectedProcedure
.input(
z
.object({
slug: z.string(),
})
.or(z.object({ id: z.string() }))
)
.input(z.object({ id: z.string() }))
.query(async ({ input }) => {
if ('id' in input) {
return db.dashboard.findUnique({
where: {
id: input.id,
},
});
} else {
return getDashboardBySlug(input.slug);
}
return db.dashboard.findUnique({
where: {
id: input.id,
},
});
}),
list: protectedProcedure
.input(
z
.object({
projectSlug: z.string(),
projectId: z.string(),
})
.or(
z.object({
projectId: z.string(),
organizationId: z.string(),
})
)
)
.query(async ({ input }) => {
let projectId = null;
if ('projectId' in input) {
projectId = input.projectId;
return db.dashboard.findMany({
where: {
project_id: input.projectId,
},
orderBy: {
createdAt: 'desc',
},
include: {
project: true,
},
});
} else {
projectId = (await getProjectBySlug(input.projectSlug)).id;
return db.dashboard.findMany({
where: {
project: {
organization_id: input.organizationId,
},
},
include: {
project: true,
},
orderBy: {
createdAt: 'desc',
},
});
}
return db.dashboard.findMany({
where: {
project_id: projectId,
},
orderBy: {
createdAt: 'desc',
},
});
}),
create: protectedProcedure
.input(
z.object({
name: z.string(),
projectSlug: z.string(),
projectId: z.string(),
})
)
.mutation(async ({ input: { projectSlug, name } }) => {
const project = await getProjectBySlug(projectSlug);
.mutation(async ({ input: { projectId, name } }) => {
return db.dashboard.create({
data: {
slug: slug(name),
project_id: project.id,
project_id: projectId,
name,
},
});
@@ -95,17 +92,33 @@ export const dashboardRouter = createTRPCRouter({
.input(
z.object({
id: z.string(),
forceDelete: z.boolean().optional(),
})
)
.mutation(async ({ input: { id } }) => {
.mutation(async ({ input: { id, forceDelete } }) => {
try {
if (forceDelete) {
await db.report.deleteMany({
where: {
dashboard_id: id,
},
});
}
await db.recentDashboards.deleteMany({
where: {
dashboard_id: id,
},
});
await db.dashboard.delete({
where: {
id,
},
});
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
} catch (e) {
// Below does not work...
// error instanceof Prisma.PrismaClientKnownRequestError
if (typeof e === 'object' && e && 'code' in e) {
const error = e as Prisma.PrismaClientKnownRequestError;
switch (error.code) {
case PrismaError.ForeignConstraintViolation:
throw new Error(

View File

@@ -2,29 +2,37 @@ import { createTRPCRouter, protectedProcedure } from '@/server/api/trpc';
import { db } from '@/server/db';
import { z } from 'zod';
import type { Event, Profile } from '@mixan/db';
function transformEvent(
event: Event & {
profile: Profile;
}
) {
return {
...event,
properties: event.properties as Record<string, unknown>,
};
}
export const eventRouter = createTRPCRouter({
list: protectedProcedure
.input(
z.object({
projectSlug: z.string(),
projectId: z.string(),
take: z.number().default(100),
skip: z.number().default(0),
profileId: z.string().optional(),
events: z.array(z.string()).optional(),
})
)
.query(
async ({ input: { take, skip, projectSlug, profileId, events } }) => {
const project = await db.project.findUniqueOrThrow({
where: {
slug: projectSlug,
},
});
return db.event.findMany({
.query(async ({ input: { take, skip, projectId, profileId, events } }) => {
return db.event
.findMany({
take,
skip,
where: {
project_id: project.id,
project_id: projectId,
profile_id: profileId,
...(events && events.length > 0
? {
@@ -40,7 +48,7 @@ export const eventRouter = createTRPCRouter({
include: {
profile: true,
},
});
}
),
})
.then((events) => events.map(transformEvent));
}),
});

View File

@@ -1,10 +1,20 @@
import { createTRPCRouter, protectedProcedure } from '@/server/api/trpc';
import { db } from '@/server/db';
import { getOrganizationBySlug } from '@/server/services/organization.service';
import { slug } from '@/utils/slug';
import { getOrganizationById } from '@/server/services/organization.service';
import { z } from 'zod';
export const organizationRouter = createTRPCRouter({
list: protectedProcedure.query(({ ctx }) => {
return db.organization.findMany({
where: {
users: {
some: {
id: ctx.session.user.id,
},
},
},
});
}),
first: protectedProcedure.query(({ ctx }) => {
return db.organization.findFirst({
where: {
@@ -19,11 +29,11 @@ export const organizationRouter = createTRPCRouter({
get: protectedProcedure
.input(
z.object({
slug: z.string(),
id: z.string(),
})
)
.query(({ input }) => {
return getOrganizationBySlug(input.slug);
return getOrganizationById(input.id);
}),
update: protectedProcedure
.input(
@@ -39,7 +49,6 @@ export const organizationRouter = createTRPCRouter({
},
data: {
name: input.name,
slug: slug(input.name),
},
});
}),

View File

@@ -6,22 +6,42 @@ export const profileRouter = createTRPCRouter({
list: protectedProcedure
.input(
z.object({
projectSlug: z.string(),
query: z.string().nullable(),
projectId: z.string(),
take: z.number().default(100),
skip: z.number().default(0),
})
)
.query(async ({ input: { take, skip, projectSlug } }) => {
const project = await db.project.findUniqueOrThrow({
where: {
slug: projectSlug,
},
});
.query(async ({ input: { take, skip, projectId, query } }) => {
return db.profile.findMany({
take,
skip,
where: {
project_id: project.id,
project_id: projectId,
...(query
? {
OR: [
{
first_name: {
contains: query,
mode: 'insensitive',
},
},
{
last_name: {
contains: query,
mode: 'insensitive',
},
},
{
email: {
contains: query,
mode: 'insensitive',
},
},
],
}
: {}),
},
orderBy: {
createdAt: 'desc',

View File

@@ -1,40 +1,34 @@
import { createTRPCRouter, protectedProcedure } from '@/server/api/trpc';
import { db } from '@/server/db';
import { getOrganizationBySlug } from '@/server/services/organization.service';
import { getProjectBySlug } from '@/server/services/project.service';
import { db, getId } from '@/server/db';
import { slug } from '@/utils/slug';
import { z } from 'zod';
export const projectRouter = createTRPCRouter({
list: protectedProcedure
.input(
z.object({
organizationSlug: z.string(),
organizationId: z.string().nullable(),
})
)
.query(async ({ input }) => {
const organization = await getOrganizationBySlug(input.organizationSlug);
.query(async ({ input: { organizationId } }) => {
if (organizationId === null) return [];
return db.project.findMany({
where: {
organization_id: organization.id,
organization_id: organizationId,
},
});
}),
get: protectedProcedure
.input(
z
.object({
id: z.string(),
})
.or(z.object({ slug: z.string() }))
z.object({
id: z.string(),
})
)
.query(({ input }) => {
if ('slug' in input) {
return getProjectBySlug(input.slug);
}
.query(({ input: { id } }) => {
return db.project.findUniqueOrThrow({
where: {
id: input.id,
id,
},
});
}),
@@ -58,15 +52,15 @@ export const projectRouter = createTRPCRouter({
create: protectedProcedure
.input(
z.object({
name: z.string(),
organizationSlug: z.string(),
name: z.string().min(1),
organizationId: z.string(),
})
)
.mutation(async ({ input }) => {
const organization = await getOrganizationBySlug(input.organizationSlug);
return db.project.create({
data: {
organization_id: organization.id,
id: await getId('project', input.name),
organization_id: input.organizationId,
name: input.name,
},
});

View File

@@ -1,58 +1,9 @@
import { createTRPCRouter, protectedProcedure } from '@/server/api/trpc';
import { db } from '@/server/db';
import { getDashboardBySlug } from '@/server/services/dashboard.service';
import { getProjectBySlug } from '@/server/services/project.service';
import type {
IChartBreakdown,
IChartEvent,
IChartEventFilter,
IChartInput,
IChartRange,
} from '@/types';
import { alphabetIds, timeRanges } from '@/utils/constants';
import { transformReport } from '@/server/services/reports.service';
import { zChartInput } from '@/utils/validation';
import { z } from 'zod';
import type { Report as DbReport } from '@mixan/db';
function transformFilter(
filter: Partial<IChartEventFilter>,
index: number
): IChartEventFilter {
return {
id: filter.id ?? alphabetIds[index]!,
name: filter.name ?? 'Unknown Filter',
operator: filter.operator ?? 'is',
value:
typeof filter.value === 'string' ? [filter.value] : filter.value ?? [],
};
}
function transformEvent(
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,
};
}
function transformReport(report: DbReport): IChartInput & { id: string } {
return {
id: report.id,
events: (report.events as IChartEvent[]).map(transformEvent),
breakdowns: report.breakdowns as IChartBreakdown[],
chartType: report.chart_type,
interval: report.interval,
name: report.name || 'Untitled',
range: (report.range as IChartRange) ?? timeRanges['1m'],
};
}
export const reportRouter = createTRPCRouter({
get: protectedProcedure
.input(
@@ -72,22 +23,27 @@ export const reportRouter = createTRPCRouter({
list: protectedProcedure
.input(
z.object({
projectSlug: z.string(),
dashboardSlug: z.string(),
projectId: z.string(),
dashboardId: z.string(),
})
)
.query(async ({ input: { projectSlug, dashboardSlug } }) => {
const project = await getProjectBySlug(projectSlug);
const dashboard = await getDashboardBySlug(dashboardSlug);
const reports = await db.report.findMany({
where: {
project_id: project.id,
dashboard_id: dashboard.id,
},
orderBy: {
createdAt: 'desc',
},
});
.query(async ({ input: { projectId, dashboardId } }) => {
const [dashboard, reports] = await db.$transaction([
db.dashboard.findUniqueOrThrow({
where: {
id: dashboardId,
},
}),
db.report.findMany({
where: {
project_id: projectId,
dashboard_id: dashboardId,
},
orderBy: {
createdAt: 'desc',
},
}),
]);
return {
reports: reports.map(transformReport),
@@ -116,6 +72,7 @@ export const reportRouter = createTRPCRouter({
interval: report.interval,
breakdowns: report.breakdowns,
chart_type: report.chartType,
line_type: report.lineType,
range: report.range,
},
});
@@ -138,6 +95,7 @@ export const reportRouter = createTRPCRouter({
interval: report.interval,
breakdowns: report.breakdowns,
chart_type: report.chartType,
line_type: report.lineType,
range: report.range,
},
});

View File

@@ -56,4 +56,19 @@ export const userRouter = createTRPCRouter({
},
});
}),
invite: protectedProcedure
.input(
z.object({
email: z.string().email(),
organizationId: z.string(),
})
)
.mutation(async ({ input, ctx }) => {
await db.invite.create({
data: {
organization_id: input.organizationId,
email: input.email,
},
});
}),
});

View File

@@ -7,8 +7,7 @@
* need to use are documented accordingly near the end.
*/
import { getServerAuthSession } from '@/server/auth';
import { db } from '@/server/db';
import { getSession } from '@/server/auth';
import { initTRPC, TRPCError } from '@trpc/server';
import type { CreateNextContextOptions } from '@trpc/server/adapters/next';
import type { Session } from 'next-auth';
@@ -37,10 +36,9 @@ interface CreateContextOptions {
*
* @see https://create.t3.gg/en/usage/trpc#-serverapitrpcts
*/
const createInnerTRPCContext = (opts: CreateContextOptions) => {
export const createInnerTRPCContext = (opts: CreateContextOptions) => {
return {
session: opts.session,
db,
};
};
@@ -51,9 +49,8 @@ const createInnerTRPCContext = (opts: CreateContextOptions) => {
* @see https://trpc.io/docs/context
*/
export const createTRPCContext = async (opts: CreateNextContextOptions) => {
const { req, res } = opts;
// Get the session from the server using the getServerSession wrapper function
const session = await getServerAuthSession({ req, res });
const session = await getSession();
return createInnerTRPCContext({
session,