migrate to app dir and ssr
This commit is contained in:
@@ -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,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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));
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -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),
|
||||
},
|
||||
});
|
||||
}),
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import { cache } from 'react';
|
||||
import { db } from '@/server/db';
|
||||
import { verifyPassword } from '@/server/services/hash.service';
|
||||
import type {
|
||||
GetServerSidePropsContext,
|
||||
NextApiRequest,
|
||||
NextApiResponse,
|
||||
} from 'next';
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { getServerSession } from 'next-auth';
|
||||
import type { DefaultSession, NextAuthOptions } from 'next-auth';
|
||||
import Credentials from 'next-auth/providers/credentials';
|
||||
@@ -78,29 +75,12 @@ export const authOptions: NextAuthOptions = {
|
||||
};
|
||||
},
|
||||
}),
|
||||
/**
|
||||
* ...add more providers here.
|
||||
*
|
||||
* Most other providers require a bit more work than the Discord provider. For example, the
|
||||
* GitHub provider requires you to add the `refresh_token_expires_in` field to the Account
|
||||
* model. Refer to the NextAuth.js docs for the provider you want to use. Example:
|
||||
*
|
||||
* @see https://next-auth.js.org/providers/github
|
||||
*/
|
||||
],
|
||||
};
|
||||
|
||||
/**
|
||||
* Wrapper for `getServerSession` so that you don't need to import the `authOptions` in every file.
|
||||
*
|
||||
* @see https://next-auth.js.org/configuration/nextjs
|
||||
*/
|
||||
export const getServerAuthSession = (ctx: {
|
||||
req: GetServerSidePropsContext['req'];
|
||||
res: GetServerSidePropsContext['res'];
|
||||
}) => {
|
||||
return getServerSession(ctx.req, ctx.res, authOptions);
|
||||
};
|
||||
export const getSession = cache(
|
||||
async () => await getServerSession(authOptions)
|
||||
);
|
||||
|
||||
export async function validateSdkRequest(
|
||||
req: NextApiRequest,
|
||||
|
||||
@@ -1 +1,40 @@
|
||||
import { slug } from '@/utils/slug';
|
||||
|
||||
import { db } from '@mixan/db';
|
||||
|
||||
export { db } from '@mixan/db';
|
||||
|
||||
export async function getId(
|
||||
tableName: 'project' | 'organization' | 'dashboard',
|
||||
name: string
|
||||
) {
|
||||
const newId = slug(name);
|
||||
if (!db[tableName]) {
|
||||
throw new Error('Table does not exists');
|
||||
}
|
||||
|
||||
if (!('findUnique' in db[tableName])) {
|
||||
throw new Error('findUnique does not exists');
|
||||
}
|
||||
|
||||
// @ts-expect-error
|
||||
const existingProject = await db[tableName]!.findUnique({
|
||||
where: {
|
||||
id: newId,
|
||||
},
|
||||
});
|
||||
|
||||
function random(str: string) {
|
||||
const numbers = Math.floor(1000 + Math.random() * 9000);
|
||||
if (str.match(/-\d{4}$/g)) {
|
||||
return str.replace(/-\d{4}$/g, `-${numbers}`);
|
||||
}
|
||||
return `${str}-${numbers}`;
|
||||
}
|
||||
|
||||
if (existingProject) {
|
||||
return getId(tableName, random(name));
|
||||
}
|
||||
|
||||
return newId;
|
||||
}
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
import type { GetServerSidePropsContext, GetServerSidePropsResult } from 'next';
|
||||
|
||||
import { getServerAuthSession } from './auth';
|
||||
import { db } from './db';
|
||||
|
||||
export function createServerSideProps(
|
||||
cb?: (context: GetServerSidePropsContext) => Promise<any>
|
||||
) {
|
||||
return async function getServerSideProps(
|
||||
context: GetServerSidePropsContext
|
||||
): Promise<GetServerSidePropsResult<any>> {
|
||||
const session = await getServerAuthSession(context);
|
||||
|
||||
if (!session) {
|
||||
return {
|
||||
redirect: {
|
||||
destination: '/api/auth/signin',
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (context.params?.organization) {
|
||||
const user = await db.user.findFirst({
|
||||
where: {
|
||||
id: session.user.id,
|
||||
organization: {
|
||||
slug: context.params.organization as string,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return {
|
||||
notFound: true,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
const user = await db.user.findFirst({
|
||||
where: {
|
||||
id: session.user.id,
|
||||
},
|
||||
include: {
|
||||
organization: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return {
|
||||
notFound: true,
|
||||
};
|
||||
}
|
||||
|
||||
if (user.organization) {
|
||||
return {
|
||||
redirect: {
|
||||
destination: `/${user.organization.slug}`,
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const res = await (typeof cb === 'function'
|
||||
? cb(context)
|
||||
: Promise.resolve({}));
|
||||
return {
|
||||
...(res ?? {}),
|
||||
props: {
|
||||
session,
|
||||
...(res?.props ?? {}),
|
||||
},
|
||||
};
|
||||
};
|
||||
}
|
||||
12
apps/web/src/server/services/clients.service.ts
Normal file
12
apps/web/src/server/services/clients.service.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { db } from '@mixan/db';
|
||||
|
||||
export function getClientsByOrganizationId(organizationId: string) {
|
||||
return db.client.findMany({
|
||||
where: {
|
||||
organization_id: organizationId,
|
||||
},
|
||||
include: {
|
||||
project: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1,9 +1,86 @@
|
||||
import { unstable_cache } from 'next/cache';
|
||||
|
||||
import { db } from '../db';
|
||||
|
||||
export function getDashboardBySlug(slug: string) {
|
||||
export type IServiceRecentDashboards = Awaited<
|
||||
ReturnType<typeof getRecentDashboardsByUserId>
|
||||
>;
|
||||
export type IServiceDashboard = Awaited<ReturnType<typeof getDashboardById>>;
|
||||
export type IServiceDashboardWithProject = Awaited<
|
||||
ReturnType<typeof getDashboardsByProjectId>
|
||||
>[number];
|
||||
|
||||
export function getDashboardById(id: string) {
|
||||
return db.dashboard.findUniqueOrThrow({
|
||||
where: {
|
||||
slug,
|
||||
id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function getDashboardsByProjectId(projectId: string) {
|
||||
return db.dashboard.findMany({
|
||||
where: {
|
||||
project_id: projectId,
|
||||
},
|
||||
include: {
|
||||
project: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function getRecentDashboardsByUserId(userId: string) {
|
||||
const tag = `recentDashboards_${userId}`;
|
||||
|
||||
return unstable_cache(
|
||||
async (userId: string) => {
|
||||
return db.recentDashboards.findMany({
|
||||
where: {
|
||||
user_id: userId,
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
include: {
|
||||
project: true,
|
||||
dashboard: true,
|
||||
},
|
||||
take: 5,
|
||||
});
|
||||
},
|
||||
tag.split('_'),
|
||||
{
|
||||
revalidate: 3600,
|
||||
tags: [tag],
|
||||
}
|
||||
)(userId);
|
||||
}
|
||||
|
||||
export async function createRecentDashboard({
|
||||
organizationId,
|
||||
projectId,
|
||||
dashboardId,
|
||||
userId,
|
||||
}: {
|
||||
organizationId: string;
|
||||
projectId: string;
|
||||
dashboardId: string;
|
||||
userId: string;
|
||||
}) {
|
||||
await db.recentDashboards.deleteMany({
|
||||
where: {
|
||||
user_id: userId,
|
||||
project_id: projectId,
|
||||
dashboard_id: dashboardId,
|
||||
organization_id: organizationId,
|
||||
},
|
||||
});
|
||||
return db.recentDashboards.create({
|
||||
data: {
|
||||
user_id: userId,
|
||||
organization_id: organizationId,
|
||||
project_id: projectId,
|
||||
dashboard_id: dashboardId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,9 +1,25 @@
|
||||
import { db } from '../db';
|
||||
|
||||
export function getOrganizationBySlug(slug: string) {
|
||||
return db.organization.findUniqueOrThrow({
|
||||
export type IServiceOrganization = Awaited<
|
||||
ReturnType<typeof getOrganizations>
|
||||
>[number];
|
||||
|
||||
export function getOrganizations() {
|
||||
return db.organization.findMany({
|
||||
where: {
|
||||
slug,
|
||||
// users: {
|
||||
// some: {
|
||||
// id: '1',
|
||||
// },
|
||||
// }
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function getOrganizationById(id: string) {
|
||||
return db.organization.findUniqueOrThrow({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,6 +1,32 @@
|
||||
import { db } from '@/server/db';
|
||||
import { HttpError } from '@/server/exceptions';
|
||||
|
||||
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: {
|
||||
|
||||
@@ -1,9 +1,44 @@
|
||||
import { unstable_cache } from 'next/cache';
|
||||
|
||||
import { db } from '../db';
|
||||
|
||||
export function getProjectBySlug(slug: string) {
|
||||
return db.project.findUniqueOrThrow({
|
||||
export type IServiceProject = Awaited<ReturnType<typeof getProjectById>>;
|
||||
|
||||
export function getProjectById(id: string) {
|
||||
return db.project.findUnique({
|
||||
where: {
|
||||
slug,
|
||||
id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function getProjectsByOrganizationId(organizationId: string) {
|
||||
return db.project.findMany({
|
||||
where: {
|
||||
organization_id: organizationId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function getFirstProjectByOrganizationId(organizationId: string) {
|
||||
const tag = `getFirstProjectByOrganizationId_${organizationId}`;
|
||||
return unstable_cache(
|
||||
async (organizationId: string) => {
|
||||
return db.project.findFirst({
|
||||
where: {
|
||||
organization_id: organizationId,
|
||||
},
|
||||
orderBy: {
|
||||
events: {
|
||||
_count: 'desc',
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
tag.split('_'),
|
||||
{
|
||||
tags: [tag],
|
||||
revalidate: 3600 * 24,
|
||||
}
|
||||
)(organizationId);
|
||||
}
|
||||
|
||||
75
apps/web/src/server/services/reports.service.ts
Normal file
75
apps/web/src/server/services/reports.service.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import type {
|
||||
IChartBreakdown,
|
||||
IChartEvent,
|
||||
IChartEventFilter,
|
||||
IChartInput,
|
||||
IChartLineType,
|
||||
IChartRange,
|
||||
} from '@/types';
|
||||
import { alphabetIds, timeRanges } from '@/utils/constants';
|
||||
|
||||
import { db } from '@mixan/db';
|
||||
import type { Report as DbReport } from '@mixan/db';
|
||||
|
||||
export type IServiceReport = Awaited<ReturnType<typeof getReportById>>;
|
||||
|
||||
export 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 ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
export 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,
|
||||
};
|
||||
}
|
||||
|
||||
export 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,
|
||||
lineType: (report.line_type ?? 'kuk') as IChartLineType,
|
||||
interval: report.interval,
|
||||
name: report.name || 'Untitled',
|
||||
range: (report.range as IChartRange) ?? timeRanges['1m'],
|
||||
};
|
||||
}
|
||||
|
||||
export function getReportsByDashboardId(dashboardId: string) {
|
||||
return db.report
|
||||
.findMany({
|
||||
where: {
|
||||
dashboard_id: dashboardId,
|
||||
},
|
||||
})
|
||||
.then((reports) => reports.map(transformReport));
|
||||
}
|
||||
|
||||
export function getReportById(id: string) {
|
||||
return db.report
|
||||
.findUniqueOrThrow({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
})
|
||||
.then(transformReport);
|
||||
}
|
||||
20
apps/web/src/server/services/user.service.ts
Normal file
20
apps/web/src/server/services/user.service.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { db } from '@/server/db';
|
||||
|
||||
export function getUserById(id: string) {
|
||||
return db.user.findUniqueOrThrow({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export type IServiceInvite = Awaited<
|
||||
ReturnType<typeof getInvitesByOrganizationId>
|
||||
>[number];
|
||||
export function getInvitesByOrganizationId(organizationId: string) {
|
||||
return db.invite.findMany({
|
||||
where: {
|
||||
organization_id: organizationId,
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user