diff --git a/packages/hydrooj/package.json b/packages/hydrooj/package.json index 5139ac81a..29464258c 100644 --- a/packages/hydrooj/package.json +++ b/packages/hydrooj/package.json @@ -1,6 +1,6 @@ { "name": "hydrooj", - "version": "2.33.4", + "version": "2.33.5", "bin": "bin/hydrooj.js", "main": "src/loader", "module": "src/loader", @@ -20,6 +20,7 @@ "detect-browser": "^5.2.0", "fs-extra": "^10.0.0", "graphql": "^15.5.1", + "graphql-scalars": "^1.10.0", "js-yaml": "^4.1.0", "koa": "^2.13.1", "koa-body": "^4.2.0", diff --git a/packages/hydrooj/src/handler/api.ts b/packages/hydrooj/src/handler/api.ts index 8a211ef6e..036258348 100644 --- a/packages/hydrooj/src/handler/api.ts +++ b/packages/hydrooj/src/handler/api.ts @@ -1,34 +1,78 @@ import graphql from 'graphql'; -import UserModel from '../model/user'; +import { typeDefs, resolvers } from 'graphql-scalars'; import { Handler, Route } from '../service/server'; +import * as bus from '../service/bus'; -const schemaStr = `\ -type Query { - user(id: Int, uname: String, mail: String): User - problem(id: Int): String +const types: Record> = {}; +const descriptions: Record> = {}; +const handlers: Record> = {}; +let root: Record = {}; + +export function registerValue(typeName: string, key: string, value: string, description?: string): void; +export function registerValue(typeName: string, vals: [string, string, string?][]): void; +export function registerValue(typeName: string, arg1: [string, string, string?][] | string, value?: string, description?: string) { + if (typeof arg1 === 'string') arg1 = [[arg1, value!, description]]; + for (const [k, v, d] of arg1) { + if (!types[typeName]) types[typeName] = { [k]: v }; + else types[typeName][k] = v; + if (d) { + if (!descriptions[typeName]) descriptions[typeName] = { [k]: d }; + else descriptions[typeName][k] = d; + } + } + bus.emit('api/update'); } -type User { - _id: Int! - uname: String! - mail: String! -}`; +export function registerResolver(typeName: string, key: string, value: string, func: Function, description?: string) { + registerValue(typeName, key, value, description); + if (handlers[typeName]) handlers[typeName][key.split('(')[0].trim()] = func; + else handlers[typeName] = { [key.split('(')[0].trim()]: func }; + bus.emit('api/update'); +} -const schema = graphql.buildSchema(schemaStr); -const root = { - user: (arg, ctx) => { - if (arg.id) return UserModel.getById(ctx.domainId, arg.id); - if (arg.mail) return UserModel.getByEmail(ctx.domainId, arg.mail); - if (arg.uname) return UserModel.getByUname(ctx.domainId, arg.uname); - return ctx.user; - }, - problem: async () => { }, -}; +function setDescription(desc: string) { + if (desc.includes('\n')) return ['"""', desc, '"""'].join('\n'); + return JSON.stringify(desc); +} + +function buildSchemaStr() { + let res = `${typeDefs.join('\n')}\n`; + for (const key in types) { + if (descriptions[key]._description) res += `${setDescription(descriptions[key]._description)}\n`; + res += `type ${key}{\n`; + for (const k in types[key]) { + if (descriptions[key][k]) res += ` ${setDescription(descriptions[key][k])}\n`; + res += ` ${k}: ${types[key][k]}\n`; + } + res += '}\n'; + } + return res; +} + +let schemaStr = buildSchemaStr(); +let schema = graphql.buildSchema(schemaStr); +root = { ...handlers.Query, ...resolvers }; + +export function rebuild() { + try { + const str = buildSchemaStr(); + schema = graphql.buildSchema(str); + schemaStr = str; + root = { ...handlers.Query, ...resolvers }; + } catch (e) { + console.error(e); + } +} +bus.on('app/started', () => { + rebuild(); + bus.on('api/update', rebuild); +}); class ApiHandler extends Handler { async get() { const q = decodeURIComponent(this.ctx.request.querystring); if (q === 'schema') { + this.response.type = 'application/json'; this.response.body = { schema: schemaStr }; } else if (q) { this.response.type = 'application/json'; @@ -38,6 +82,7 @@ class ApiHandler extends Handler { async post() { this.response.type = 'application/json'; + // FIXME validation for fields like ObjectID doesn't work. this.response.body = await graphql.graphql(schema, this.args.query, root, this, this.args.variables); } } diff --git a/packages/hydrooj/src/handler/user.ts b/packages/hydrooj/src/handler/user.ts index 22d598de0..a8dc92a4c 100644 --- a/packages/hydrooj/src/handler/user.ts +++ b/packages/hydrooj/src/handler/user.ts @@ -1,4 +1,5 @@ import moment from 'moment-timezone'; +import { registerResolver, registerValue } from './api'; import { UserAlreadyExistError, InvalidTokenError, VerifyPasswordError, UserNotFoundError, SystemError, BlacklistedError, @@ -22,6 +23,34 @@ import { sendMail } from '../lib/mail'; import paginate from '../lib/paginate'; import avatar from '../lib/avatar'; +registerValue('User', [ + ['_id', 'Int!'], + ['uname', 'String!'], + ['mail', 'String!'], + ['perm', 'Int'], + ['scope', 'Int'], + ['role', 'String'], + ['loginat', 'Date'], + ['regat', 'Date'], + ['priv', 'Int', 'User Privilege'], +]); + +registerResolver('Query', 'user(id: Int, uname: String, mail: String)', 'User', (arg, ctx) => { + if (arg.id) return user.getById(ctx.domainId, arg.id); + if (arg.mail) return user.getByEmail(ctx.domainId, arg.mail); + if (arg.uname) return user.getByUname(ctx.domainId, arg.uname); + return ctx.user; +}, `Get a user by id, uname, or mail. +Returns current user if no argument is provided.`); + +registerResolver('Query', 'users(ids: [Int], search: String)', '[User]', async (arg, ctx) => { + if (arg.ids) { + const res = await user.getList(ctx.domainId, arg.ids); + return Object.keys(res).map((id) => res[+id]); + } + return arg.search ? await user.getPrefixList(ctx.domainId, arg.search) : []; +}, 'Get a list of user by ids, or search users with the prefix.'); + class UserLoginHandler extends Handler { noCheckPermView = true;