import { sql } from '@databases/pg' import { FastifyPluginAsync } from 'fastify' import S from 'jsonschema-definer' import { db } from '../shared' import { QSplit, pgOp, qParseNum } from '../util/search' export const allRouter: FastifyPluginAsync = async (f) => { { const sQuery = S.shape({ q: S.string(), offset: S.integer().optional(), limit: S.integer().optional(), distinct: S.string().enum('id').optional(), }) const sResponse = S.shape({ result: S.list( S.shape({ entry: S.string(), type: S.string(), }).additionalProperties(true) ), offset: S.integer(), limit: S.integer(), count: S.integer(), }) const makeEntry = new QSplit({ default(v) { if (v.includes('*')) { return sql`"entry" && ( SELECT array_agg("entry") FROM "frequency" WHERE "entry" LIKE ${ '%' + v.replace('*', '%') + '%' } )` } if (/[\p{sc=Han}\p{sc=Katakana}\p{sc=Hiragana}]/u.test(v)) { return sql`(${sql.join( [ this.fields['entry']![':']!(v), this.fields['reading']![':']!(v), this.fields['translation']![':']!(v), ], ' OR ' )})` } if (/[^\p{L}\p{N}\p{M} :-]/u.test(v)) { return null } return sql`(${sql.join( [ this.fields['reading']![':']!(v), this.fields['translation']![':']!(v), ], ' OR ' )})` }, fields: { entry: { ':': (v) => sql`"entry" &@ ${v}` }, kana: { ':': (v) => sql`"reading" &@ ${v}` }, reading: { ':': (v) => sql`"reading" &@ ${v}` }, kunyomi: { ':': (v) => sql`"reading" ${pgOp} ${`paths @ ".kunyomi" && string @ ${JSON.stringify( v )}`}`, }, onyomi: { ':': (v) => sql`"reading" ${pgOp} ${`paths @ ".onyomi" && string @ ${JSON.stringify( v )}`}`, }, nanori: { ':': (v) => sql`"reading" ${pgOp} ${`paths @ ".nanori" && string @ ${JSON.stringify( v )}`}`, }, english: { ':': (v) => sql`"translation" ${pgOp} ${`paths @ ".en" && string @ ${JSON.stringify( v )}`}`, }, translation: { ':': (v) => sql`"translation" &@ ${v}` }, type: { ':': (v) => sql`"_type" @> ARRAY[${v.toLocaleLowerCase()}]`, }, }, }) const makeLevel = new QSplit({ default: () => null, fields: { level: qParseNum(sql`("wanikani.subjects"."data" -> 'level')::int`), }, }) f.get<{ Querystring: typeof sQuery.type }>( '/q', { schema: { querystring: sQuery.valueOf(), response: { 200: sResponse.valueOf(), }, }, }, async (req): Promise => { const { q, offset = 0, limit = 5 } = req.query if (!q.trim()) { return { result: [], offset, limit, count: 0, } } const entryCond = makeEntry.parse(q) const levelCond = makeLevel.parse(q) const [r] = await db.query(sql` WITH cte AS ( SELECT DISTINCT ON (unnest, "type") unnest "entry", "type", "priority", "frequency" FROM ( SELECT unnest("entry"), * FROM "entry" ${ levelCond ? sql`JOIN "wanikani.subjects" ON "entry"."_type" @> ARRAY["wanikani.subjects"."object"] AND "entry"."entry" @> ARRAY["wanikani.subjects"."data" ->> 'characters']` : sql`` } WHERE ${entryCond || sql`TRUE`} AND ${levelCond || sql`TRUE`} ) t1 JOIN "frequency" ON "frequency"."entry" = unnest ) SELECT (SELECT array_agg(jsonb_build_object( 'entry', "entry", 'type', "type", 'frequency', "frequency" )) FROM ( SELECT * FROM cte ORDER BY "priority" DESC, "frequency" DESC ${limit > 0 ? sql`LIMIT ${limit}` : sql``} OFFSET ${offset} ) t1)||'{}'::jsonb[] "result", ${offset}::int "offset", ${limit}::int "limit", (SELECT count(*) FROM cte) "count" `) return ( r || { result: [], offset, limit, count: 0, } ) } ) } }