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,

View File

@@ -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,

View File

@@ -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;
}

View File

@@ -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 ?? {}),
},
};
};
}

View 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,
},
});
}

View File

@@ -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,
},
});
}

View File

@@ -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,
},
});
}

View File

@@ -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: {

View File

@@ -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);
}

View 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);
}

View 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,
},
});
}