feat: add API

- try localhost:1225/search?search_term=akk
This commit is contained in:
Sheldon Cooper 2025-09-11 23:15:08 -04:00
parent fdae502ec7
commit 38919f3a5a
13 changed files with 367 additions and 161 deletions

View file

@ -25,6 +25,7 @@
"express": "^5.1.0",
"google-auth-library": "^10.3.0",
"googleapis": "^159.0.0",
"node-fetch": "^3.3.2",
"reflect-metadata": "^0.2.2",
"sqlite3": "^5.1.7",
"typeorm": "0.3.26"

View file

@ -1,5 +1,6 @@
import { promises as fs } from "fs";
import fetch from 'node-fetch';
import * as path from "path";
import { authenticate } from "@google-cloud/local-auth";
import { google } from "googleapis";

View file

@ -1,124 +1,120 @@
import "reflect-metadata"
import "reflect-metadata";
import {
Entity,
PrimaryGeneratedColumn,
Column,
PrimaryColumn,
BaseEntity,
ManyToOne,
OneToMany,
OneToOne,
ManyToMany,
JoinTable,
ColumnType
Entity,
PrimaryGeneratedColumn,
Column,
PrimaryColumn,
BaseEntity,
ManyToOne,
OneToMany,
OneToOne,
ManyToMany,
JoinTable,
ColumnType,
} from "typeorm";
@Entity()
export class Lemma extends BaseEntity {
@PrimaryColumn({type: 'text'})
lemma_name: string;
@PrimaryColumn({ type: "text" })
lemma_name: string;
@OneToMany(() => WordForm, (word_form) => word_form.lemma)
word_forms: WordForm[];
@OneToMany(() => WordForm, (word_form) => word_form.lemma, { cascade: true})
word_forms: WordForm[];
@OneToMany(() => Example, (example) => example.lemma)
examples: Example[];
@OneToMany(() => Example, (example) => example.lemma)
examples: Example[];
@OneToMany(() => Definition, (definition) => definition.lemma)
definitions: Definition[];
@OneToMany(() => Definition, (definition) => definition.lemma)
definitions: Definition[];
@OneToMany(() => Comment, (comment) => comment.lemma)
comments: Comment[];
@OneToMany(() => Comment, (comment) => comment.lemma)
comments: Comment[];
@OneToMany(() => Media, (media) => media.lemma)
media: Media[];
@ManyToMany(() => PartOfSpeech)
@JoinTable()
parts_of_speech: PartOfSpeech[];
@OneToMany(() => Media, (media) => media.lemma)
media: Media[];
@ManyToMany(() => PartOfSpeech)
@JoinTable()
parts_of_speech: PartOfSpeech[];
}
@Entity()
export class Lect extends BaseEntity {
@PrimaryColumn({type: 'text'})
name: string;
@OneToMany(() => WordForm, (w) => w.lect)
word_forms: WordForm[];
@PrimaryColumn({ type: "text" })
name: string;
@OneToMany(() => WordForm, (w) => w.lect)
word_forms: WordForm[];
}
@Entity()
export class WordForm extends BaseEntity {
@PrimaryColumn({type: 'integer'})
word_form_id: number
@Column({type: 'text'})
word_form: string;
@PrimaryColumn({ type: "integer" })
word_form_id: number;
@ManyToOne(() => Lemma, (lemma) => lemma.word_forms)
lemma: Lemma;
@Column({ type: "text" })
word_form: string;
@ManyToOne(() => Lect, (lect) => lect.word_forms)
lect: Lect;
@ManyToOne(() => Lemma, (lemma) => lemma.word_forms)
lemma: Lemma;
@ManyToOne(() => Lect, (lect) => lect.word_forms)
lect: Lect;
}
@Entity()
export class Example extends BaseEntity {
@PrimaryGeneratedColumn()
example_id: number;
@PrimaryGeneratedColumn()
example_id: number;
@Column({ nullable: false, type: 'text' })
example_text: string;
@Column({ nullable: false, type: "text" })
example_text: string;
@ManyToOne(() => Lemma, (lemma) => lemma.examples)
lemma: Lemma;
@ManyToOne(() => Lemma, (lemma) => lemma.examples)
lemma: Lemma;
}
@Entity()
export class Media extends BaseEntity {
@PrimaryGeneratedColumn()
media_id: number;
@PrimaryGeneratedColumn()
media_id: number;
@Column({ nullable: false, type: 'text'})
media_url: string;
@Column({ nullable: false, type: "text" })
media_url: string;
@ManyToOne(() => Lemma, (lemma) => lemma.media)
lemma: Lemma;
@ManyToOne(() => Lemma, (lemma) => lemma.media)
lemma: Lemma;
}
@Entity()
export class Definition extends BaseEntity {
@PrimaryGeneratedColumn()
definition_id: number;
@PrimaryGeneratedColumn()
definition_id: number;
@Column({ nullable: false, type: 'text' })
definition_text: string;
@Column({ nullable: false, type: "text" })
definition_text: string;
@ManyToOne(() => Lemma, (lemma) => lemma.definitions)
lemma: Lemma;
@ManyToOne(() => Lemma, (lemma) => lemma.definitions)
lemma: Lemma;
}
@Entity()
export class Comment extends BaseEntity {
@PrimaryGeneratedColumn()
comment_id: number;
@PrimaryGeneratedColumn()
comment_id: number;
@Column({ nullable: false, type: 'text' })
comment_text: string;
@Column({ nullable: false, type: "text" })
comment_text: string;
@ManyToOne(() => Lemma, (lemma) => lemma.comments)
lemma: Lemma;
@ManyToOne(() => Lemma, (lemma) => lemma.comments)
lemma: Lemma;
}
@Entity()
export class PartOfSpeech extends BaseEntity {
@PrimaryColumn({type: 'text'})
long_form: string;
@PrimaryColumn({ type: "text" })
long_form: string;
@Column({ nullable: false, unique: true, type: 'text' })
short_form: string;
@Column({ nullable: false, unique: true, type: "text" })
short_form: string;
}

