add basic wanikani auth
This commit is contained in:
1 parent
a82e49ae34
commit
646c7aed1f
8 files changed
+2490
No files matched your search
@@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"name": "@jaquiz/server",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"main": "lib/index.js",
|
||||||
|
"private": true,
|
||||||
|
"repository": "git@git.polv.cc:jaquiz/server.git",
|
||||||
|
"author": "Pacharapol Withayasakpunt <polv@polv.cc>",
|
||||||
|
"license": "MIT",
|
||||||
|
"scripts": {
|
||||||
|
"ts": "ts-node",
|
||||||
|
"build": "rm -r lib && tsc --rootDir src --outDir lib"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@databases/pg": "^5.1.1",
|
||||||
|
"axios": "^0.23.0",
|
||||||
|
"connect-pg-simple": "^7.0.0",
|
||||||
|
"dayjs": "^1.10.7",
|
||||||
|
"fastify": "^3.22.0",
|
||||||
|
"fastify-cookie": "^5.3.1",
|
||||||
|
"fastify-rate-limit": "^5.6.2",
|
||||||
|
"fastify-session": "^5.2.1",
|
||||||
|
"fastify-static": "^4.4.2",
|
||||||
|
"fastify-swagger": "^4.12.4",
|
||||||
|
"jsonschema-definer": "^1.3.2",
|
||||||
|
"short-uuid": "^4.2.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/better-sqlite3": "^7.4.0",
|
||||||
|
"@types/connect-pg-simple": "^4.2.4",
|
||||||
|
"@types/node": "^16.10.9",
|
||||||
|
"@types/pino": "^6.3.11",
|
||||||
|
"better-sqlite3": "^7.4.3",
|
||||||
|
"import-sort-parser-typescript": "^6.0.0",
|
||||||
|
"ts-node": "^10.3.0",
|
||||||
|
"typescript": "^4.4.4"
|
||||||
|
},
|
||||||
|
"importSort": {
|
||||||
|
".js, .ts": {
|
||||||
|
"parser": "typescript",
|
||||||
|
"style": "module"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
import qs from 'querystring'
|
||||||
|
|
||||||
|
import sql from '@databases/sql'
|
||||||
|
import axios from 'axios'
|
||||||
|
import ConnectPG from 'connect-pg-simple'
|
||||||
|
import fastify, { FastifyPluginAsync } from 'fastify'
|
||||||
|
import fCookie from 'fastify-cookie'
|
||||||
|
import rateLimit from 'fastify-rate-limit'
|
||||||
|
import fSession from 'fastify-session'
|
||||||
|
import fastifySwagger from 'fastify-swagger'
|
||||||
|
|
||||||
|
import { db, isDev } from '../shared'
|
||||||
|
|
||||||
|
const apiRouter: FastifyPluginAsync = async (f) => {
|
||||||
|
f.register(fCookie)
|
||||||
|
f.register(fSession, {
|
||||||
|
secret: process.env['SECRET']!,
|
||||||
|
// @ts-ignore
|
||||||
|
store: new (ConnectPG(fSession))(
|
||||||
|
process.env['DATABASE_URL']
|
||||||
|
? {
|
||||||
|
conString: process.env['DATABASE_URL'],
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
conObject: {
|
||||||
|
user: process.env['POSTGRES_USER'],
|
||||||
|
password: process.env['POSTGRES_PASSWORD'],
|
||||||
|
database: process.env['POSTGRES_DB'],
|
||||||
|
host: process.env['POSTGRES_HOST'],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
),
|
||||||
|
})
|
||||||
|
|
||||||
|
f.register(rateLimit, {
|
||||||
|
max: 10,
|
||||||
|
timeWindow: '1 second',
|
||||||
|
allowList: (req) => {
|
||||||
|
if (req.routerPath === '/api/quiz/getSrsLevel') {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
f.register(fastifySwagger, {
|
||||||
|
openapi: {
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
BearerAuth: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
components: {
|
||||||
|
securitySchemes: {
|
||||||
|
BearerAuth: {
|
||||||
|
type: 'http',
|
||||||
|
scheme: 'bearer',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
exposeRoute: isDev,
|
||||||
|
routePrefix: '/doc',
|
||||||
|
})
|
||||||
|
|
||||||
|
f.addHook('preHandler', async (req) => {
|
||||||
|
const { body, log } = req
|
||||||
|
|
||||||
|
if (body && typeof body === 'object' && body.constructor === Object) {
|
||||||
|
log.info({ body }, 'parsed body')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
f.addHook('preHandler', async (req) => {
|
||||||
|
if (['/api/doc', '/api/settings'].some((s) => req.url.startsWith(s))) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.session['userId']) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const [, apiKey] =
|
||||||
|
/^Bearer (.+)$/.exec(req.headers.authorization || '') || []
|
||||||
|
|
||||||
|
if (!apiKey) {
|
||||||
|
throw { statusCode: 401, message: 'no apiKey in header' }
|
||||||
|
}
|
||||||
|
|
||||||
|
const userData = await axios
|
||||||
|
.create({
|
||||||
|
baseURL: 'https://api.wanikani.com/v2/',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${apiKey}`,
|
||||||
|
},
|
||||||
|
validateStatus: function () {
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.get('/user')
|
||||||
|
.then(
|
||||||
|
(r) =>
|
||||||
|
r.data as {
|
||||||
|
data: {
|
||||||
|
id: string
|
||||||
|
username: string
|
||||||
|
level: number
|
||||||
|
subscription: {
|
||||||
|
active: boolean
|
||||||
|
type: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const userId = userData.data.id
|
||||||
|
|
||||||
|
await db.query(sql`
|
||||||
|
INSERT INTO "user" ("id", "level")
|
||||||
|
VALUES (${userId}, ${userData.data.level})
|
||||||
|
ON CONFLICT ("id") DO UPDATE SET
|
||||||
|
"level" = EXCLUDED."level"
|
||||||
|
`)
|
||||||
|
|
||||||
|
req.session['userId'] = userId
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export default apiRouter
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const app = fastify({
|
||||||
|
logger: {
|
||||||
|
prettyPrint: isDev,
|
||||||
|
serializers: {
|
||||||
|
req(req) {
|
||||||
|
if (!req.url) {
|
||||||
|
return { method: req.method }
|
||||||
|
}
|
||||||
|
|
||||||
|
const [url = '', q] = req.url.split(/\?(.+)$/)
|
||||||
|
const query = q ? qs.parse(q) : undefined
|
||||||
|
|
||||||
|
return { method: req.method, url, query }
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
app.register(apiRouter, {
|
||||||
|
prefix: '/api',
|
||||||
|
})
|
||||||
|
|
||||||
|
return app.listen(process.env['PORT']!, '0.0.0.0')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (require.main === module) {
|
||||||
|
main()
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import path from 'path'
|
||||||
|
import qs from 'querystring'
|
||||||
|
|
||||||
|
import fastify from 'fastify'
|
||||||
|
import fastifyStatic from 'fastify-static'
|
||||||
|
|
||||||
|
import apiRouter from './api'
|
||||||
|
import { gCloudLogger } from './logger'
|
||||||
|
import { isDev } from './shared'
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const port = parseInt(process.env['PORT']!) || 35594
|
||||||
|
process.env['PORT'] = port.toString()
|
||||||
|
|
||||||
|
const app = fastify({
|
||||||
|
logger: gCloudLogger({
|
||||||
|
prettyPrint: isDev,
|
||||||
|
serializers: {
|
||||||
|
req(req) {
|
||||||
|
if (!req.url) {
|
||||||
|
return { method: req.method }
|
||||||
|
}
|
||||||
|
|
||||||
|
const [url, q] = req.url.split(/\?(.+)$/)
|
||||||
|
const query = q ? qs.parse(q) : undefined
|
||||||
|
|
||||||
|
return { method: req.method, url, query }
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
app.register(apiRouter, {
|
||||||
|
prefix: '/api',
|
||||||
|
})
|
||||||
|
|
||||||
|
app.register(fastifyStatic, {
|
||||||
|
root: path.join(__dirname, '../public'),
|
||||||
|
redirect: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
app.setNotFoundHandler((_, reply) => {
|
||||||
|
reply.redirect(200, '/')
|
||||||
|
})
|
||||||
|
|
||||||
|
await app.listen(port, '0.0.0.0')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (require.main === module) {
|
||||||
|
main()
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import pino from 'pino'
|
||||||
|
|
||||||
|
const SeverityLookup = {
|
||||||
|
default: 'DEFAULT',
|
||||||
|
silly: 'DEFAULT',
|
||||||
|
verbose: 'DEBUG',
|
||||||
|
debug: 'DEBUG',
|
||||||
|
http: 'notice',
|
||||||
|
info: 'INFO',
|
||||||
|
warn: 'WARNING',
|
||||||
|
error: 'ERROR',
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultPinoConfig: pino.LoggerOptions = {
|
||||||
|
messageKey: 'message',
|
||||||
|
formatters: {
|
||||||
|
level(label, number) {
|
||||||
|
return {
|
||||||
|
severity: SeverityLookup[label as keyof typeof SeverityLookup],
|
||||||
|
level: number,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const gCloudLogger = (pinoConfigOverrides: pino.LoggerOptions = {}) =>
|
||||||
|
pino({
|
||||||
|
...defaultPinoConfig,
|
||||||
|
...pinoConfigOverrides,
|
||||||
|
})
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import createConnectionPool, { ConnectionPool } from '@databases/pg'
|
||||||
|
|
||||||
|
export const isDev = process.env['NODE_ENV'] === 'development'
|
||||||
|
|
||||||
|
export let db: ConnectionPool
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
if (!db) {
|
||||||
|
db = createConnectionPool({
|
||||||
|
...(process.env['DATABASE_URL']
|
||||||
|
? {
|
||||||
|
connectionString: process.env['DATABASE_URL'],
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
user: process.env['POSTGRES_USER']!,
|
||||||
|
password: process.env['POSTGRES_PASSWORD']!,
|
||||||
|
database: process.env['POSTGRES_DB']!,
|
||||||
|
host: process.env['POSTGRES_HOST']!,
|
||||||
|
port: parseInt(process.env['POSTGRES_PORT']!),
|
||||||
|
}),
|
||||||
|
bigIntMode: 'number',
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,299 @@
|
|||||||
|
import { SQLQuery, sql } from '@databases/pg'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
|
||||||
|
export class QSplit {
|
||||||
|
constructor(
|
||||||
|
private opts: {
|
||||||
|
default: (v: string) => SQLQuery | null
|
||||||
|
fields: {
|
||||||
|
[name: string]: {
|
||||||
|
[op: string]: (v: string) => SQLQuery
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {}
|
||||||
|
|
||||||
|
parse(q: string) {
|
||||||
|
const $and: SQLQuery[] = []
|
||||||
|
const $or: SQLQuery[] = []
|
||||||
|
const $not: SQLQuery[] = []
|
||||||
|
|
||||||
|
const ops = new Set(
|
||||||
|
Object.values(this.opts.fields).flatMap((def) => Object.keys(def))
|
||||||
|
)
|
||||||
|
for (let kv of this.doSplit(q, ' ')) {
|
||||||
|
let $current = $and
|
||||||
|
if (kv[0] === '-') {
|
||||||
|
$current = $not
|
||||||
|
kv = kv.substr(1)
|
||||||
|
} else if (kv[0] === '?') {
|
||||||
|
$current = $or
|
||||||
|
kv = kv.substr(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const op of ops) {
|
||||||
|
const segs = this.doSplit(kv, op)
|
||||||
|
|
||||||
|
if (segs.length === 1) {
|
||||||
|
const cond = this.opts.default(segs[0]!)
|
||||||
|
if (cond) {
|
||||||
|
$current.push(cond)
|
||||||
|
}
|
||||||
|
} else if (segs.length === 2) {
|
||||||
|
const fnMap = this.opts.fields[segs[0]!]
|
||||||
|
if (!fnMap) continue
|
||||||
|
|
||||||
|
const fn = fnMap[op]
|
||||||
|
if (!fn) continue
|
||||||
|
|
||||||
|
$current.push(fn(segs[1]!))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let cond: SQLQuery | null = null
|
||||||
|
|
||||||
|
if ($not.length) {
|
||||||
|
$and.push(sql`NOT ((${sql.join($not, ') AND (')}))`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($and.length) {
|
||||||
|
$or.push(sql`(${sql.join($and, ') AND (')})`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($or.length) {
|
||||||
|
cond = sql`((${sql.join($or, ') OR (')}))`
|
||||||
|
}
|
||||||
|
|
||||||
|
return cond
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ```js
|
||||||
|
* > this.doSplit('')
|
||||||
|
* []
|
||||||
|
* > this.doSplit('a:b "c:d e:f"')
|
||||||
|
* ['a:b', 'c:d e:f']
|
||||||
|
* > this.doSplit('a "b c" "d e"')
|
||||||
|
* ['a', 'b c', 'd e']
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
private doSplit(ss: string, splitter: string) {
|
||||||
|
const brackets = [
|
||||||
|
['"', '"'],
|
||||||
|
["'", "'"],
|
||||||
|
] as const
|
||||||
|
const keepBraces = false
|
||||||
|
|
||||||
|
const bracketStack = {
|
||||||
|
data: [] as string[],
|
||||||
|
push(c: string) {
|
||||||
|
this.data.push(c)
|
||||||
|
},
|
||||||
|
pop() {
|
||||||
|
return this.data.pop()
|
||||||
|
},
|
||||||
|
peek() {
|
||||||
|
return this.data.length > 0
|
||||||
|
? this.data[this.data.length - 1]
|
||||||
|
: undefined
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const tokenStack = {
|
||||||
|
data: [] as string[],
|
||||||
|
currentChars: [] as string[],
|
||||||
|
addChar(c: string) {
|
||||||
|
this.currentChars.push(c)
|
||||||
|
},
|
||||||
|
flush() {
|
||||||
|
const d = this.currentChars.join('')
|
||||||
|
if (d) {
|
||||||
|
this.data.push(d)
|
||||||
|
}
|
||||||
|
this.currentChars = []
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
let prev = ''
|
||||||
|
ss.split('').map((c) => {
|
||||||
|
if (prev === '\\') {
|
||||||
|
tokenStack.addChar(c)
|
||||||
|
} else {
|
||||||
|
let canAddChar = true
|
||||||
|
|
||||||
|
for (const [op, cl] of brackets) {
|
||||||
|
if (c === cl) {
|
||||||
|
if (bracketStack.peek() === op) {
|
||||||
|
bracketStack.pop()
|
||||||
|
canAddChar = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (c === op) {
|
||||||
|
bracketStack.push(c)
|
||||||
|
canAddChar = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (c === splitter && !bracketStack.peek()) {
|
||||||
|
tokenStack.flush()
|
||||||
|
} else {
|
||||||
|
if (keepBraces || canAddChar) {
|
||||||
|
tokenStack.addChar(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
prev = c
|
||||||
|
})
|
||||||
|
|
||||||
|
tokenStack.flush()
|
||||||
|
|
||||||
|
return tokenStack.data.map((s) => s.trim()).filter((s) => s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const qParseNum: (
|
||||||
|
k: SQLQuery
|
||||||
|
) => Record<string, (v: string) => SQLQuery> = (k) => ({
|
||||||
|
':': (v) => {
|
||||||
|
switch (v.toLocaleLowerCase()) {
|
||||||
|
case 'null':
|
||||||
|
return sql`${k} IS NULL`
|
||||||
|
case 'any':
|
||||||
|
return sql`${k} IS NOT NULL`
|
||||||
|
}
|
||||||
|
|
||||||
|
const m = /^([[\(])(\d+),(\d+)([\])])$/.exec(v)
|
||||||
|
if (m) {
|
||||||
|
let gt = sql`>`
|
||||||
|
let lt = sql`<`
|
||||||
|
|
||||||
|
if (m[1] === '[') gt = sql`>=`
|
||||||
|
if (m[4] === ']') gt = sql`<=`
|
||||||
|
|
||||||
|
return sql`${k} ${gt} ${parseInt(m[2])} OR ${k} ${lt} ${parseInt(m[3])}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return sql`${k} = ${parseInt(v)}`
|
||||||
|
},
|
||||||
|
'>': (v) => {
|
||||||
|
return v[0] === '='
|
||||||
|
? sql`${k} >= ${parseInt(v.substr(1))}`
|
||||||
|
: sql`${k} > ${parseInt(v)}`
|
||||||
|
},
|
||||||
|
'<': (v) => {
|
||||||
|
return v[0] === '='
|
||||||
|
? sql`${k} <= ${parseInt(v.substr(1))}`
|
||||||
|
: sql`${k} < ${parseInt(v)}`
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const reDur = /^([+-]?\d+(?:\.\d+)?)([A-Z]+)$/i
|
||||||
|
const toDate = (s: string) => {
|
||||||
|
const m = reDur.exec(s)
|
||||||
|
let d = dayjs(s)
|
||||||
|
if (m) {
|
||||||
|
d = dayjs().add(parseFloat(m[1]!), m[2] as any)
|
||||||
|
if (d.isValid()) {
|
||||||
|
return d.toDate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (d.isValid()) {
|
||||||
|
return d.toDate()
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const toBetween = (s: string) => {
|
||||||
|
const m = reDur.exec(s)
|
||||||
|
if (m && toDate(s) instanceof Date) {
|
||||||
|
return [
|
||||||
|
dayjs()
|
||||||
|
.add(parseFloat(m[1]!) - 0.5, m[2] as any)
|
||||||
|
.toDate(),
|
||||||
|
dayjs()
|
||||||
|
.add(parseFloat(m[1]!) + 0.5, m[2] as any)
|
||||||
|
.toDate(),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
export const qParseDate: (
|
||||||
|
k: SQLQuery
|
||||||
|
) => Record<string, (v: string) => SQLQuery> = (k) => ({
|
||||||
|
':': (v) => {
|
||||||
|
let b: Date[] = []
|
||||||
|
|
||||||
|
switch (v.toLocaleLowerCase()) {
|
||||||
|
case 'null':
|
||||||
|
return sql`${k} IS NULL`
|
||||||
|
case 'any':
|
||||||
|
return sql`${k} IS NOT NULL`
|
||||||
|
case 'now':
|
||||||
|
b = toBetween('-0.5d')
|
||||||
|
}
|
||||||
|
|
||||||
|
const reBetween =
|
||||||
|
/^([[\(])([+-]?\d+(?:\.\d+)?[A-Z]+),([+-]?\d+(?:\.\d+)?[A-Z]+)([\])])$/i
|
||||||
|
const m = reBetween.exec(v)
|
||||||
|
if (m) {
|
||||||
|
let gt = sql`>`
|
||||||
|
let lt = sql`<`
|
||||||
|
|
||||||
|
if (m[1] === '[') gt = sql`>=`
|
||||||
|
if (m[4] === ']') gt = sql`<=`
|
||||||
|
|
||||||
|
return sql`${k} ${gt} ${toDate(m[2]!)} OR ${k} ${lt} ${toDate(m[3]!)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reDur.test(v)) {
|
||||||
|
b = toBetween(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (b.length === 2) {
|
||||||
|
return sql`${k} > ${b[0]} AND ${k} > ${b[1]}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return sql`FALSE`
|
||||||
|
},
|
||||||
|
'>': (v) => sql`${k} > ${toDate(v)}`,
|
||||||
|
'<': (v) => sql`${k} < ${toDate(v)}`,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const makeQuiz = new QSplit({
|
||||||
|
default: () => null,
|
||||||
|
fields: {
|
||||||
|
srsLevel: qParseNum(sql`quiz."srsLevel"`),
|
||||||
|
nextReview: qParseDate(sql`quiz."nextReview"`),
|
||||||
|
lastRight: qParseDate(sql`quiz."lastRight"`),
|
||||||
|
lastWrong: qParseDate(sql`quiz."lastWrong"`),
|
||||||
|
maxRight: qParseNum(sql`quiz."maxRight"`),
|
||||||
|
maxWrong: qParseNum(sql`quiz."maxWrong"`),
|
||||||
|
rightStreak: qParseNum(sql`quiz."rightStreak"`),
|
||||||
|
wrongStreak: qParseNum(sql`quiz."wrongStreak"`),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const makeTag = new QSplit({
|
||||||
|
default: () => null,
|
||||||
|
fields: {
|
||||||
|
tag: { ':': (v) => sql`${v} = ANY("entry"."tag")` },
|
||||||
|
type: {
|
||||||
|
':': (v) => sql`"entry"."type" = ${v.replace(/hanzi/gi, 'character')}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const makeLevel = new QSplit({
|
||||||
|
default: () => null,
|
||||||
|
fields: {
|
||||||
|
level: qParseNum(sql`"entry"."level"`),
|
||||||
|
hLevel: qParseNum(sql`"entry"."hLevel"`),
|
||||||
|
},
|
||||||
|
})
|
||||||
+100
@@ -0,0 +1,100 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
/* Visit https://aka.ms/tsconfig.json to read more about this file */
|
||||||
|
|
||||||
|
/* Projects */
|
||||||
|
// "incremental": true, /* Enable incremental compilation */
|
||||||
|
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
|
||||||
|
// "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */
|
||||||
|
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */
|
||||||
|
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
|
||||||
|
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
|
||||||
|
|
||||||
|
/* Language and Environment */
|
||||||
|
"target": "es2019", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
|
||||||
|
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
|
||||||
|
// "jsx": "preserve", /* Specify what JSX code is generated. */
|
||||||
|
// "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
|
||||||
|
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
|
||||||
|
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */
|
||||||
|
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
|
||||||
|
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */
|
||||||
|
// "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */
|
||||||
|
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
|
||||||
|
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
|
||||||
|
|
||||||
|
/* Modules */
|
||||||
|
"module": "commonjs", /* Specify what module code is generated. */
|
||||||
|
// "rootDir": "./", /* Specify the root folder within your source files. */
|
||||||
|
// "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
|
||||||
|
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
|
||||||
|
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
|
||||||
|
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
|
||||||
|
// "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */
|
||||||
|
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
|
||||||
|
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||||
|
// "resolveJsonModule": true, /* Enable importing .json files */
|
||||||
|
// "noResolve": true, /* Disallow `import`s, `require`s or `<reference>`s from expanding the number of files TypeScript should add to a project. */
|
||||||
|
|
||||||
|
/* JavaScript Support */
|
||||||
|
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */
|
||||||
|
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
|
||||||
|
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */
|
||||||
|
|
||||||
|
/* Emit */
|
||||||
|
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
|
||||||
|
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
|
||||||
|
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
|
||||||
|
"sourceMap": true, /* Create source map files for emitted JavaScript files. */
|
||||||
|
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */
|
||||||
|
// "outDir": "./", /* Specify an output folder for all emitted files. */
|
||||||
|
// "removeComments": true, /* Disable emitting comments. */
|
||||||
|
// "noEmit": true, /* Disable emitting files from a compilation. */
|
||||||
|
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
|
||||||
|
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */
|
||||||
|
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
|
||||||
|
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
|
||||||
|
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||||
|
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
|
||||||
|
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
|
||||||
|
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
|
||||||
|
// "newLine": "crlf", /* Set the newline character for emitting files. */
|
||||||
|
// "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */
|
||||||
|
// "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */
|
||||||
|
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
|
||||||
|
// "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */
|
||||||
|
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
|
||||||
|
|
||||||
|
/* Interop Constraints */
|
||||||
|
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
|
||||||
|
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
|
||||||
|
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */
|
||||||
|
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
|
||||||
|
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
|
||||||
|
|
||||||
|
/* Type Checking */
|
||||||
|
"strict": true, /* Enable all strict type-checking options. */
|
||||||
|
"noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */
|
||||||
|
"strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */
|
||||||
|
"strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
|
||||||
|
"strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */
|
||||||
|
"strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
|
||||||
|
"noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */
|
||||||
|
"useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */
|
||||||
|
"alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
|
||||||
|
"noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */
|
||||||
|
"noUnusedParameters": true, /* Raise an error when a function parameter isn't read */
|
||||||
|
"exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
|
||||||
|
"noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
|
||||||
|
"noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
|
||||||
|
"noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */
|
||||||
|
"noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
|
||||||
|
"noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */
|
||||||
|
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
|
||||||
|
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
|
||||||
|
|
||||||
|
/* Completeness */
|
||||||
|
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
|
||||||
|
"skipLibCheck": true /* Skip type checking all .d.ts files. */
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user