core: init graphql

This commit is contained in:
undefined committed 2021-08-23 02:39:58 +08:00
1 parent 2097c7257f
commit c39c406535
16 files changed
+131 -23

No files matched your search

+2 -1
View File
@@ -1,6 +1,6 @@
{
"name": "hydrooj",
"version": "2.32.35",
"version": "2.33.0",
"bin": "bin/hydrooj.js",
"main": "src/loader",
"module": "src/loader",
@@ -19,6 +19,7 @@
"cookies": "^0.8.0",
"detect-browser": "^5.2.0",
"fs-extra": "^10.0.0",
"graphql": "^15.5.1",
"js-yaml": "^4.1.0",
"koa": "^2.13.1",
"koa-body": "^4.2.0",
+1 -1
View File
@@ -26,7 +26,7 @@ export const builtinModel = [
export const builtinHandler = [
'home', 'problem', 'record', 'judge', 'user',
'contest', 'training', 'discussion', 'manage', 'import',
'misc', 'homework', 'domain', 'status',
'misc', 'homework', 'domain', 'status', 'api',
];
export const builtinScript = [
+3 -1
View File
@@ -78,5 +78,7 @@ export async function load(call: Entry) {
await modelSystem.set('db.ver', dbVer);
}
await bus.serial('app/started');
return await modelSystem.get('server.worker');
return typeof argv.options.worker === 'number'
? argv.options.worker
: await modelSystem.get('server.worker');
}
+50
View File
@@ -0,0 +1,50 @@
import graphql from 'graphql';
import UserModel from '../model/user';
import { Handler, Route } from '../service/server';
const schemaStr = `\
type Query {
user(id: Int, uname: String, mail: String): User
problem(id: Int): String
}
type User {
_id: Int!
uname: String!
mail: String!
}`;
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 (arg, ctx, node) => {
},
};
class ApiHandler extends Handler {
async get() {
const q = decodeURIComponent(this.ctx.request.querystring);
if (q === 'schema') {
this.response.body = { schema: schemaStr };
} else if (q) {
this.response.type = 'application/json';
this.response.body = await graphql.graphql(schema, q, root, this);
} else this.response.template = 'api.html';
}
async post() {
this.response.type = 'application/json';
this.response.body = await graphql.graphql(schema, this.args.query, root, this, this.args.variables);
}
}
export function apply() {
Route('api', '/api', ApiHandler);
}
global.Hydro.handler.api = apply;
+1 -1
View File
@@ -246,7 +246,7 @@ class RecordMainConnectionHandler extends ConnectionHandler {
else {
this.send({
html: await this.renderHTML('record_main_tr.html', {
rdoc, udoc, pdoc, tdoc,
rdoc, udoc, pdoc, tdoc, all: this.all,
}),
});
}
+4 -6
View File
@@ -439,7 +439,7 @@ export class Handler extends HandlerCommon {
query: ctx.query,
path: ctx.path,
params: ctx.params,
referer: ctx.request.headers.referer,
referer: ctx.request.headers.referer || '',
json: (ctx.request.headers.accept || '').includes('application/json'),
};
this.response = {
@@ -679,11 +679,9 @@ async function handle(ctx, HandlerClass, checker) {
h.domainId = args.domainId;
try {
const method = ctx.method.toLowerCase();
let operation: string;
if (method === 'post' && ctx.request.body.operation) {
operation = `_${ctx.request.body.operation}`
.replace(/_([a-z])/gm, (s) => s[1].toUpperCase());
}
const operation = (method === 'post' && ctx.request.body.operation)
? `_${ctx.request.body.operation}`.replace(/_([a-z])/gm, (s) => s[1].toUpperCase())
: '';
await bus.serial('handler/create', h);
+3
View File
@@ -29,6 +29,9 @@ require.extensions['.js'] = function loader(module, filename) {
const content = fs.readFileSync(filename, 'utf-8');
return module._compile(content, filename);
};
require.extensions['.ts'] = function loader(module, filename) {
return module._compile(transform(filename), filename);
};
const main = require('./main.js');
if (!module.parent) main();
+2 -2
View File
@@ -2,8 +2,8 @@ import { Page } from './Page';
export default class PageLoader {
constructor() {
const pages = require.context('../pages/', true, /\.page\.jsx?$/i);
const components = require.context('../components/', true, /\.page\.jsx?$/i);
const pages = require.context('../pages/', true, /\.page\.[jt]sx?$/i);
const components = require.context('../components/', true, /\.page\.[jt]sx?$/i);
this.pageInstances = [
...pages.keys().map((key) => {
const page = pages(key).default;
+2 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@hydrooj/ui-default",
"version": "4.17.8",
"version": "4.18.0",
"author": "undefined <i@undefined.moe>",
"license": "AGPL-3.0",
"main": "hydro.js",
@@ -41,6 +41,7 @@
"file-loader": "^6.2.0",
"friendly-errors-webpack-plugin": "^1.7.0",
"fs-extra": "^10.0.0",
"graphiql": "^1.4.2",
"gulp": "^4.0.2",
"gulp-cli": "^2.3.0",
"gulp-iconfont": "^11.0.0",
+2
View File
@@ -0,0 +1,2 @@
.graphiql-container .doc-explorer-title, .graphiql-container .history-title
padding: 2px !important
+45
View File
@@ -0,0 +1,45 @@
import { NamedPage } from 'vj/misc/Page';
import request from 'vj/utils/request';
import React from 'react';
import { render } from 'react-dom';
import 'graphiql/graphiql.css';
const Logo = () => <span>Hydro API Console </span>;
// @ts-ignore
const page = new NamedPage('api', async () => {
const [{ default: GraphiQL }, { buildSchema }, res] = await Promise.all([
import('graphiql'),
import('graphql'),
request.get('/api?schema'),
]);
// @ts-ignore
GraphiQL.Logo = Logo;
console.log(res);
const App = () => (
<GraphiQL
schema={buildSchema(res.schema)}
style={{ height: '100vh' }}
fetcher={async (graphQLParams) => {
const data = await fetch(
'/api',
{
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify(graphQLParams),
credentials: 'same-origin',
},
);
return data.json().catch(() => data.text());
}}
/>
);
render(<App />, document.getElementById('graphiql'));
});
export default page;
+4
View File
@@ -0,0 +1,4 @@
{% extends "layout/basic.html" %}
{% block content %}
<div id="graphiql" style="height:90vh;"></div>
{% endblock %}
@@ -1,9 +1,12 @@
{% macro render_status_td(rdoc, rid_key='_id', class='') %}
{% macro render_status_td(rdoc, rid_key='_id', class='', allDomain = false) %}
<td class="col--status record-status--border {{ class }} {{ model.builtin.STATUS_CODES[rdoc.status] }}">
<div class="col--status__text">
<span class="icon record-status--icon {{ model.builtin.STATUS_CODES[rdoc.status] }}"></span>
<span style="color: {{ utils.status.getScoreColor(rdoc.score|default(0)) }}">{{ rdoc.score|default(0) }}</span>
<a href="{{ url('record_detail', rid=rdoc[rid_key]) }}" class="record-status--text {{ model.builtin.STATUS_CODES[rdoc.status] }}">
<a
href="{{ url('record_detail', rid=rdoc[rid_key], domainId=rdoc.domainId) if allDomain else url('record_detail', rid=rdoc[rid_key]) }}"
class="record-status--text {{ model.builtin.STATUS_CODES[rdoc.status] }}"
>
{{ model.builtin.STATUS_TEXTS[rdoc.status] }}
</a>
</div>
@@ -10,14 +10,14 @@
<h1 class="section__title">{{ tdoc.title }}</h1>
</div>
<div class="section__body">
<span class="bp3-tag bp3-large bp3-minimal problem__tag-item">{{ _('Status') }}: {{ _(model.contest.statusText(tdoc)) }}</span>
<span class="bp3-tag bp3-large bp3-minimal problem__tag-item bp3-icon-standard bp3-icon-book">{{ _(model.contest.RULES[tdoc.rule].TEXT) }}</span>
<span class="bp3-tag bp3-large bp3-minimal problem__tag-item">{{ _(model.contest.statusText(tdoc)) }}</span>
<span class="bp3-tag bp3-large bp3-minimal problem__tag-item icon icon-award">{{ _(model.contest.RULES[tdoc.rule].TEXT) }}</span>
<span class="bp3-tag bp3-large bp3-minimal problem__tag-item">{{ _('Start at') }}: {{ contest.render_time(tdoc.beginAt) }}</span>
<span class="bp3-tag bp3-large bp3-minimal problem__tag-item">{{ _('End at') }}: {{ contest.render_time(tdoc.endAt) }}</span>
<span class="bp3-tag bp3-large bp3-minimal problem__tag-item icon icon-schedule">{{ contest.render_duration(tdoc) }} {{ _('hour(s)') }}</span>
<span class="bp3-tag bp3-large bp3-minimal problem__tag-item">{{ _('Host') }}: {{ user.render_inline(udict[tdoc.owner], badge=false) }}</span>
<span class="bp3-tag bp3-large bp3-minimal problem__tag-item">{{ _('Partic.') }}: {{ tdoc['attend']|default(0) }}</span>
<span class="bp3-tag bp3-large bp3-minimal problem__tag-item icon icon-user--multiple">{{ tdoc['attend']|default(0) }}</span>
{% if attended %}
<span class="bp3-tag bp3-large bp3-minimal problem__tag-item bp3-icon-standard bp3-icon-tick">{{ _('Attended') }}</span>
<span class="bp3-tag bp3-large bp3-minimal problem__tag-item icon icon-check">{{ _('Attended') }}</span>
{% endif %}
</div>
</div>
-1
View File
@@ -1,4 +1,3 @@
{% set no_path_section = true %}
{% extends "layout/basic.html" %}
{% block content %}
<div class="error__container clearfix">
@@ -1,10 +1,10 @@
{% import "components/record.html" as record with context %}
{% import "components/problem.html" as problem with context %}
<tr data-rid="{{ rdoc['_id'] }}">
{{ record.render_status_td(rdoc) }}
{{ record.render_status_td(rdoc, allDomain = all) }}
<td class="col--problem col--problem-name">
{% if handler.user.hasPerm(perm.PERM_REJUDGE) %}
<form class="form--inline" method="post" action="{{ url('record_detail', rid=rdoc._id) }}">
<form class="form--inline" method="post" action="{{ url('record_detail', rid=rdoc._id, domainId=rdoc.domainId) }}">
<input type="hidden" name="operation" value="rejudge">
<input type="hidden" name="csrfToken" value="{{ handler.csrfToken }}">
<button type="submit" class="link text-maroon lighter">