PronounsPage/server/nouns.ts

150 lines
5.2 KiB
TypeScript

/* eslint-disable camelcase */
import SQL from 'sql-template-strings';
import { loadWords, Noun } from '#shared/classes.ts';
import type { NounRaw } from '#shared/classes.ts';
import { clearKey, PermissionAreas } from '#shared/helpers.ts';
import { addWordsFromClassInstance } from '#shared/nouns.ts';
import type { NounsData } from '#shared/nouns.ts';
import type { User } from '#shared/user.ts';
import type { Config } from '~~/locale/config.ts';
import type { Database } from '~~/server/db.ts';
import type { UserRow } from '~~/server/express/user.ts';
import type { SourceRow } from '~~/server/sources.ts';
import type { IsGrantedFn } from '~~/server/utils/useAuthentication.ts';
export interface NounRow {
id: string;
key: string;
words: string;
classInstance: string | null;
approved: number;
base_id: string | null;
locale: string;
author_id: string | null;
deleted: number;
sources: string | null;
categories: string | null;
}
type NounRowWithAuthor = NounRow & { author: User['username'] };
export const parseNounRow = (nounRow: NounRow): Omit<NounRaw, 'sourcesData'> => {
return {
id: nounRow.id,
key: nounRow.key,
words: JSON.parse(nounRow.words),
classInstance: nounRow.classInstance ? JSON.parse(nounRow.classInstance) : null,
categories: nounRow.categories?.split('|') ?? [],
sources: nounRow.sources ? nounRow.sources.split(',') : [],
};
};
export const buildNoun = (nounRaw: Omit<NounRaw, 'sourcesData'>, config: Config, nounsData: NounsData): Noun => {
const nounRawWithLoadedWords = loadWords(nounRaw, config, { singular: {}, plural: {} });
if (!nounRawWithLoadedWords.classInstance) {
return new Noun(config, nounRawWithLoadedWords);
}
const words =
addWordsFromClassInstance(nounRawWithLoadedWords.words, nounRawWithLoadedWords.classInstance, nounsData);
return new Noun(config, { ...nounRaw, words });
};
const parseNounRowWithAuthor = (nounRow: NounRowWithAuthor, isGranted: IsGrantedFn): Omit<NounRaw, 'sourcesData'> => {
const noun = parseNounRow(nounRow);
if (isGranted(PermissionAreas.Nouns)) {
noun.approved = !!nounRow.approved;
noun.base = nounRow.base_id;
noun.author = nounRow.author;
}
return noun;
};
export const addVersions = async (
db: Database,
isGranted: IsGrantedFn,
locale: string,
nouns: Omit<NounRaw, 'sourcesData'>[],
) => {
const keys = new Set(nouns.flatMap((nounsRaw) => nounsRaw.sources ?? [])
.map((sourceKey) => `'${clearKey(sourceKey.split('#')[0])}'`));
const sources = await db.all<SourceRow & Pick<UserRow, 'username'>>(SQL`
SELECT s.*, u.username AS submitter FROM sources s
LEFT JOIN users u ON s.submitter_id = u.id
WHERE s.locale == ${locale}
AND s.deleted = 0
AND s.approved >= ${isGranted(PermissionAreas.Sources) ? 0 : 1}
AND s.key IN (`.append([...keys].join(',')).append(SQL`)
`));
const sourcesMap: Record<string, SourceRow> = {};
sources.forEach((s) => sourcesMap[s.key!] = s);
return nouns.map((nounRaw) => ({
...nounRaw,
sourcesData: (nounRaw.sources ?? [])
.map((sourceKey) => selectFragment(sourcesMap, sourceKey))
.filter((source) => source !== undefined),
}));
};
const selectFragment = (sourcesMap: Record<string, SourceRow>, keyAndFragment: string) => {
const [key, fragment] = keyAndFragment.split('#');
if (sourcesMap[key] === undefined) {
return undefined;
}
if (fragment === undefined) {
return sourcesMap[key];
}
const source = { ...sourcesMap[key] };
const fragments = source.fragments
? source.fragments.replace(/\\@/g, '###').split('@')
.map((x) => x.replace(/###/g, '@'))
: [];
source.fragments = fragments[parseInt(fragment) - 1];
return source;
};
export const getNounEntries = defineCachedFunction(async (
db: Database,
isGranted: IsGrantedFn,
locale: string,
): Promise<NounRaw[]> => {
const nouns = (await db.all<NounRowWithAuthor>(SQL`
SELECT n.*, u.username AS author FROM nouns n
LEFT JOIN users u ON n.author_id = u.id
WHERE n.locale = ${locale}
AND n.deleted = 0
AND n.approved >= ${isGranted(PermissionAreas.Nouns) ? 0 : 1}
ORDER BY n.approved, n.key
`)).map((nounRow) => parseNounRowWithAuthor(nounRow, isGranted));
return await addVersions(db, isGranted, locale, nouns);
}, {
name: 'nouns',
getKey: (db, isGranted, locale) => locale,
shouldBypassCache: (db, isGranted) => isGranted(PermissionAreas.Nouns),
maxAge: 24 * 60 * 60,
});
export const approveNounEntry = async (db: Database, id: string, locale: string) => {
const { base_id } = (await db.get<Pick<NounRow, 'base_id'>>(SQL`SELECT base_id FROM nouns WHERE id=${id}`))!;
if (base_id) {
await db.get(SQL`
UPDATE nouns
SET deleted=1
WHERE id = ${base_id}
`);
}
await db.get(SQL`
UPDATE nouns
SET approved = 1, base_id = NULL
WHERE id = ${id}
`);
await invalidateCacheKind('nouns', locale);
};