feat: add survey backend endpoint
This commit is contained in:
parent
9143cc468c
commit
27642625ac
@ -377,6 +377,9 @@ importers:
|
||||
'@trpc/server':
|
||||
specifier: ^10.45.0
|
||||
version: 10.45.0
|
||||
accept-language-parser:
|
||||
specifier: ^1.5.0
|
||||
version: 1.5.0
|
||||
axios:
|
||||
specifier: ^1.5.0
|
||||
version: 1.5.0
|
||||
@ -501,6 +504,9 @@ importers:
|
||||
'@faker-js/faker':
|
||||
specifier: ^8.4.0
|
||||
version: 8.4.0
|
||||
'@types/accept-language-parser':
|
||||
specifier: ^1.5.6
|
||||
version: 1.5.6
|
||||
'@types/bcryptjs':
|
||||
specifier: ^2.4.3
|
||||
version: 2.4.3
|
||||
@ -9967,6 +9973,10 @@ packages:
|
||||
d3-voronoi: 1.1.2
|
||||
dev: false
|
||||
|
||||
/@types/accept-language-parser@1.5.6:
|
||||
resolution: {integrity: sha512-lhSQUsAhAtbKjYgaw3f0c4EQKNQHFXhX87+OXUIqDHMkycvHGaqGskSRtnzysIUiqHPqNJ4BqI5SE++drsxx6A==}
|
||||
dev: true
|
||||
|
||||
/@types/acorn@4.0.6:
|
||||
resolution: {integrity: sha512-veQTnWP+1D/xbxVrPC3zHnCZRjSrKfhbMUlEA43iMZLu7EsnTtkJklIuwrCPbOi8YkvDQAiW05VQQFvvz9oieQ==}
|
||||
dependencies:
|
||||
@ -10754,6 +10764,10 @@ packages:
|
||||
event-target-shim: 5.0.1
|
||||
dev: true
|
||||
|
||||
/accept-language-parser@1.5.0:
|
||||
resolution: {integrity: sha512-QhyTbMLYo0BBGg1aWbeMG4ekWtds/31BrEU+DONOg/7ax23vxpL03Pb7/zBmha2v7vdD3AyzZVWBVGEZxKOXWw==}
|
||||
dev: false
|
||||
|
||||
/accepts@1.3.8:
|
||||
resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
@ -25,6 +25,7 @@
|
||||
"@prisma/client": "5.4.2",
|
||||
"@tianji/shared": "workspace:^",
|
||||
"@trpc/server": "^10.45.0",
|
||||
"accept-language-parser": "^1.5.0",
|
||||
"axios": "^1.5.0",
|
||||
"badge-maker": "^3.3.1",
|
||||
"bcryptjs": "^2.4.3",
|
||||
@ -68,6 +69,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@faker-js/faker": "^8.4.0",
|
||||
"@types/accept-language-parser": "^1.5.6",
|
||||
"@types/bcryptjs": "^2.4.3",
|
||||
"@types/compression": "^1.7.2",
|
||||
"@types/cors": "^2.8.15",
|
||||
|
@ -10,10 +10,12 @@ export const MonitorStatusPageListSchema = z.array(
|
||||
);
|
||||
|
||||
export const SurveyPayloadSchema = z.object({
|
||||
items: z.object({
|
||||
label: z.string(),
|
||||
name: z.string(),
|
||||
type: z.enum(['text', 'select', 'email']),
|
||||
options: z.array(z.string()).optional(),
|
||||
}),
|
||||
items: z.array(
|
||||
z.object({
|
||||
label: z.string(),
|
||||
name: z.string(),
|
||||
type: z.enum(['text', 'select', 'email']),
|
||||
options: z.array(z.string()).optional(),
|
||||
})
|
||||
),
|
||||
});
|
||||
|
276
src/server/trpc/routers/survey.ts
Normal file
276
src/server/trpc/routers/survey.ts
Normal file
@ -0,0 +1,276 @@
|
||||
import { z } from 'zod';
|
||||
import {
|
||||
OpenApiMetaInfo,
|
||||
publicProcedure,
|
||||
router,
|
||||
workspaceOwnerProcedure,
|
||||
workspaceProcedure,
|
||||
} from '../trpc';
|
||||
import { OPENAPI_TAG } from '../../utils/const';
|
||||
import { prisma } from '../../model/_client';
|
||||
import { SurveyModelSchema, SurveyResultModelSchema } from '../../prisma/zod';
|
||||
import { OpenApiMeta } from 'trpc-openapi';
|
||||
import { hashUuid } from '../../utils/common';
|
||||
import { getRequestInfo } from '../../utils/detect';
|
||||
import { SurveyPayloadSchema } from '../../prisma/zod/schemas';
|
||||
import { buildCursorResponseSchema } from '../../utils/schema';
|
||||
import { fetchDataByCursor } from '../../utils/prisma';
|
||||
|
||||
export const telemetryRouter = router({
|
||||
all: workspaceProcedure
|
||||
.meta(
|
||||
buildSurveyOpenapi({
|
||||
method: 'GET',
|
||||
path: '/all',
|
||||
})
|
||||
)
|
||||
.output(z.array(SurveyModelSchema))
|
||||
.query(async ({ input }) => {
|
||||
const { workspaceId } = input;
|
||||
|
||||
const res = await prisma.survey.findMany({
|
||||
where: {
|
||||
workspaceId,
|
||||
},
|
||||
orderBy: {
|
||||
updatedAt: 'desc',
|
||||
},
|
||||
});
|
||||
|
||||
return res;
|
||||
}),
|
||||
get: publicProcedure
|
||||
.meta(
|
||||
buildSurveyOpenapi({
|
||||
method: 'GET',
|
||||
path: '/{surveyId}',
|
||||
})
|
||||
)
|
||||
.input(
|
||||
z.object({
|
||||
workspaceId: z.string(),
|
||||
surveyId: z.string(),
|
||||
})
|
||||
)
|
||||
.output(SurveyModelSchema.nullable())
|
||||
.query(async ({ input }) => {
|
||||
const { workspaceId, surveyId } = input;
|
||||
|
||||
const res = await prisma.survey.findUnique({
|
||||
where: {
|
||||
workspaceId,
|
||||
id: surveyId,
|
||||
},
|
||||
});
|
||||
|
||||
return res;
|
||||
}),
|
||||
count: workspaceProcedure
|
||||
.meta(
|
||||
buildSurveyOpenapi({
|
||||
method: 'GET',
|
||||
path: '/{surveyId}/count',
|
||||
})
|
||||
)
|
||||
.input(
|
||||
z.object({
|
||||
surveyId: z.string(),
|
||||
})
|
||||
)
|
||||
.output(z.number())
|
||||
.query(async ({ input }) => {
|
||||
const { surveyId } = input;
|
||||
|
||||
const count = await prisma.surveyResult.count({
|
||||
where: {
|
||||
surveyId: surveyId,
|
||||
},
|
||||
});
|
||||
|
||||
return count;
|
||||
}),
|
||||
submit: publicProcedure
|
||||
.meta(
|
||||
buildSurveyOpenapi({
|
||||
method: 'POST',
|
||||
path: '/{surveyId}/submit',
|
||||
})
|
||||
)
|
||||
.input(
|
||||
z.object({
|
||||
workspaceId: z.string(),
|
||||
surveyId: z.string(),
|
||||
payload: z.record(z.string(), z.any()),
|
||||
})
|
||||
)
|
||||
.output(z.any())
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { req } = ctx;
|
||||
const { workspaceId, surveyId, payload } = input;
|
||||
|
||||
const {
|
||||
userAgent,
|
||||
browser,
|
||||
os,
|
||||
language,
|
||||
ip,
|
||||
country,
|
||||
subdivision1,
|
||||
subdivision2,
|
||||
city,
|
||||
longitude,
|
||||
latitude,
|
||||
accuracyRadius,
|
||||
} = await getRequestInfo(req);
|
||||
|
||||
const sessionId = hashUuid(workspaceId, surveyId, ip, userAgent!);
|
||||
|
||||
await prisma.surveyResult.create({
|
||||
data: {
|
||||
surveyId,
|
||||
sessionId,
|
||||
payload,
|
||||
browser,
|
||||
os,
|
||||
language,
|
||||
ip,
|
||||
country,
|
||||
subdivision1,
|
||||
subdivision2,
|
||||
city,
|
||||
longitude,
|
||||
latitude,
|
||||
accuracyRadius,
|
||||
},
|
||||
});
|
||||
}),
|
||||
create: workspaceOwnerProcedure
|
||||
.meta(
|
||||
buildSurveyOpenapi({
|
||||
method: 'POST',
|
||||
path: '/create',
|
||||
})
|
||||
)
|
||||
.input(
|
||||
z.object({
|
||||
name: z.string(),
|
||||
payload: SurveyPayloadSchema,
|
||||
})
|
||||
)
|
||||
.output(SurveyModelSchema)
|
||||
.mutation(async ({ input }) => {
|
||||
const { workspaceId, name, payload } = input;
|
||||
|
||||
const res = await prisma.survey.create({
|
||||
data: {
|
||||
workspaceId,
|
||||
name,
|
||||
payload,
|
||||
},
|
||||
});
|
||||
|
||||
return res;
|
||||
}),
|
||||
update: workspaceOwnerProcedure
|
||||
.meta(
|
||||
buildSurveyOpenapi({
|
||||
method: 'PATCH',
|
||||
path: '/{surveyId}/update',
|
||||
})
|
||||
)
|
||||
.input(
|
||||
z.object({
|
||||
surveyId: z.string(),
|
||||
name: z.string().optional(),
|
||||
payload: SurveyPayloadSchema.optional(),
|
||||
})
|
||||
)
|
||||
.output(SurveyModelSchema)
|
||||
.mutation(async ({ input }) => {
|
||||
const { workspaceId, surveyId, name, payload } = input;
|
||||
|
||||
const res = await prisma.survey.update({
|
||||
where: {
|
||||
workspaceId,
|
||||
id: surveyId,
|
||||
},
|
||||
data: {
|
||||
name,
|
||||
payload,
|
||||
},
|
||||
});
|
||||
|
||||
return res;
|
||||
}),
|
||||
delete: workspaceOwnerProcedure
|
||||
.meta(
|
||||
buildSurveyOpenapi({
|
||||
method: 'DELETE',
|
||||
path: '/{surveyId}/delete',
|
||||
})
|
||||
)
|
||||
.input(
|
||||
z.object({
|
||||
surveyId: z.string(),
|
||||
})
|
||||
)
|
||||
.output(SurveyModelSchema)
|
||||
.mutation(async ({ input }) => {
|
||||
const { workspaceId, surveyId } = input;
|
||||
|
||||
const res = await prisma.survey.delete({
|
||||
where: {
|
||||
id: surveyId,
|
||||
workspaceId,
|
||||
},
|
||||
});
|
||||
|
||||
return res;
|
||||
}),
|
||||
resultList: workspaceProcedure
|
||||
.meta(
|
||||
buildSurveyOpenapi({
|
||||
method: 'GET',
|
||||
path: '/{surveyId}/result/list',
|
||||
})
|
||||
)
|
||||
.input(
|
||||
z.object({
|
||||
surveyId: z.string(),
|
||||
limit: z.number().min(1).max(100).default(50),
|
||||
cursor: z.string().optional(),
|
||||
})
|
||||
)
|
||||
.output(buildCursorResponseSchema(SurveyResultModelSchema))
|
||||
.query(async ({ input }) => {
|
||||
const limit = input.limit;
|
||||
const { cursor, surveyId } = input;
|
||||
|
||||
const { items, nextCursor } = await fetchDataByCursor(
|
||||
prisma.surveyResult,
|
||||
{
|
||||
where: {
|
||||
surveyId,
|
||||
},
|
||||
limit,
|
||||
cursor,
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
items,
|
||||
nextCursor,
|
||||
};
|
||||
}),
|
||||
});
|
||||
|
||||
function buildSurveyOpenapi(meta: OpenApiMetaInfo): OpenApiMeta {
|
||||
return {
|
||||
openapi: {
|
||||
tags: [OPENAPI_TAG.SURVEY],
|
||||
protect: true,
|
||||
...meta,
|
||||
path: `/workspace/{workspaceId}/survey/${meta.path}`,
|
||||
},
|
||||
};
|
||||
}
|
@ -427,7 +427,7 @@ function buildTelemetryOpenapi(meta: OpenApiMetaInfo): OpenApiMeta {
|
||||
tags: [OPENAPI_TAG.TELEMETRY],
|
||||
protect: true,
|
||||
...meta,
|
||||
path: `/workspace/{workspaceId}${meta.path}`,
|
||||
path: `/workspace/{workspaceId}/telemetry${meta.path}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ export function createContext({ req }: { req: IncomingMessage }) {
|
||||
const authorization = req.headers['authorization'] ?? '';
|
||||
const token = authorization.replace('Bearer ', '');
|
||||
|
||||
return { token };
|
||||
return { token, req };
|
||||
}
|
||||
|
||||
type Context = inferAsyncReturnType<typeof createContext>;
|
||||
|
@ -110,4 +110,5 @@ export enum OPENAPI_TAG {
|
||||
AUDIT_LOG = 'AuditLog',
|
||||
BILLING = 'Billing',
|
||||
TELEMETRY = 'Telemetry',
|
||||
SURVEY = 'Survey',
|
||||
}
|
||||
|
@ -12,8 +12,10 @@ import {
|
||||
} from './const';
|
||||
import maxmind, { Reader, CityResponse } from 'maxmind';
|
||||
import { libraryPath } from './lib';
|
||||
import { IncomingMessage } from 'http';
|
||||
import { parse as pareseAcceptLanguage } from 'accept-language-parser';
|
||||
|
||||
export async function getRequestInfo(req: Request) {
|
||||
export async function getRequestInfo(req: IncomingMessage) {
|
||||
const userAgent = req.headers['user-agent'];
|
||||
const ip = getIpAddress(req);
|
||||
const location = await getLocation(ip);
|
||||
@ -28,11 +30,14 @@ export async function getRequestInfo(req: Request) {
|
||||
} = location ?? {};
|
||||
const browser = browserName(userAgent ?? '');
|
||||
const os = detectOS(userAgent ?? '');
|
||||
const language: string | undefined =
|
||||
pareseAcceptLanguage(req.headers['accept-language'])[0]?.code ?? undefined;
|
||||
|
||||
return {
|
||||
userAgent,
|
||||
browser,
|
||||
os,
|
||||
language,
|
||||
ip,
|
||||
country,
|
||||
subdivision1,
|
||||
@ -57,7 +62,7 @@ export async function getClientInfo(
|
||||
};
|
||||
}
|
||||
|
||||
export function getIpAddress(req: Request): string {
|
||||
export function getIpAddress(req: IncomingMessage): string {
|
||||
// Custom header
|
||||
if (
|
||||
process.env.CLIENT_IP_HEADER &&
|
||||
|
@ -116,13 +116,16 @@ export async function parseTelemetryFilters(
|
||||
}
|
||||
|
||||
function normalizeFilters(filters: Record<string, any> = {}) {
|
||||
return Object.keys(filters).reduce((obj, key) => {
|
||||
const value = filters[key];
|
||||
return Object.keys(filters).reduce(
|
||||
(obj, key) => {
|
||||
const value = filters[key];
|
||||
|
||||
obj[key] = value?.value ?? value;
|
||||
obj[key] = value?.value ?? value;
|
||||
|
||||
return obj;
|
||||
}, {} as Record<string, any>);
|
||||
return obj;
|
||||
},
|
||||
{} as Record<string, any>
|
||||
);
|
||||
}
|
||||
|
||||
export function getWebsiteFilterQuery(
|
||||
@ -217,11 +220,24 @@ type ExtractFindManyReturnType<T> = T extends (
|
||||
? R
|
||||
: never;
|
||||
|
||||
/**
|
||||
* @example
|
||||
* const { items, nextCursor } = await fetchDataByCursor(
|
||||
* prisma.workspaceAuditLog,
|
||||
* {
|
||||
* where: {
|
||||
* workspaceId,
|
||||
* },
|
||||
* limit,
|
||||
* cursor,
|
||||
* }
|
||||
* );
|
||||
*/
|
||||
export async function fetchDataByCursor<
|
||||
Model extends {
|
||||
findMany: (args?: any) => Prisma.PrismaPromise<any>;
|
||||
},
|
||||
CursorType
|
||||
CursorType,
|
||||
>(
|
||||
fetchModel: Model,
|
||||
options: {
|
||||
|
11
src/server/utils/schema.ts
Normal file
11
src/server/utils/schema.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export function buildCursorResponseSchema(
|
||||
itemSchema: z.ZodType,
|
||||
cursorSchema = z.string()
|
||||
) {
|
||||
return z.object({
|
||||
items: z.array(itemSchema),
|
||||
nextCursor: cursorSchema.optional(),
|
||||
});
|
||||
}
|
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user