fix: analytics type errors

This commit is contained in:
Justin Lin committed 2025-04-26 13:10:48 +08:00
1 parent f7e2b00358
commit 253564376d
26 files changed
+53 -322

No files matched your search

-6
View File
@@ -1,6 +0,0 @@
{
"watch": ["src", "views"],
"ext": "ts,handlebars,js",
"ignore": ["src/**/*.spec.ts"],
"exec": "node --loader ts-node/esm src/index.ts"
}
+5 -7
View File
@@ -3,13 +3,12 @@
"version": "1.0.0",
"main": "src/index.js",
"private": true,
"type": "module",
"license": "GPL",
"scripts": {
"start": "node dist/index.js",
"build": "tsc -b -f",
"watch": "tsc -b --watch",
"dev": "nodemon"
"start": "node dist/analytics/src/index.js",
"build": "tsc",
"type-check": "tsc --noEmit",
"dev": "tsx watch src/index.ts"
},
"dependencies": {
"dotenv": "^16.4.5",
@@ -23,9 +22,8 @@
"@swc/helpers": "^0.5.6",
"@types/express": "^4.17.21",
"@types/node": "^20.11.20",
"nodemon": "^3.1.0",
"regenerator-runtime": "^0.14.1",
"ts-node": "^10.9.2",
"tsx": "^4.19.3",
"typescript": "^5.3.3"
}
}
View File
Whitespace-only changes.
@@ -1,6 +1,6 @@
import type { Request, Response } from 'express'
import Pagination from '../models/pagination.js'
import { errorCommands } from '../models/commands.js'
import Pagination from '../models/pagination'
import { errorCommands } from '../models/commands'
export async function commandListController(
req: Request,
+2 -2
View File
@@ -1,6 +1,6 @@
import { Request, Response } from 'express'
import { userCount } from '../models/users.js'
import { questList } from '../models/quests.js'
import { userCount } from '../models/users'
import { questList } from '../models/quests'
export async function homeController(req: Request, res: Response) {
const quests = await questList()
+1 -1
View File
@@ -1,5 +1,5 @@
import { Request, Response } from 'express'
import { questList } from '../models/quests.js'
import { questList } from '../models/quests'
export async function questListController(req: Request, res: Response) {
const quests = await questList()
@@ -1,6 +1,6 @@
import { Request, Response } from 'express'
import Pagination from '../models/pagination.js'
import { sessionList, sessionDetail } from '../models/sessions.js'
import Pagination from '../models/pagination'
import { sessionList, sessionDetail } from '../models/sessions'
export async function sessionListController(req: Request, res: Response) {
const { nextKey, order } = req.query as { nextKey: string; order: string }
+2 -2
View File
@@ -1,7 +1,7 @@
import { Request, Response } from 'express'
import mongoose from 'mongoose'
import Pagination from '../models/pagination.js'
import { userList, userDetail, idToUser } from '../models/users.js'
import Pagination from '../models/pagination'
import { userList, userDetail, idToUser } from '../models/users'
export async function userListController(req: Request, res: Response) {
const { nextKey, order } = req.query as { nextKey: string; order: string }
+3 -3
View File
@@ -1,9 +1,9 @@
import connectDB from '@models'
import { connectDB } from '../../models'
import express from 'express'
import { engine } from 'express-handlebars'
import basicAuth from 'express-basic-auth'
import config from './config.js'
import router from './router.js'
import config from './config'
import router from './router'
async function main() {
await connectDB(config.db)
+2 -2
View File
@@ -1,5 +1,5 @@
import { Command } from '@models'
import Pagination from './pagination.js'
import { Command } from '../../../models'
import Pagination from './pagination'
// eslint-disable-next-line import/prefer-default-export
export async function errorCommands(pagination: Pagination) {
@@ -1,5 +1,3 @@
/* eslint-disable class-methods-use-this */
// eslint-disable-next-line max-classes-per-file
import mongoose, { Types } from 'mongoose'
export const DESC: string = 'desc'
+10 -4
View File
@@ -1,8 +1,14 @@
/* eslint-disable object-shorthand */
/* eslint-disable func-names */
import { UserProfile } from '@models'
import { UserProfile } from '../../../models'
export function questList() {
export interface QuestProgress {
_id: string
started: number
completed: number
completedRate: number
failed: number
}
export function questList(): Promise<QuestProgress[]> {
return UserProfile.aggregate([
{
$project: {
+2 -2
View File
@@ -1,5 +1,5 @@
import { Command, Session } from '@models'
import Pagination from './pagination.js'
import { Command, Session } from '../../../models'
import Pagination from './pagination'
import {
CommandObject,
SessionDetail,
+10 -6
View File
@@ -1,7 +1,7 @@
import { Session, User } from '@models'
import { Types } from 'mongoose'
import Pagination from './pagination.js'
import { IUser, UserSessionDetail } from '../interface.js'
import { Session, User, IUser, ISession } from '../../../models'
import { HydratedDocument, Types } from 'mongoose'
import Pagination from './pagination'
import { UserSessionDetail } from '../interface'
function loginMethods(user: IUser): string[] {
const methods: string[] = []
@@ -23,7 +23,11 @@ export function userCount() {
}
export async function userList(pagination: Pagination) {
const users = await Session.aggregate([
const users: {
userData: HydratedDocument<IUser>
count: number
lastActivityAt: Date
}[] = await Session.aggregate([
pagination.match('user'),
{
$group: {
@@ -62,7 +66,7 @@ export async function userList(pagination: Pagination) {
export async function userDetail(
id: Types.ObjectId
): Promise<UserSessionDetail[]> {
const sessions = await Session.find({
const sessions: HydratedDocument<ISession>[] = await Session.find({
user: id,
})
return sessions.map((session): UserSessionDetail => {
+5 -8
View File
@@ -1,16 +1,13 @@
import { Router } from 'express'
import { homeController } from './controllers/index.js'
import {
userListController,
userDetailController,
} from './controllers/users.js'
import { questListController } from './controllers/quests.js'
import { commandListController } from './controllers/commands.js'
import { homeController } from './controllers/index'
import { userListController, userDetailController } from './controllers/users'
import { questListController } from './controllers/quests'
import { commandListController } from './controllers/commands'
import {
sessionDetailController,
sessionListController,
} from './controllers/sessions.js'
} from './controllers/sessions'
const router = Router()
+1 -3
View File
@@ -1,11 +1,9 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "./dist"
},
"include": ["src"],
"references": [{ "path": "../models" }],
"include": ["src", "../models"],
"ts-node": {
"swc": true
}
-2
View File
@@ -1,7 +1,5 @@
import { z } from 'zod'
export * from './session.js'
export const passwordPolicy = {
minLength: 8,
minNumbers: 1,
-15
View File
@@ -1,15 +0,0 @@
export interface SessionSummaryResponse {
_id: string
user: string
quest: string
status: string
createdAt: Date
lastActivityAt: Date
}
export interface SessionDetailResponse extends SessionSummaryResponse {
hints: string[][]
tasks: string[][]
responses: string[][]
graph: any
}
-52
View File
@@ -1,52 +0,0 @@
import { describe, it, expect, beforeAll } from '@jest/globals'
import fs from 'fs/promises'
import path from 'path'
import yaml from 'yaml'
import { questSchema } from '../src/quest/quest.js'
describe('Parse all quests', () => {
let quests: any[] = []
test('should parse all quests', async () => {
const questDirectory = path.join(process.cwd(), 'quests')
const loadQuests = async () => {
const questNames = await fs.readdir(questDirectory, {
withFileTypes: true,
})
return Promise.all(
questNames
.filter(
(dirent) => dirent.isDirectory() && !dirent.name.startsWith('.')
)
.map(async (dirent) => {
const id = dirent.name
const questPath = path.join(questDirectory, id)
const files = await fs.readdir(questPath)
if (!files.includes('game.yml')) {
throw new Error(`Missing game.yml in quest ${id}`)
}
const body = await fs.readFile(
path.join(questPath, 'game.yml'),
'utf-8'
)
const questData = yaml.parse(body, { merge: true })
const image = files.includes('Dockerfile') ? id : 'base'
return {
id,
image,
...questData,
}
})
)
}
quests = await loadQuests()
quests.forEach((quest) => {
console.log(`Quest ID: ${quest.id}`)
expect(questSchema.safeParse(quest).error).toBeUndefined()
})
})
})
-93
View File
@@ -1,93 +0,0 @@
import { describe, it, expect } from '@jest/globals'
import { questSchema } from '../src/index.js'
describe('Test quest schema', () => {
it('should parse quest schema', () => {
const sampleQuest = {
id: 'get-started',
image: 'base',
title: '終端機的基本知識',
instruction: `歡迎來到 Linux Odyssey!
在這個關卡中,你將會學到:
1. 如何使用這個網站
2. 如何使用終端機`,
stages: [
{
id: 'start',
task: '按下 `開始` ����開始',
condition: {
command: ['^echo start$'],
},
responses: [
{
content: `在畫面的中下方的區��是終端機,你可以在終端機��輸入指令來操作電��。
在終端機中,你所看到的 \`commander:~ $\` 叫做命令提示字串。
��令提示字串最後的��字號 \`$\` 叫命令提示符,你必須將指令輸入在它的後方。
��你輸入完指令並按下 Enter 之後,終端機會��行你輸入的指令,並且會從終端機的下一行開始輸出指令的��行結果。
直到你看到新的 \`commander:~ $\` 出現時,代表你可以輸入下一個指令了。
��在請你輸入 \`echo hello\` 來看看會發生什麼事。`,
},
],
hints: ['�� 在 `commander:~ $` ��面輸入 `echo hello` ��按下 Enter'],
},
{
id: 'echo-hello',
task: '在終端機輸入 `echo hello`',
requirements: ['start'],
condition: {
command: ['^echo hello$'],
},
responses: [
{
content: `你應��可以看到終端機上��示了以下內容:
\`\`\`
commander:~ $ echo hello
hello
\`\`\`
第一行的 \`commander:~ $\` 是命令提示字串,接在後方的 \`echo hello\` 是你輸入的指令。
第二行的 \`hello\` ��是終端機的輸出結果。
你現在知道了如何在終端機上輸入指令,以及查看終端機輸出的位置。
��在請輸入 \`echo next\` 來��續下一個步��。`,
},
],
hints: ['�� ���入 `echo next` ���續下一個步��'],
},
{
id: 'echo-next',
task: '在終端機輸入 `echo next`',
requirements: ['echo-hello'],
condition: {
command: ['^echo next$'],
},
responses: [
{
content: `\`Ctrl + C\` 在終端機中是一個很重要的快捷鍵,它可以用來中��正在��行的指令,並產生一個新的命令提示字串,讓你可以��續輸入指令。
如果指令��行很久沒有反應,或你因為其他原因想要中��指令,你可以按下 \`Ctrl + C\` 來中��指令。
又或者你輸入了錯��的指令,����得慢慢��除,你也可以按下 \`Ctrl + C\` 來產生新的命令提示字串重新輸入指令。
如果你已經理解了��才的說明,請輸入 \`echo finish\` 來結束這一關。`,
},
],
hints: [
'�� ��下 `Ctrl + C` 能��中��正在��行的指令',
'�� ��下 `Ctrl + C` 能��產生新的命令提示字串',
'�� ���入 `echo finish` 來結束這一關',
],
},
{
id: 'END',
task: '結束關卡',
requirements: ['echo-next'],
condition: {
command: ['^echo finish$'],
},
responses: [
{
content: '恭喜你完成了這個關卡!',
},
],
},
],
}
expect(questSchema.safeParse(sampleQuest).error).toBeUndefined()
})
})
-22
View File
@@ -1,22 +0,0 @@
import { describe, it, expect } from '@jest/globals'
import { responseSchema } from '../src/quest/response.js'
describe('parsing response', () => {
it('should parse valid response', () => {
const response = {
type: 'narrative',
content: 'This is a response content',
}
const result = responseSchema.safeParse(response)
expect(result.error).toBeUndefined()
})
it('should fail to parse invalid response', () => {
const response = {
type: 'dialogue',
content: 'This is a response content',
}
const result = responseSchema.safeParse(response)
expect(result.error).toBeUndefined()
})
})
-29
View File
@@ -1,29 +0,0 @@
import { describe, it, expect } from '@jest/globals'
import { stageSchema } from '../src/quest/stage.js'
describe('parsing stage', () => {
it('should parse valid stage', () => {
const stage = {
id: 'start',
task: '按下 `開始` ����開始',
requirements: ['start'],
condition: {
command: ['^echo start$'],
},
responses: [
{
type: 'text',
content: `在畫面的中下方的區��是終端機,你可以在終端機��輸入指令來操作電��。
在終端機中,你所看到的 \`commander:~ $\` 叫做命令提示字串。
��令提示字串最後的��字號 \`$\` 叫命令提示符,你必須將指令輸入在它的後方。
��你輸入完指令並按下 Enter 之後,終端機會��行你輸入的指令,並且會從終端機的下一行開始輸出指令的��行結果。
直到你看到新的 \`commander:~ $\` 出現時,代表你可以輸入下一個指令了。
��在請你輸入 \`echo hello\` 來看看會發生什麼事。`,
},
],
hints: ['�� 在 `commander:~ $` ��面輸入 `echo hello` ��按下 Enter'],
}
const result = stageSchema.safeParse(stage)
expect(result.error).toBeUndefined()
})
})
+1 -1
View File
@@ -5,7 +5,7 @@ export { Session, ISession } from './session.js'
export { Command, ICommand } from './command.js'
export { UserProfile, IUserProfile, IProgress } from './userProfile.js'
export default async function connectDB(uri: string) {
export async function connectDB(uri: string) {
const db = await mongoose.connect(uri)
return db.connection.getClient()
}