View file

@ -1,10 +1,11 @@
import "reflect-metadata";
import fetch from "node-fetch";
import { SAMPLE } from "@repo/common/sample";
import express from "express";
import { google, sheets_v4 } from "googleapis";
import { OAuth2Client } from 'google-auth-library';
import { OAuth2Client } from "google-auth-library";
import { appDataSource } from "./config/dbconfig.js";
import { authorize } from "./auth.js"
import { authorize } from "./auth.js";
import {
Lemma,
WordForm,
@ -18,7 +19,7 @@ import {
const RELOAD_SHEET_ON_START = false;
global.fetch = fetch as any;
appDataSource
.initialize()
@ -39,7 +40,6 @@ appDataSource
})
.catch((error) => console.log(error));
function initExpress() {
const app = express();
const PORT = 1225;
@ -48,28 +48,19 @@ function initExpress() {
const word_form_repository = appDataSource.getRepository(WordForm);
const lemma_repository = appDataSource.getRepository(Lemma);
app.get("/", (req, res) => {
res.render("search", { title: "Suha", message: "Bratsa" });
});
app.get("/sample", (_req, res) => {
res.status(200).send(SAMPLE);
});
app.get("/search", async (req, res) => {
const search_term = req.query.search_term?.toString();
if(!search_term) {
if (!search_term) {
return;
}
const lemmas: Lemma[] = (
await Lemma.find({
relations: {
word_forms: { lect: true },
},
})
await Lemma.find({ relations: { word_forms: { lect: true } } })
).filter((e) => {
for (const wf of e.word_forms) {
if (wf.word_form.includes(search_term)) {
@ -79,15 +70,34 @@ function initExpress() {
});
const lects = await Lect.find();
return res.render("search_results", {
res.status(200).send({
terms: lemmas.length,
results: lemmas,
lects: lects,
});
});
app.set("views", "./res/views");
app.set("view engine", "pug");
app.get("/lect", async (req, res) => {
const name = req.query.name?.toString();
if (!name) {
return;
}
const lect = await Lect.findOne({
where: { name: name },
relations: { word_forms: { lemma: true } },
});
res.status(200).send({ lect });
});
app.get("/lects", async (req, res) => {
const lects = await Lect.find()
res.status(200).send({
lects
});
});
app.listen(PORT, () => {
console.log(`Backend started @ http://localhost:${PORT} !`);
@ -99,86 +109,101 @@ function initExpress() {
* @param {google.auth.OAuth2Client} auth The authenticated Google OAuth client.
*/
async function loadSheet(auth: OAuth2Client) {
const options: any = { version: "v4", auth };
const sheets = google.sheets(options);
const res = await sheets.spreadsheets.values.get({
spreadsheetId: "1-YkCeynx_-KYdubvt14augSPo37_20YgUv_f-i8HVwY",
range: "al_ko_mit_govor",
});
const lect_repository = appDataSource.getRepository(Lect);
const word_form_repository = appDataSource.getRepository(WordForm);
const lemma_repository = appDataSource.getRepository(Lemma);
const options: any = { version: "v4", auth };
const sheets = google.sheets(options);
const res = await sheets.spreadsheets.values.get({
spreadsheetId: "1-YkCeynx_-KYdubvt14augSPo37_20YgUv_f-i8HVwY",
range: "al_ko_mit_govor",
});
const rows = res.data.values;
const keys_res = await sheets.spreadsheets.values.get({
spreadsheetId: "1-YkCeynx_-KYdubvt14augSPo37_20YgUv_f-i8HVwY",
range: "klucz",
});
const rows = res.data.values;
const keys = keys_res.data.values;
if (!rows || rows.length === 0) {
console.error("No data found.");
return;
}
const keys_res = await sheets.spreadsheets.values.get({
spreadsheetId: "1-YkCeynx_-KYdubvt14augSPo37_20YgUv_f-i8HVwY",
range: "klucz",
});
if (!keys || keys.length === 0) {
console.error("No lemma keys found.");
return;
}
const keys = keys_res.data.values;
const lects = rows.shift();
if (!rows || rows.length === 0) {
console.error("No data found.");
return;
}
if (keys.length != rows.length) {
console.error("Lemma count doesn't match number of rows.");
return;
}
if (!keys || keys.length === 0) {
console.error("No lemma keys found.");
return;
}
if (!lects){
console.error("No lects found");
return;
}
const lects = rows.shift();
for (const lect of lects) {
let l = new Lect();
l.name = lect;
l.save();
console.log(l);
}
if (keys.length != rows.length) {
console.error("Lemma count doesn't match number of rows.");
return;
}
const nikolect = lects.indexOf("Nikomiko");
if (!lects) {
console.error("No lects found");
return;
}
for (let i = 0; i < rows.length; i++) {
const row = rows[i];
for (const lect of lects) {
let l = new Lect();
l.name = lect;
l.save();
console.log(l);
}
const lemma_key = keys[i]?.[0];
const lemma = new Lemma();
const nikolect = lects.indexOf("Nikomiko");
const lemmas = Array<Lemma>();
for (let i = 0; i < rows.length; i++) {
const row = rows[i];
if (lemma_key == null || lemma_key.length == 0) {
//assign a random UUID to the lemma as punishment for our failures
lemma.lemma_name = crypto.randomUUID();
console.log(`womp womp, missing lemma name, calling it ${lemma.lemma_name}`)
} else {
lemma.lemma_name = lemma_key;
}
const lemma_key = keys[i]?.[0];
const lemma = new Lemma();
console.log(lemma);
await lemma.save();
if (lemma_key == null || lemma_key.length == 0) {
//assign a random UUID to the lemma as punishment for our failures
lemma.lemma_name = crypto.randomUUID();
console.log(
`womp womp, missing lemma name, calling it ${lemma.lemma_name}`,
);
} else {
lemma.lemma_name = lemma_key;
}
for(let i=0; i<rows.length; i++){
const cell = row?.[i];
if (cell === null || cell === undefined || (typeof cell === "string" && cell.length === 0)) {
continue;
}
lemmas.push(lemma);
lemma.word_forms = Array<WordForm>();
for (let word_form of cell.split(";")) {
const f = new WordForm();
f.word_form = word_form;
f.lemma = lemma;
f.lect = lects[i];
//console.log(f);
await f.save();
}
for (let i = 0; i < rows.length; i++) {
const cell = row?.[i];
if (
cell === null
|| cell === undefined
|| (typeof cell === "string" && cell.length === 0)
) {
continue;
}
}
for (let word_form of cell.split(";")) {
const f = new WordForm();
f.word_form = word_form;
f.lect = lects[i];
//console.log(f);
lemma.word_forms.push(f);
}
}
}
}
}
for (let i = 0; i < lemmas.length; i += 100) {
await lemma_repository.save(lemmas.slice(i, i + 100));
console.log(
`Saved ${Math.min(i + 100, lemmas.length)} / ${lemmas.length} lemmas...`,
);
}
console.log(`Loaded ${lemmas.length} lemmas!`);
}

View file

@ -11,6 +11,7 @@
"dependencies": {
"@tailwindcss/vite": "^4.1.6",
"@types/node": "^22.15.17",
"axios": "^1.11.0",
"bulma": "^1.0.4",
"tailwindcss": "^4.1.6",
"vue": "^3.5.13",

View file

@ -42,9 +42,9 @@ const toggleBurger = (): void => {
<RouterLink class="navbar-item" to="/resources"
>Resources</RouterLink
>
<RouterLink class="navbar-item" to="/resources">{{
SAMPLE
}}</RouterLink>
<RouterLink class="navbar-item" to="/kotoba">
Kotoba
</RouterLink>
</div>
</div>
</nav>

View file

@ -0,0 +1,17 @@
<template>
<div>
<section class="section">
<h1 class="title">Tropos-agnostic search</h1>
</section>
<section class="section container">
<input type="text" name="text" id="aaa">
</section>
</div>
</template>
<!-- -->
<script setup lang="ts">
</script>

View file

@ -3,10 +3,12 @@ import type { RouteRecordRaw } from "vue-router";
import HomePage from "@/components/pages/HomePage.vue";
import ResourcesPage from "@/components/pages/ResourcesPage.vue";
import KotobaPage from "@/components/pages/KotobaPage.vue";
const routes: RouteRecordRaw[] = [
{ path: "/", name: "Home", component: HomePage },
{ path: "/resources", name: "Resources", component: ResourcesPage },
{ path: "/kotoba", name: "Kotoba", component: KotobaPage },
// {
// path: '/:pathMatch(.*)*', // Vue Router 4 catch-all for 404s
// name: 'NotFound',