diff --git a/apps/openpage-api/app/growth/new-users/route.ts b/apps/openpage-api/app/growth/new-users/route.ts new file mode 100644 index 000000000..460adf9da --- /dev/null +++ b/apps/openpage-api/app/growth/new-users/route.ts @@ -0,0 +1,25 @@ +import cors from '@/lib/cors'; +import { getUserMonthlyGrowth } from '@/lib/growth/get-user-monthly-growth'; + +export async function GET(request: Request) { + const monthlyUsers = await getUserMonthlyGrowth(); + + return cors( + request, + new Response(JSON.stringify(monthlyUsers), { + status: 200, + headers: { + 'content-type': 'application/json', + }, + }), + ); +} + +export function OPTIONS(request: Request) { + return cors( + request, + new Response(null, { + status: 204, + }), + ); +} diff --git a/apps/openpage-api/app/growth/route.ts b/apps/openpage-api/app/growth/route.ts new file mode 100644 index 000000000..091d4ea70 --- /dev/null +++ b/apps/openpage-api/app/growth/route.ts @@ -0,0 +1,37 @@ +import type { NextRequest } from 'next/server'; + +import cors from '@/lib/cors'; + +const paths = [ + { path: '/total-customers', description: 'Total Customers' }, + { path: '/total-users', description: 'Total Users' }, + { path: '/new-users', description: 'New Users' }, + { path: '/completed-documents', description: 'Completed Documents per Month' }, + { path: '/total-completed-documents', description: 'Total Completed Documents' }, +]; + +export function GET(request: NextRequest) { + const url = request.nextUrl.toString(); + const apis = paths.map(({ path, description }) => { + return { path: url + path, description }; + }); + + return cors( + request, + new Response(JSON.stringify(apis), { + status: 200, + headers: { + 'content-type': 'application/json', + }, + }), + ); +} + +export function OPTIONS(request: Request) { + return cors( + request, + new Response(null, { + status: 204, + }), + ); +} diff --git a/apps/openpage-api/app/growth/total-users/route.ts b/apps/openpage-api/app/growth/total-users/route.ts new file mode 100644 index 000000000..b3fbe62bc --- /dev/null +++ b/apps/openpage-api/app/growth/total-users/route.ts @@ -0,0 +1,25 @@ +import cors from '@/lib/cors'; +import { getUserMonthlyGrowth } from '@/lib/growth/get-user-monthly-growth'; + +export async function GET(request: Request) { + const totalUsers = await getUserMonthlyGrowth(); + + return cors( + request, + new Response(JSON.stringify(totalUsers), { + status: 200, + headers: { + 'content-type': 'application/json', + }, + }), + ); +} + +export function OPTIONS(request: Request) { + return cors( + request, + new Response(null, { + status: 204, + }), + ); +} diff --git a/apps/openpage-api/app/route.ts b/apps/openpage-api/app/route.ts index b218cb0b9..47d2d4ba2 100644 --- a/apps/openpage-api/app/route.ts +++ b/apps/openpage-api/app/route.ts @@ -5,6 +5,7 @@ import cors from '@/lib/cors'; const paths = [ { path: 'github', description: 'GitHub Data' }, { path: 'community', description: 'Community Data' }, + { path: 'growth', description: 'Growth Data' }, ]; export function GET(request: NextRequest) { diff --git a/apps/openpage-api/lib/growth/get-user-monthly-growth.ts b/apps/openpage-api/lib/growth/get-user-monthly-growth.ts new file mode 100644 index 000000000..6d4e526cb --- /dev/null +++ b/apps/openpage-api/lib/growth/get-user-monthly-growth.ts @@ -0,0 +1,38 @@ +import { DateTime } from 'luxon'; + +import { kyselyPrisma, sql } from '@documenso/prisma'; + +export const getUserMonthlyGrowth = async (type: 'count' | 'cumulative' = 'count') => { + const qb = kyselyPrisma.$kysely + .selectFrom('User') + .select(({ fn }) => [ + fn('DATE_TRUNC', [sql.lit('MONTH'), 'User.createdAt']).as('month'), + fn.count('id').as('count'), + fn + .sum(fn.count('id')) + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions, @typescript-eslint/no-explicit-any + .over((ob) => ob.orderBy(fn('DATE_TRUNC', [sql.lit('MONTH'), 'User.createdAt']) as any)) + .as('cume_count'), + ]) + .groupBy('month') + .orderBy('month', 'desc') + .limit(12); + + const result = await qb.execute(); + + const transformedData = { + labels: result.map((row) => DateTime.fromJSDate(row.month).toFormat('MMM yyyy')).reverse(), + datasets: [ + { + label: type === 'count' ? 'New Users' : 'Total Users', + data: result + .map((row) => (type === 'count' ? Number(row.count) : Number(row.cume_count))) + .reverse(), + }, + ], + }; + + return transformedData; +}; + +export type GetUserMonthlyGrowthResult = Awaited>; diff --git a/package-lock.json b/package-lock.json index 7a5d2e30c..f636360eb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,8 +13,10 @@ ], "dependencies": { "@documenso/pdf-sign": "^0.1.0", + "@documenso/prisma": "^0.0.0", "@lingui/core": "^4.11.3", "inngest-cli": "^0.29.1", + "luxon": "^3.5.0", "next-runtime-env": "^3.2.0", "react": "^18" }, @@ -22279,9 +22281,9 @@ } }, "node_modules/luxon": { - "version": "3.4.4", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz", - "integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==", + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.5.0.tgz", + "integrity": "sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==", "engines": { "node": ">=12" } diff --git a/package.json b/package.json index f29cf2e45..a85613116 100644 --- a/package.json +++ b/package.json @@ -64,8 +64,10 @@ ], "dependencies": { "@documenso/pdf-sign": "^0.1.0", + "@documenso/prisma": "^0.0.0", "@lingui/core": "^4.11.3", "inngest-cli": "^0.29.1", + "luxon": "^3.5.0", "next-runtime-env": "^3.2.0", "react": "^18" },