diff --git a/apps/api/package.json b/apps/api/package.json index 4819f1b3..1debcc8a 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -12,12 +12,16 @@ }, "dependencies": { "@baselime/pino-transport": "^0.1.5", + "@clerk/fastify": "^1.0.0", + "@fastify/cookie": "^9.3.1", "@fastify/cors": "^9.0.0", "@fastify/websocket": "^8.3.1", "@openpanel/common": "workspace:*", "@openpanel/db": "workspace:*", "@openpanel/queue": "workspace:*", "@openpanel/redis": "workspace:*", + "@openpanel/trpc": "workspace:*", + "@trpc/server": "^10.45.1", "fastify": "^4.25.2", "ico-to-png": "^0.2.1", "pino": "^8.17.2", @@ -28,8 +32,7 @@ "sqlstring": "^2.3.3", "superjson": "^1.13.3", "ua-parser-js": "^1.0.37", - "url-metadata": "^4.1.0", - "uuid": "^9.0.1" + "url-metadata": "^4.1.0" }, "devDependencies": { "@openpanel/eslint-config": "workspace:*", diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index 88f591dc..4493255b 100644 --- a/apps/api/src/index.ts +++ b/apps/api/src/index.ts @@ -1,8 +1,14 @@ +import { clerkPlugin } from '@clerk/fastify'; +import cookie from '@fastify/cookie'; import cors from '@fastify/cors'; +import type { FastifyTRPCPluginOptions } from '@trpc/server/adapters/fastify'; +import { fastifyTRPCPlugin } from '@trpc/server/adapters/fastify'; import Fastify from 'fastify'; import type { IServiceClient } from '@openpanel/db'; import { redisPub } from '@openpanel/redis'; +import type { AppRouter } from '@openpanel/trpc'; +import { appRouter, createContext } from '@openpanel/trpc'; import eventRouter from './routes/event.router'; import exportRouter from './routes/export.router'; @@ -23,10 +29,47 @@ const port = parseInt(process.env.API_PORT || '3000', 10); const startServer = async () => { logInfo('Starting server'); try { - const fastify = Fastify(); + const fastify = Fastify({ + maxParamLength: 5000, + }); + + const origin = []; + if (process.env.NODE_ENV === 'production') { + if (process.env.NEXT_PUBLIC_DASHBOARD_URL) { + origin.push(process.env.NEXT_PUBLIC_DASHBOARD_URL); + } + } else { + origin.push('http://localhost:3000'); + } fastify.register(cors, { - origin: '*', + origin, + credentials: true, + }); + + fastify.register(cookie, { + secret: 'random', // for cookies signature + hook: 'onRequest', + }); + + fastify.register(clerkPlugin, { + publishableKey: process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY, + secretKey: process.env.CLERK_SECRET_KEY, + }); + + fastify.register(fastifyTRPCPlugin, { + prefix: '/trpc', + trpcOptions: { + router: appRouter, + createContext: createContext, + onError(error: unknown) { + if (error instanceof Error) { + logger.error(error, error.message); + } else { + logger.error(error, 'Unknown error trpc error'); + } + }, + } satisfies FastifyTRPCPluginOptions['trpcOptions'], }); fastify.decorateRequest('projectId', ''); diff --git a/apps/dashboard/package.json b/apps/dashboard/package.json index b5fb8baf..631e2ffb 100644 --- a/apps/dashboard/package.json +++ b/apps/dashboard/package.json @@ -109,6 +109,7 @@ "@openpanel/eslint-config": "workspace:*", "@openpanel/prettier-config": "workspace:*", "@openpanel/tsconfig": "workspace:*", + "@openpanel/trpc": "workspace:*", "@prisma/nextjs-monorepo-workaround-plugin": "^5.12.1", "@types/bcrypt": "^5.0.2", "@types/lodash.debounce": "^4.0.9", diff --git a/apps/dashboard/src/app/api/trpc/[trpc]/route.ts b/apps/dashboard/src/app/api/trpc/[trpc]/route.ts deleted file mode 100644 index d6c26d34..00000000 --- a/apps/dashboard/src/app/api/trpc/[trpc]/route.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { appRouter } from '@/trpc/api/root'; -import { getAuth } from '@clerk/nextjs/server'; -import { fetchRequestHandler } from '@trpc/server/adapters/fetch'; - -const handler = (req: Request) => - fetchRequestHandler({ - endpoint: '/api/trpc', - req, - router: appRouter, - createContext({ req }) { - const session = getAuth(req as any); - return { - session, - }; - }, - onError(opts) { - const { error, type, path, input, ctx, req } = opts; - console.error('---- TRPC ERROR'); - console.error('Error:', error); - console.error('Context:', ctx); - console.error(); - }, - }); - -export { handler as GET, handler as POST }; diff --git a/apps/dashboard/src/app/providers.tsx b/apps/dashboard/src/app/providers.tsx index f2b08ee9..27115a12 100644 --- a/apps/dashboard/src/app/providers.tsx +++ b/apps/dashboard/src/app/providers.tsx @@ -35,7 +35,15 @@ function AllProviders({ children }: { children: React.ReactNode }) { transformer: superjson, links: [ httpLink({ - url: `${process.env.NEXT_PUBLIC_DASHBOARD_URL}/api/trpc`, + url: `${process.env.NEXT_PUBLIC_API_URL}/trpc`, + fetch(url, options) { + // Send cookies + return fetch(url, { + ...options, + credentials: 'include', + mode: 'cors', + }); + }, }), ], }) diff --git a/apps/dashboard/src/components/report/chart/ChartProvider.tsx b/apps/dashboard/src/components/report/chart/ChartProvider.tsx index cfe8799d..385e4eb4 100644 --- a/apps/dashboard/src/components/report/chart/ChartProvider.tsx +++ b/apps/dashboard/src/components/report/chart/ChartProvider.tsx @@ -9,8 +9,8 @@ import { useMemo, useState, } from 'react'; -import type { IChartSerie } from '@/trpc/api/routers/chart'; +import type { IChartSerie } from '@openpanel/trpc/src/routers/chart'; import type { IChartInput } from '@openpanel/validation'; import { ChartLoading } from './ChartLoading'; diff --git a/apps/dashboard/src/components/report/funnel/Funnel.old.tsx b/apps/dashboard/src/components/report/funnel/Funnel.old.tsx index 7f50d4f9..7b2c1938 100644 --- a/apps/dashboard/src/components/report/funnel/Funnel.old.tsx +++ b/apps/dashboard/src/components/report/funnel/Funnel.old.tsx @@ -1,3 +1,5 @@ +// @ts-nocheck + 'use client'; import { diff --git a/apps/dashboard/src/instrumentation.ts b/apps/dashboard/src/instrumentation.ts index da260698..d3c7db2f 100644 --- a/apps/dashboard/src/instrumentation.ts +++ b/apps/dashboard/src/instrumentation.ts @@ -1,7 +1,10 @@ /* eslint-disable */ export async function register() { - if (process.env.NEXT_RUNTIME === 'nodejs') { + if ( + process.env.NODE_ENV === 'production' && + process.env.NEXT_RUNTIME === 'nodejs' + ) { const { BaselimeSDK, VercelPlugin, BetterHttpInstrumentation } = // @ts-expect-error await import('@baselime/node-opentelemetry'); diff --git a/apps/dashboard/src/trpc/api/routers/user.ts b/apps/dashboard/src/trpc/api/routers/user.ts deleted file mode 100644 index 16aedcd0..00000000 --- a/apps/dashboard/src/trpc/api/routers/user.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { createTRPCRouter, protectedProcedure } from '@/trpc/api/trpc'; -import { clerkClient } from '@clerk/nextjs'; -import { z } from 'zod'; - -import { transformUser } from '@openpanel/db'; - -export const userRouter = createTRPCRouter({ - update: protectedProcedure - .input( - z.object({ - firstName: z.string(), - lastName: z.string(), - }) - ) - .mutation(({ input, ctx }) => { - return clerkClient.users - .updateUser(ctx.session.userId, { - firstName: input.firstName, - lastName: input.lastName, - }) - .then(transformUser); - }), -}); diff --git a/apps/dashboard/src/trpc/client.tsx b/apps/dashboard/src/trpc/client.tsx index cde4a612..729f60a5 100644 --- a/apps/dashboard/src/trpc/client.tsx +++ b/apps/dashboard/src/trpc/client.tsx @@ -1,10 +1,11 @@ -import type { AppRouter } from '@/trpc/api/root'; import type { TRPCClientErrorBase } from '@trpc/react-query'; import { createTRPCReact } from '@trpc/react-query'; import type { inferRouterInputs, inferRouterOutputs } from '@trpc/server'; import type { ExternalToast } from 'sonner'; import { toast } from 'sonner'; +import type { AppRouter } from '@openpanel/trpc'; + export const api = createTRPCReact({}); /** diff --git a/apps/dashboard/src/utils/math.ts b/apps/dashboard/src/utils/math.ts index c24eb461..23ea0296 100644 --- a/apps/dashboard/src/utils/math.ts +++ b/apps/dashboard/src/utils/math.ts @@ -1,26 +1 @@ -import { isNumber } from 'mathjs'; - -export const round = (num: number, decimals = 2) => { - const factor = Math.pow(10, decimals); - return Math.round((num + Number.EPSILON) * factor) / factor; -}; - -export const average = (arr: (number | null)[]) => { - const filtered = arr.filter( - (n): n is number => - isNumber(n) && !Number.isNaN(n) && Number.isFinite(n) && n !== 0 - ); - const avg = filtered.reduce((p, c) => p + c, 0) / filtered.length; - return Number.isNaN(avg) ? 0 : avg; -}; - -export const sum = (arr: (number | null)[]): number => - round(arr.filter(isNumber).reduce((acc, item) => acc + item, 0)); - -export const min = (arr: (number | null)[]): number => - Math.min(...arr.filter(isNumber)); - -export const max = (arr: (number | null)[]): number => - Math.max(...arr.filter(isNumber)); - -export const isFloat = (n: number) => n % 1 !== 0; +export * from '@openpanel/common/src/math' \ No newline at end of file diff --git a/apps/dashboard/src/utils/slug.ts b/apps/dashboard/src/utils/slug.ts index 2c448002..ebdad3b9 100644 --- a/apps/dashboard/src/utils/slug.ts +++ b/apps/dashboard/src/utils/slug.ts @@ -1,18 +1 @@ -import _slugify from 'slugify'; - -const slugify = (str: string) => { - return _slugify( - str - .replace('å', 'a') - .replace('ä', 'a') - .replace('ö', 'o') - .replace('Å', 'A') - .replace('Ä', 'A') - .replace('Ö', 'O'), - { lower: true, strict: true, trim: true } - ); -}; - -export function slug(str: string): string { - return slugify(str); -} +export * from '@openpanel/common/src/slug'; diff --git a/packages/common/index.ts b/packages/common/index.ts index 215dd315..081f50fb 100644 --- a/packages/common/index.ts +++ b/packages/common/index.ts @@ -4,3 +4,5 @@ export * from './src/date'; export * from './src/object'; export * from './src/names'; export * from './src/string'; +export * from './src/math'; +export * from './src/slug'; diff --git a/packages/common/package.json b/packages/common/package.json index b1558e57..45b35728 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -8,7 +8,9 @@ "typecheck": "tsc --noEmit" }, "dependencies": { + "mathjs": "^12.3.2", "ramda": "^0.29.1", + "slugify": "^1.6.6", "superjson": "^1.13.3", "unique-names-generator": "^4.7.1" }, diff --git a/packages/common/src/math.ts b/packages/common/src/math.ts new file mode 100644 index 00000000..c24eb461 --- /dev/null +++ b/packages/common/src/math.ts @@ -0,0 +1,26 @@ +import { isNumber } from 'mathjs'; + +export const round = (num: number, decimals = 2) => { + const factor = Math.pow(10, decimals); + return Math.round((num + Number.EPSILON) * factor) / factor; +}; + +export const average = (arr: (number | null)[]) => { + const filtered = arr.filter( + (n): n is number => + isNumber(n) && !Number.isNaN(n) && Number.isFinite(n) && n !== 0 + ); + const avg = filtered.reduce((p, c) => p + c, 0) / filtered.length; + return Number.isNaN(avg) ? 0 : avg; +}; + +export const sum = (arr: (number | null)[]): number => + round(arr.filter(isNumber).reduce((acc, item) => acc + item, 0)); + +export const min = (arr: (number | null)[]): number => + Math.min(...arr.filter(isNumber)); + +export const max = (arr: (number | null)[]): number => + Math.max(...arr.filter(isNumber)); + +export const isFloat = (n: number) => n % 1 !== 0; diff --git a/packages/common/src/slug.ts b/packages/common/src/slug.ts new file mode 100644 index 00000000..2c448002 --- /dev/null +++ b/packages/common/src/slug.ts @@ -0,0 +1,18 @@ +import _slugify from 'slugify'; + +const slugify = (str: string) => { + return _slugify( + str + .replace('å', 'a') + .replace('ä', 'a') + .replace('ö', 'o') + .replace('Å', 'A') + .replace('Ä', 'A') + .replace('Ö', 'O'), + { lower: true, strict: true, trim: true } + ); +}; + +export function slug(str: string): string { + return slugify(str); +} diff --git a/packages/db/index.ts b/packages/db/index.ts index efd5626d..6f29807c 100644 --- a/packages/db/index.ts +++ b/packages/db/index.ts @@ -13,3 +13,4 @@ export * from './src/services/salt.service'; export * from './src/services/share.service'; export * from './src/services/user.service'; export * from './src/services/reference.service'; +export * from './src/services/id.service'; diff --git a/packages/db/src/services/id.service.ts b/packages/db/src/services/id.service.ts new file mode 100644 index 00000000..8b183a1c --- /dev/null +++ b/packages/db/src/services/id.service.ts @@ -0,0 +1,35 @@ +import { slug } from '@openpanel/common'; + +import { db } from '../prisma-client'; + +export async function getId(tableName: 'project' | '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; +} diff --git a/packages/trpc/index.ts b/packages/trpc/index.ts new file mode 100644 index 00000000..15db407c --- /dev/null +++ b/packages/trpc/index.ts @@ -0,0 +1,2 @@ +export * from './src/root'; +export * from './src/trpc'; diff --git a/packages/trpc/package.json b/packages/trpc/package.json new file mode 100644 index 00000000..e14ee1c9 --- /dev/null +++ b/packages/trpc/package.json @@ -0,0 +1,46 @@ +{ + "name": "@openpanel/trpc", + "version": "0.0.1", + "main": "index.ts", + "scripts": { + "lint": "eslint .", + "format": "prettier --check \"**/*.{mjs,ts,md,json}\"", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@clerk/fastify": "^1.0.0", + "@openpanel/common": "workspace:*", + "@openpanel/constants": "workspace:*", + "@openpanel/db": "workspace:*", + "@openpanel/validation": "workspace:*", + "@trpc/server": "^10.45.1", + "date-fns": "^3.3.1", + "mathjs": "^12.3.2", + "prisma-error-enum": "^0.1.3", + "ramda": "^0.29.1", + "short-unique-id": "^5.0.3", + "sqlstring": "^2.3.3", + "superjson": "^1.13.3", + "uuid": "^9.0.1", + "zod": "^3.22.4" + }, + "devDependencies": { + "@openpanel/eslint-config": "workspace:*", + "@openpanel/prettier-config": "workspace:*", + "@openpanel/tsconfig": "workspace:*", + "@types/node": "^18.16.0", + "@types/ramda": "^0.29.6", + "@types/sqlstring": "^2.3.2", + "eslint": "^8.48.0", + "prettier": "^3.0.3", + "prisma": "^5.1.1", + "typescript": "^5.2.2" + }, + "eslintConfig": { + "root": true, + "extends": [ + "@openpanel/eslint-config/base" + ] + }, + "prettier": "@openpanel/prettier-config" +} diff --git a/apps/dashboard/src/trpc/api/root.ts b/packages/trpc/src/root.ts similarity index 95% rename from apps/dashboard/src/trpc/api/root.ts rename to packages/trpc/src/root.ts index 30a5907d..fbd3af14 100644 --- a/apps/dashboard/src/trpc/api/root.ts +++ b/packages/trpc/src/root.ts @@ -1,5 +1,3 @@ -import { createTRPCRouter } from '@/trpc/api/trpc'; - import { chartRouter } from './routers/chart'; import { clientRouter } from './routers/client'; import { dashboardRouter } from './routers/dashboard'; @@ -12,6 +10,7 @@ import { referenceRouter } from './routers/reference'; import { reportRouter } from './routers/report'; import { shareRouter } from './routers/share'; import { userRouter } from './routers/user'; +import { createTRPCRouter } from './trpc'; /** * This is the primary router for your server. diff --git a/apps/dashboard/src/trpc/api/routers/chart.helpers.ts b/packages/trpc/src/routers/chart.helpers.ts similarity index 99% rename from apps/dashboard/src/trpc/api/routers/chart.helpers.ts rename to packages/trpc/src/routers/chart.helpers.ts index 830ddddf..08adeb1e 100644 --- a/apps/dashboard/src/trpc/api/routers/chart.helpers.ts +++ b/packages/trpc/src/routers/chart.helpers.ts @@ -1,9 +1,9 @@ -import { round } from '@/utils/math'; import { subDays } from 'date-fns'; import * as mathjs from 'mathjs'; import { repeat, reverse, sort } from 'ramda'; import { escape } from 'sqlstring'; +import { round } from '@openpanel/common'; import { alphabetIds, NOT_SET_VALUE } from '@openpanel/constants'; import { chQuery, @@ -13,7 +13,6 @@ import { getChartSql, getEventFiltersWhereClause, getProfiles, - transformProfile, } from '@openpanel/db'; import type { IChartEvent, diff --git a/apps/dashboard/src/trpc/api/routers/chart.ts b/packages/trpc/src/routers/chart.ts similarity index 98% rename from apps/dashboard/src/trpc/api/routers/chart.ts rename to packages/trpc/src/routers/chart.ts index 3e3a923f..bdba5387 100644 --- a/apps/dashboard/src/trpc/api/routers/chart.ts +++ b/packages/trpc/src/routers/chart.ts @@ -1,17 +1,13 @@ -import { - createTRPCRouter, - protectedProcedure, - publicProcedure, -} from '@/trpc/api/trpc'; -import { average, max, min, round, sum } from '@/utils/math'; import { flatten, map, pipe, prop, sort, uniq } from 'ramda'; import { escape } from 'sqlstring'; import { z } from 'zod'; +import { average, max, min, round, sum } from '@openpanel/common'; import { chQuery, createSqlBuilder } from '@openpanel/db'; import { zChartInput } from '@openpanel/validation'; import type { IChartEvent, IChartInput } from '@openpanel/validation'; +import { createTRPCRouter, protectedProcedure, publicProcedure } from '../trpc'; import { getChartPrevStartEndDate, getChartStartEndDate, @@ -180,7 +176,7 @@ export const chartRouter = createTRPCRouter({ }), // TODO: Make this private - chart: publicProcedure.input(zChartInput).query(async ({ input }) => { + chart: publicProcedure.input(zChartInput).query(async ({ input, ctx }) => { const currentPeriod = getChartStartEndDate(input); const previousPeriod = getChartPrevStartEndDate({ range: input.range, diff --git a/apps/dashboard/src/trpc/api/routers/client.ts b/packages/trpc/src/routers/client.ts similarity index 95% rename from apps/dashboard/src/trpc/api/routers/client.ts rename to packages/trpc/src/routers/client.ts index a04b0e1d..e657efd9 100644 --- a/apps/dashboard/src/trpc/api/routers/client.ts +++ b/packages/trpc/src/routers/client.ts @@ -1,11 +1,12 @@ import { randomUUID } from 'crypto'; -import { createTRPCRouter, protectedProcedure } from '@/trpc/api/trpc'; import { z } from 'zod'; import { hashPassword, stripTrailingSlash } from '@openpanel/common'; import type { Prisma } from '@openpanel/db'; import { db } from '@openpanel/db'; +import { createTRPCRouter, protectedProcedure } from '../trpc'; + export const clientRouter = createTRPCRouter({ update: protectedProcedure .input( diff --git a/apps/dashboard/src/trpc/api/routers/dashboard.ts b/packages/trpc/src/routers/dashboard.ts similarity index 92% rename from apps/dashboard/src/trpc/api/routers/dashboard.ts rename to packages/trpc/src/routers/dashboard.ts index 57a48638..6ae8623f 100644 --- a/apps/dashboard/src/trpc/api/routers/dashboard.ts +++ b/packages/trpc/src/routers/dashboard.ts @@ -1,11 +1,11 @@ -import { createTRPCRouter, protectedProcedure } from '@/trpc/api/trpc'; -import { getId } from '@/utils/getDbId'; import { PrismaError } from 'prisma-error-enum'; import { z } from 'zod'; -import { db, getDashboardsByProjectId } from '@openpanel/db'; +import { db, getDashboardsByProjectId, getId } from '@openpanel/db'; import type { Prisma } from '@openpanel/db'; +import { createTRPCRouter, protectedProcedure } from '../trpc'; + export const dashboardRouter = createTRPCRouter({ list: protectedProcedure .input( diff --git a/apps/dashboard/src/trpc/api/routers/event.ts b/packages/trpc/src/routers/event.ts similarity index 94% rename from apps/dashboard/src/trpc/api/routers/event.ts rename to packages/trpc/src/routers/event.ts index d3e728f7..94a2cf2c 100644 --- a/apps/dashboard/src/trpc/api/routers/event.ts +++ b/packages/trpc/src/routers/event.ts @@ -1,13 +1,10 @@ -import { - createTRPCRouter, - protectedProcedure, - publicProcedure, -} from '@/trpc/api/trpc'; import { escape } from 'sqlstring'; import { z } from 'zod'; import { chQuery, convertClickhouseDateToJs, db } from '@openpanel/db'; +import { createTRPCRouter, protectedProcedure, publicProcedure } from '../trpc'; + export const eventRouter = createTRPCRouter({ updateEventMeta: protectedProcedure .input( diff --git a/apps/dashboard/src/trpc/api/routers/onboarding.ts b/packages/trpc/src/routers/onboarding.ts similarity index 79% rename from apps/dashboard/src/trpc/api/routers/onboarding.ts rename to packages/trpc/src/routers/onboarding.ts index 701ccbf4..042f91a0 100644 --- a/apps/dashboard/src/trpc/api/routers/onboarding.ts +++ b/packages/trpc/src/routers/onboarding.ts @@ -1,15 +1,13 @@ import { randomUUID } from 'crypto'; -import { createTRPCRouter, protectedProcedure } from '@/trpc/api/trpc'; -import { getId } from '@/utils/getDbId'; -import { slug } from '@/utils/slug'; -import { clerkClient } from '@clerk/nextjs'; -import { cookies } from 'next/headers'; +import { clerkClient } from '@clerk/fastify'; -import { hashPassword, stripTrailingSlash } from '@openpanel/common'; +import { hashPassword, slug, stripTrailingSlash } from '@openpanel/common'; +import { db, getId } from '@openpanel/db'; import type { ProjectType } from '@openpanel/db'; -import { db } from '@openpanel/db'; import { zOnboardingProject } from '@openpanel/validation'; +import { createTRPCRouter, protectedProcedure } from '../trpc'; + export const onboardingRouter = createTRPCRouter({ project: protectedProcedure .input(zOnboardingProject) @@ -50,7 +48,7 @@ export const onboardingRouter = createTRPCRouter({ }, }); - cookies().set('onboarding_client_secret', secret, { + ctx.setCookie('onboarding_client_secret', secret, { maxAge: 60 * 60, // 1 hour path: '/', }); diff --git a/apps/dashboard/src/trpc/api/routers/organization.ts b/packages/trpc/src/routers/organization.ts similarity index 95% rename from apps/dashboard/src/trpc/api/routers/organization.ts rename to packages/trpc/src/routers/organization.ts index 226ab997..7247db4d 100644 --- a/apps/dashboard/src/trpc/api/routers/organization.ts +++ b/packages/trpc/src/routers/organization.ts @@ -1,10 +1,11 @@ -import { createTRPCRouter, protectedProcedure } from '@/trpc/api/trpc'; -import { clerkClient } from '@clerk/nextjs'; +import { clerkClient } from '@clerk/fastify'; import { z } from 'zod'; import { db, getOrganizationBySlug } from '@openpanel/db'; import { zInviteUser } from '@openpanel/validation'; +import { createTRPCRouter, protectedProcedure } from '../trpc'; + export const organizationRouter = createTRPCRouter({ list: protectedProcedure.query(() => { return clerkClient.organizations.getOrganizationList(); diff --git a/apps/dashboard/src/trpc/api/routers/profile.ts b/packages/trpc/src/routers/profile.ts similarity index 95% rename from apps/dashboard/src/trpc/api/routers/profile.ts rename to packages/trpc/src/routers/profile.ts index b1982e83..902e4d41 100644 --- a/apps/dashboard/src/trpc/api/routers/profile.ts +++ b/packages/trpc/src/routers/profile.ts @@ -1,14 +1,11 @@ -import { - createTRPCRouter, - protectedProcedure, - publicProcedure, -} from '@/trpc/api/trpc'; import { flatten, map, pipe, prop, sort, uniq } from 'ramda'; import { escape } from 'sqlstring'; import { z } from 'zod'; import { chQuery, createSqlBuilder } from '@openpanel/db'; +import { createTRPCRouter, protectedProcedure, publicProcedure } from '../trpc'; + export const profileRouter = createTRPCRouter({ properties: protectedProcedure .input(z.object({ projectId: z.string() })) diff --git a/apps/dashboard/src/trpc/api/routers/project.ts b/packages/trpc/src/routers/project.ts similarity index 88% rename from apps/dashboard/src/trpc/api/routers/project.ts rename to packages/trpc/src/routers/project.ts index fa12c0b1..444826f2 100644 --- a/apps/dashboard/src/trpc/api/routers/project.ts +++ b/packages/trpc/src/routers/project.ts @@ -1,8 +1,8 @@ -import { createTRPCRouter, protectedProcedure } from '@/trpc/api/trpc'; -import { getId } from '@/utils/getDbId'; import { z } from 'zod'; -import { db, getProjectsByOrganizationSlug } from '@openpanel/db'; +import { db, getId, getProjectsByOrganizationSlug } from '@openpanel/db'; + +import { createTRPCRouter, protectedProcedure } from '../trpc'; export const projectRouter = createTRPCRouter({ list: protectedProcedure diff --git a/apps/dashboard/src/trpc/api/routers/reference.ts b/packages/trpc/src/routers/reference.ts similarity index 93% rename from apps/dashboard/src/trpc/api/routers/reference.ts rename to packages/trpc/src/routers/reference.ts index 06571363..55a4ce62 100644 --- a/apps/dashboard/src/trpc/api/routers/reference.ts +++ b/packages/trpc/src/routers/reference.ts @@ -1,13 +1,9 @@ -import { - createTRPCRouter, - protectedProcedure, - publicProcedure, -} from '@/trpc/api/trpc'; import { z } from 'zod'; import { db, getReferences } from '@openpanel/db'; import { zCreateReference, zRange } from '@openpanel/validation'; +import { createTRPCRouter, protectedProcedure, publicProcedure } from '../trpc'; import { getChartStartEndDate } from './chart.helpers'; export const referenceRouter = createTRPCRouter({ diff --git a/apps/dashboard/src/trpc/api/routers/report.ts b/packages/trpc/src/routers/report.ts similarity index 96% rename from apps/dashboard/src/trpc/api/routers/report.ts rename to packages/trpc/src/routers/report.ts index b5a58ef7..2f7ae0a8 100644 --- a/apps/dashboard/src/trpc/api/routers/report.ts +++ b/packages/trpc/src/routers/report.ts @@ -1,9 +1,10 @@ -import { createTRPCRouter, protectedProcedure } from '@/trpc/api/trpc'; import { z } from 'zod'; import { db } from '@openpanel/db'; import { zChartInput } from '@openpanel/validation'; +import { createTRPCRouter, protectedProcedure } from '../trpc'; + export const reportRouter = createTRPCRouter({ create: protectedProcedure .input( diff --git a/apps/dashboard/src/trpc/api/routers/share.ts b/packages/trpc/src/routers/share.ts similarity index 91% rename from apps/dashboard/src/trpc/api/routers/share.ts rename to packages/trpc/src/routers/share.ts index 969d42ec..fd7ee624 100644 --- a/apps/dashboard/src/trpc/api/routers/share.ts +++ b/packages/trpc/src/routers/share.ts @@ -1,9 +1,10 @@ -import { createTRPCRouter, protectedProcedure } from '@/trpc/api/trpc'; import ShortUniqueId from 'short-unique-id'; import { db } from '@openpanel/db'; import { zShareOverview } from '@openpanel/validation'; +import { createTRPCRouter, protectedProcedure } from '../trpc'; + const uid = new ShortUniqueId({ length: 6 }); export const shareRouter = createTRPCRouter({ diff --git a/packages/trpc/src/routers/user.ts b/packages/trpc/src/routers/user.ts new file mode 100644 index 00000000..a30f47cb --- /dev/null +++ b/packages/trpc/src/routers/user.ts @@ -0,0 +1,30 @@ +import { clerkClient } from '@clerk/fastify'; +import { z } from 'zod'; + +import { transformUser } from '@openpanel/db'; + +import { createTRPCRouter, protectedProcedure } from '../trpc'; + +export const userRouter = createTRPCRouter({ + update: protectedProcedure + .input( + z.object({ + firstName: z.string(), + lastName: z.string(), + }) + ) + .mutation(({ input, ctx }) => { + return ( + clerkClient.users + .updateUser(ctx.session.userId, { + firstName: input.firstName, + lastName: input.lastName, + }) + // Typescript issue that is fine for now, + // the properties we need are there + // Will be resolved when we update clerk/nextjs to v5 + // @ts-expect-error + .then(transformUser) + ); + }), +}); diff --git a/apps/dashboard/src/trpc/api/trpc.ts b/packages/trpc/src/trpc.ts similarity index 57% rename from apps/dashboard/src/trpc/api/trpc.ts rename to packages/trpc/src/trpc.ts index 278e4ba8..5245fb38 100644 --- a/apps/dashboard/src/trpc/api/trpc.ts +++ b/packages/trpc/src/trpc.ts @@ -1,13 +1,33 @@ -import type { getAuth } from '@clerk/nextjs/server'; +import { getAuth } from '@clerk/fastify'; import { initTRPC, TRPCError } from '@trpc/server'; +import type { CreateFastifyContextOptions } from '@trpc/server/adapters/fastify'; import superjson from 'superjson'; import { ZodError } from 'zod'; -interface CreateContextOptions { - session: ReturnType | null; +export function createContext({ req, res }: CreateFastifyContextOptions) { + return { + req, + res, + session: getAuth(req), + // we do not get types for `setCookie` from fastify + // so define it here and be safe in routers + setCookie: ( + key: string, + value: string, + options: { + maxAge: number; + path: string; + } + ) => { + // @ts-ignore + // eslint-disable-next-line + res.setCookie(key, value, options); + }, + }; } +export type Context = Awaited>; -const t = initTRPC.context().create({ +const t = initTRPC.context().create({ transformer: superjson, errorFormatter({ shape, error }) { return { @@ -21,10 +41,6 @@ const t = initTRPC.context().create({ }, }); -export const createTRPCRouter = t.router; - -export const publicProcedure = t.procedure; - const enforceUserIsAuthed = t.middleware(async ({ ctx, next }) => { if (!ctx.session?.userId) { throw new TRPCError({ code: 'UNAUTHORIZED', message: 'Not authenticated' }); @@ -44,4 +60,7 @@ const enforceUserIsAuthed = t.middleware(async ({ ctx, next }) => { } }); +export const createTRPCRouter = t.router; + +export const publicProcedure = t.procedure; export const protectedProcedure = t.procedure.use(enforceUserIsAuthed); diff --git a/packages/trpc/tsconfig.json b/packages/trpc/tsconfig.json new file mode 100644 index 00000000..a291eef2 --- /dev/null +++ b/packages/trpc/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "@openpanel/tsconfig/base.json", + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + }, + "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json" + }, + "include": ["."], + "exclude": ["node_modules"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3efae285..f196ffb8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,6 +29,12 @@ importers: '@baselime/pino-transport': specifier: ^0.1.5 version: 0.1.5 + '@clerk/fastify': + specifier: ^1.0.0 + version: 1.0.0(fastify-plugin@4.5.1)(fastify@4.26.1)(react@18.2.0) + '@fastify/cookie': + specifier: ^9.3.1 + version: 9.3.1 '@fastify/cors': specifier: ^9.0.0 version: 9.0.1 @@ -47,6 +53,12 @@ importers: '@openpanel/redis': specifier: workspace:* version: link:../../packages/redis + '@openpanel/trpc': + specifier: workspace:* + version: link:../../packages/trpc + '@trpc/server': + specifier: ^10.45.1 + version: 10.45.1 fastify: specifier: ^4.25.2 version: 4.26.1 @@ -80,9 +92,6 @@ importers: url-metadata: specifier: ^4.1.0 version: 4.1.0 - uuid: - specifier: ^9.0.1 - version: 9.0.1 devDependencies: '@openpanel/eslint-config': specifier: workspace:* @@ -409,6 +418,9 @@ importers: '@openpanel/prettier-config': specifier: workspace:* version: link:../../tooling/prettier + '@openpanel/trpc': + specifier: workspace:* + version: link:../../packages/trpc '@openpanel/tsconfig': specifier: workspace:* version: link:../../tooling/typescript @@ -751,9 +763,15 @@ importers: packages/common: dependencies: + mathjs: + specifier: ^12.3.2 + version: 12.3.2 ramda: specifier: ^0.29.1 version: 0.29.1 + slugify: + specifier: ^1.6.6 + version: 1.6.6 superjson: specifier: ^1.13.3 version: 1.13.3 @@ -1144,6 +1162,85 @@ importers: specifier: ^5.2.2 version: 5.3.3 + packages/trpc: + dependencies: + '@clerk/fastify': + specifier: ^1.0.0 + version: 1.0.0(fastify-plugin@4.5.1)(fastify@4.26.1)(react@18.2.0) + '@openpanel/common': + specifier: workspace:* + version: link:../common + '@openpanel/constants': + specifier: workspace:* + version: link:../constants + '@openpanel/db': + specifier: workspace:* + version: link:../db + '@openpanel/validation': + specifier: workspace:* + version: link:../validation + '@trpc/server': + specifier: ^10.45.1 + version: 10.45.1 + date-fns: + specifier: ^3.3.1 + version: 3.3.1 + mathjs: + specifier: ^12.3.2 + version: 12.3.2 + prisma-error-enum: + specifier: ^0.1.3 + version: 0.1.3 + ramda: + specifier: ^0.29.1 + version: 0.29.1 + short-unique-id: + specifier: ^5.0.3 + version: 5.0.3 + sqlstring: + specifier: ^2.3.3 + version: 2.3.3 + superjson: + specifier: ^1.13.3 + version: 1.13.3 + uuid: + specifier: ^9.0.1 + version: 9.0.1 + zod: + specifier: ^3.22.4 + version: 3.22.4 + devDependencies: + '@openpanel/eslint-config': + specifier: workspace:* + version: link:../../tooling/eslint + '@openpanel/prettier-config': + specifier: workspace:* + version: link:../../tooling/prettier + '@openpanel/tsconfig': + specifier: workspace:* + version: link:../../tooling/typescript + '@types/node': + specifier: ^18.16.0 + version: 18.19.17 + '@types/ramda': + specifier: ^0.29.6 + version: 0.29.10 + '@types/sqlstring': + specifier: ^2.3.2 + version: 2.3.2 + eslint: + specifier: ^8.48.0 + version: 8.56.0 + prettier: + specifier: ^3.0.3 + version: 3.2.5 + prisma: + specifier: ^5.1.1 + version: 5.9.1 + typescript: + specifier: ^5.2.2 + version: 5.3.3 + packages/validation: dependencies: '@openpanel/constants': @@ -2886,6 +2983,19 @@ packages: - react dev: false + /@clerk/backend@1.0.0(react@18.2.0): + resolution: {integrity: sha512-3HRpSszaRPkIsZtO+es+8qCBLg/aI6JdivhKoDtK0MctxCFXL8pQ7+CdtKgFN2ooDB5gJDzqETGg03Fb7G+QZg==} + engines: {node: '>=18.17.0'} + dependencies: + '@clerk/shared': 2.0.0(react@18.2.0) + cookie: 0.5.0 + snakecase-keys: 5.4.4 + tslib: 2.4.1 + transitivePeerDependencies: + - react + - react-dom + dev: false + /@clerk/clerk-react@4.30.10(react@18.2.0): resolution: {integrity: sha512-c2X0grf7Vo6LrycvYbVyIyU7Gtyb47mf0/fnQdmF5zL8PIF1Ih5Yn9ZkbNeVjCLQrVRLeSRBgrrEIzlIenbuaQ==} engines: {node: '>=14'} @@ -2944,6 +3054,24 @@ packages: - react dev: false + /@clerk/fastify@1.0.0(fastify-plugin@4.5.1)(fastify@4.26.1)(react@18.2.0): + resolution: {integrity: sha512-/GoMp5prCJZJWWyc8mv5kPcsu3o1yOsZl5JabBzuG4d7EZwI5+8SUqrAXxfbGfha5LpMYS32Iv+PnOIY4Kt9Dw==} + engines: {node: '>=18.17.0'} + peerDependencies: + fastify: '>=4' + fastify-plugin: ^4.5.0 + dependencies: + '@clerk/backend': 1.0.0(react@18.2.0) + '@clerk/shared': 2.0.0(react@18.2.0) + '@clerk/types': 4.0.0 + cookies: 0.8.0 + fastify: 4.26.1 + fastify-plugin: 4.5.1 + transitivePeerDependencies: + - react + - react-dom + dev: false + /@clerk/nextjs@4.29.12(next@14.2.1)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-9pB6s4E50OjUjySVhQpSHu9DC0zIGqVJdneMXcL26fk48+U7lq9XCAZEFNIYUVQeMMvEI1elWSgNmi2Xmpr8ug==} engines: {node: '>=14'} @@ -3012,6 +3140,26 @@ packages: swr: 2.2.0(react@18.2.0) dev: false + /@clerk/shared@2.0.0(react@18.2.0): + resolution: {integrity: sha512-HRbBGhAetOE6gSFd2jlLJTo+BFjuEr9lD1xigtBXdJUnmVngunciikQU2dZ4i/20X7B4VTrofhNpYi9MsCm7dw==} + engines: {node: '>=18.17.0'} + requiresBuild: true + peerDependencies: + react: '>=18' + react-dom: '>=18' + peerDependenciesMeta: + react: + optional: true + react-dom: + optional: true + dependencies: + glob-to-regexp: 0.4.1 + js-cookie: 3.0.1 + react: 18.2.0 + std-env: 3.7.0 + swr: 2.2.0(react@18.2.0) + dev: false + /@clerk/types@3.62.0: resolution: {integrity: sha512-rjtdPqNJtfayCrqOCi20i46rw7X5yzAiOoh0Dzl7KX8kdBWQn06UxpgREPEp/3gFS2imVFRyXtx+fUGRwOGjaw==} engines: {node: '>=14'} @@ -3026,6 +3174,13 @@ packages: csstype: 3.1.1 dev: false + /@clerk/types@4.0.0: + resolution: {integrity: sha512-my/uNzHflLYvoLR8RT3LBmYulYGkz+SOVYMdzWt14LpRHajQotnfDSq/GEkfjrBm2HXuZ82GxEDMyYtqHUKv8w==} + engines: {node: '>=18.17.0'} + dependencies: + csstype: 3.1.1 + dev: false + /@clickhouse/client-common@0.2.9: resolution: {integrity: sha512-ecXcegMbT4HYNWtGcfyidW6lNVRqPogbFMY5kfjJmz4IXJ4WZbQMwj2IQgemwFwE7jyia2OEwPIVfw1sNfDHRA==} dev: false @@ -3734,6 +3889,13 @@ packages: fast-uri: 2.3.0 dev: false + /@fastify/cookie@9.3.1: + resolution: {integrity: sha512-h1NAEhB266+ZbZ0e9qUE6NnNR07i7DnNXWG9VbbZ8uC6O/hxHpl+Zoe5sw1yfdZ2U6XhToUGDnzQtWJdCaPwfg==} + dependencies: + cookie-signature: 1.2.1 + fastify-plugin: 4.5.1 + dev: false + /@fastify/cors@9.0.1: resolution: {integrity: sha512-YY9Ho3ovI+QHIL2hW+9X4XqQjXLjJqsU+sMV/xFsxZkE8p3GNnYVFpoOxF7SsP5ZL76gwvbo3V9L+FIekBGU4Q==} dependencies: @@ -8942,11 +9104,24 @@ packages: resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} dev: false + /cookie-signature@1.2.1: + resolution: {integrity: sha512-78KWk9T26NhzXtuL26cIJ8/qNHANyJ/ZYrmEXFzUmhZdjpBv+DlWlOANRTGBt48YcyslsLrj0bMLFTmXvLRCOw==} + engines: {node: '>=6.6.0'} + dev: false + /cookie@0.5.0: resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} engines: {node: '>= 0.6'} dev: false + /cookies@0.8.0: + resolution: {integrity: sha512-8aPsApQfebXnuI+537McwYsDtjVxGm8gTIzQI3FDW6t5t/DAhERxtnbEPN/8RX+uZthoz4eCOgloXaE5cYyNow==} + engines: {node: '>= 0.8'} + dependencies: + depd: 2.0.0 + keygrip: 1.1.0 + dev: false + /copy-anything@3.0.5: resolution: {integrity: sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==} engines: {node: '>=12.13'} @@ -12394,6 +12569,13 @@ packages: commander: 8.3.0 dev: false + /keygrip@1.1.0: + resolution: {integrity: sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==} + engines: {node: '>= 0.6'} + dependencies: + tsscmp: 1.0.6 + dev: false + /keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} dependencies: @@ -16562,6 +16744,10 @@ packages: engines: {node: '>= 0.8'} dev: false + /std-env@3.7.0: + resolution: {integrity: sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==} + dev: false + /stream-buffers@2.2.0: resolution: {integrity: sha512-uyQK/mx5QjHun80FLJTfaWE7JtwfRMKBLkMne6udYOmvH0CawotVa7TfgYHzAnpphn4+TweIx1QKMnRIbipmUg==} engines: {node: '>= 0.10.0'} @@ -17111,6 +17297,11 @@ packages: requiresBuild: true dev: false + /tsscmp@1.0.6: + resolution: {integrity: sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==} + engines: {node: '>=0.6.x'} + dev: false + /tsup@7.3.0(typescript@5.3.3): resolution: {integrity: sha512-Ja1eaSRrE+QarmATlNO5fse2aOACYMBX+IZRKy1T+gpyH+jXgRrl5l4nHIQJQ1DoDgEjHDTw8cpE085UdBZuWQ==} engines: {node: '>=18'}