feat: add survey backend endpoint
This commit is contained in:
parent
9143cc468c
commit
27642625ac
@ -377,6 +377,9 @@ importers:
|
|||||||
'@trpc/server':
|
'@trpc/server':
|
||||||
specifier: ^10.45.0
|
specifier: ^10.45.0
|
||||||
version: 10.45.0
|
version: 10.45.0
|
||||||
|
accept-language-parser:
|
||||||
|
specifier: ^1.5.0
|
||||||
|
version: 1.5.0
|
||||||
axios:
|
axios:
|
||||||
specifier: ^1.5.0
|
specifier: ^1.5.0
|
||||||
version: 1.5.0
|
version: 1.5.0
|
||||||
@ -501,6 +504,9 @@ importers:
|
|||||||
'@faker-js/faker':
|
'@faker-js/faker':
|
||||||
specifier: ^8.4.0
|
specifier: ^8.4.0
|
||||||
version: 8.4.0
|
version: 8.4.0
|
||||||
|
'@types/accept-language-parser':
|
||||||
|
specifier: ^1.5.6
|
||||||
|
version: 1.5.6
|
||||||
'@types/bcryptjs':
|
'@types/bcryptjs':
|
||||||
specifier: ^2.4.3
|
specifier: ^2.4.3
|
||||||
version: 2.4.3
|
version: 2.4.3
|
||||||
@ -9967,6 +9973,10 @@ packages:
|
|||||||
d3-voronoi: 1.1.2
|
d3-voronoi: 1.1.2
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@types/accept-language-parser@1.5.6:
|
||||||
|
resolution: {integrity: sha512-lhSQUsAhAtbKjYgaw3f0c4EQKNQHFXhX87+OXUIqDHMkycvHGaqGskSRtnzysIUiqHPqNJ4BqI5SE++drsxx6A==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@types/acorn@4.0.6:
|
/@types/acorn@4.0.6:
|
||||||
resolution: {integrity: sha512-veQTnWP+1D/xbxVrPC3zHnCZRjSrKfhbMUlEA43iMZLu7EsnTtkJklIuwrCPbOi8YkvDQAiW05VQQFvvz9oieQ==}
|
resolution: {integrity: sha512-veQTnWP+1D/xbxVrPC3zHnCZRjSrKfhbMUlEA43iMZLu7EsnTtkJklIuwrCPbOi8YkvDQAiW05VQQFvvz9oieQ==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -10754,6 +10764,10 @@ packages:
|
|||||||
event-target-shim: 5.0.1
|
event-target-shim: 5.0.1
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/accept-language-parser@1.5.0:
|
||||||
|
resolution: {integrity: sha512-QhyTbMLYo0BBGg1aWbeMG4ekWtds/31BrEU+DONOg/7ax23vxpL03Pb7/zBmha2v7vdD3AyzZVWBVGEZxKOXWw==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/accepts@1.3.8:
|
/accepts@1.3.8:
|
||||||
resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
|
resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
|
@ -25,6 +25,7 @@
|
|||||||
"@prisma/client": "5.4.2",
|
"@prisma/client": "5.4.2",
|
||||||
"@tianji/shared": "workspace:^",
|
"@tianji/shared": "workspace:^",
|
||||||
"@trpc/server": "^10.45.0",
|
"@trpc/server": "^10.45.0",
|
||||||
|
"accept-language-parser": "^1.5.0",
|
||||||
"axios": "^1.5.0",
|
"axios": "^1.5.0",
|
||||||
"badge-maker": "^3.3.1",
|
"badge-maker": "^3.3.1",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
@ -68,6 +69,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@faker-js/faker": "^8.4.0",
|
"@faker-js/faker": "^8.4.0",
|
||||||
|
"@types/accept-language-parser": "^1.5.6",
|
||||||
"@types/bcryptjs": "^2.4.3",
|
"@types/bcryptjs": "^2.4.3",
|
||||||
"@types/compression": "^1.7.2",
|
"@types/compression": "^1.7.2",
|
||||||
"@types/cors": "^2.8.15",
|
"@types/cors": "^2.8.15",
|
||||||
|
@ -10,10 +10,12 @@ export const MonitorStatusPageListSchema = z.array(
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const SurveyPayloadSchema = z.object({
|
export const SurveyPayloadSchema = z.object({
|
||||||
items: z.object({
|
items: z.array(
|
||||||
label: z.string(),
|
z.object({
|
||||||
name: z.string(),
|
label: z.string(),
|
||||||
type: z.enum(['text', 'select', 'email']),
|
name: z.string(),
|
||||||
options: z.array(z.string()).optional(),
|
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],
|
tags: [OPENAPI_TAG.TELEMETRY],
|
||||||
protect: true,
|
protect: true,
|
||||||
...meta,
|
...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 authorization = req.headers['authorization'] ?? '';
|
||||||
const token = authorization.replace('Bearer ', '');
|
const token = authorization.replace('Bearer ', '');
|
||||||
|
|
||||||
return { token };
|
return { token, req };
|
||||||
}
|
}
|
||||||
|
|
||||||
type Context = inferAsyncReturnType<typeof createContext>;
|
type Context = inferAsyncReturnType<typeof createContext>;
|
||||||
|
@ -110,4 +110,5 @@ export enum OPENAPI_TAG {
|
|||||||
AUDIT_LOG = 'AuditLog',
|
AUDIT_LOG = 'AuditLog',
|
||||||
BILLING = 'Billing',
|
BILLING = 'Billing',
|
||||||
TELEMETRY = 'Telemetry',
|
TELEMETRY = 'Telemetry',
|
||||||
|
SURVEY = 'Survey',
|
||||||
}
|
}
|
||||||
|
@ -12,8 +12,10 @@ import {
|
|||||||
} from './const';
|
} from './const';
|
||||||
import maxmind, { Reader, CityResponse } from 'maxmind';
|
import maxmind, { Reader, CityResponse } from 'maxmind';
|
||||||
import { libraryPath } from './lib';
|
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 userAgent = req.headers['user-agent'];
|
||||||
const ip = getIpAddress(req);
|
const ip = getIpAddress(req);
|
||||||
const location = await getLocation(ip);
|
const location = await getLocation(ip);
|
||||||
@ -28,11 +30,14 @@ export async function getRequestInfo(req: Request) {
|
|||||||
} = location ?? {};
|
} = location ?? {};
|
||||||
const browser = browserName(userAgent ?? '');
|
const browser = browserName(userAgent ?? '');
|
||||||
const os = detectOS(userAgent ?? '');
|
const os = detectOS(userAgent ?? '');
|
||||||
|
const language: string | undefined =
|
||||||
|
pareseAcceptLanguage(req.headers['accept-language'])[0]?.code ?? undefined;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
userAgent,
|
userAgent,
|
||||||
browser,
|
browser,
|
||||||
os,
|
os,
|
||||||
|
language,
|
||||||
ip,
|
ip,
|
||||||
country,
|
country,
|
||||||
subdivision1,
|
subdivision1,
|
||||||
@ -57,7 +62,7 @@ export async function getClientInfo(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getIpAddress(req: Request): string {
|
export function getIpAddress(req: IncomingMessage): string {
|
||||||
// Custom header
|
// Custom header
|
||||||
if (
|
if (
|
||||||
process.env.CLIENT_IP_HEADER &&
|
process.env.CLIENT_IP_HEADER &&
|
||||||
|
@ -116,13 +116,16 @@ export async function parseTelemetryFilters(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function normalizeFilters(filters: Record<string, any> = {}) {
|
function normalizeFilters(filters: Record<string, any> = {}) {
|
||||||
return Object.keys(filters).reduce((obj, key) => {
|
return Object.keys(filters).reduce(
|
||||||
const value = filters[key];
|
(obj, key) => {
|
||||||
|
const value = filters[key];
|
||||||
|
|
||||||
obj[key] = value?.value ?? value;
|
obj[key] = value?.value ?? value;
|
||||||
|
|
||||||
return obj;
|
return obj;
|
||||||
}, {} as Record<string, any>);
|
},
|
||||||
|
{} as Record<string, any>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getWebsiteFilterQuery(
|
export function getWebsiteFilterQuery(
|
||||||
@ -217,11 +220,24 @@ type ExtractFindManyReturnType<T> = T extends (
|
|||||||
? R
|
? R
|
||||||
: never;
|
: never;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @example
|
||||||
|
* const { items, nextCursor } = await fetchDataByCursor(
|
||||||
|
* prisma.workspaceAuditLog,
|
||||||
|
* {
|
||||||
|
* where: {
|
||||||
|
* workspaceId,
|
||||||
|
* },
|
||||||
|
* limit,
|
||||||
|
* cursor,
|
||||||
|
* }
|
||||||
|
* );
|
||||||
|
*/
|
||||||
export async function fetchDataByCursor<
|
export async function fetchDataByCursor<
|
||||||
Model extends {
|
Model extends {
|
||||||
findMany: (args?: any) => Prisma.PrismaPromise<any>;
|
findMany: (args?: any) => Prisma.PrismaPromise<any>;
|
||||||
},
|
},
|
||||||
CursorType
|
CursorType,
|
||||||
>(
|
>(
|
||||||
fetchModel: Model,
|
fetchModel: Model,
|
||||||
options: {
|
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