From 10324d57997e804b19e7bf4b9be2095eef38335d Mon Sep 17 00:00:00 2001 From: monoid Date: Thu, 26 Oct 2023 23:58:33 +0900 Subject: [PATCH] feat: add api --- db/db.ts | 11 +++ db/deno-sqlite-dialect-config.ts | 27 ++++++++ db/deno-sqlite-dialect.ts | 49 ++++++++++++++ db/kysely-sqlite-driver.ts | 112 +++++++++++++++++++++++++++++++ db/type.ts | 64 ++++++++++++++++++ deno.json | 4 +- fresh.gen.ts | 36 +++++----- islands/Search.tsx | 5 ++ routes/api/corps/[index].ts | 35 ++++++++++ routes/api/corps/index.ts | 21 ++++++ routes/api/kosdaq.ts | 16 ++--- routes/api/kospi.ts | 16 ++--- 12 files changed, 363 insertions(+), 33 deletions(-) create mode 100644 db/db.ts create mode 100644 db/deno-sqlite-dialect-config.ts create mode 100644 db/deno-sqlite-dialect.ts create mode 100644 db/kysely-sqlite-driver.ts create mode 100644 db/type.ts create mode 100644 islands/Search.tsx create mode 100644 routes/api/corps/[index].ts create mode 100644 routes/api/corps/index.ts diff --git a/db/db.ts b/db/db.ts new file mode 100644 index 0000000..7fa5bcf --- /dev/null +++ b/db/db.ts @@ -0,0 +1,11 @@ +import { Kysely, ParseJSONResultsPlugin } from "kysely"; +import { DB as Sqlite } from "sqlite"; +import { DenoSqliteDialect } from "./deno-sqlite-dialect.ts"; +import { Database } from "./type.ts"; + +export const db = new Kysely({ + dialect: new DenoSqliteDialect({ + database: new Sqlite("stock.db") + }), + plugins: [new ParseJSONResultsPlugin()] +}); diff --git a/db/deno-sqlite-dialect-config.ts b/db/deno-sqlite-dialect-config.ts new file mode 100644 index 0000000..482daa3 --- /dev/null +++ b/db/deno-sqlite-dialect-config.ts @@ -0,0 +1,27 @@ +/// The MIT License (MIT) +/// Copyright (c) 2023 Alex Gleason +/// Copyright (c) 2022 Sami Koskimäki +/// https://gitlab.com/soapbox-pub/kysely-deno-sqlite + +import type { SqliteDialectConfig } from 'kysely'; + +/** Type compatible with both [dyedgreen/deno-sqlite](https://github.com/dyedgreen/deno-sqlite) and [denodrivers/sqlite3](https://github.com/denodrivers/sqlite3). */ +type DenoSqlite = + & { + close(): void; + changes: number; + lastInsertRowId: number; + } + & ({ + queryEntries(sql: string, params: any): unknown[]; + } | { + prepare(sql: string): { + all(...params: any): unknown[]; + }; + }); + +interface DenoSqliteDialectConfig extends Omit { + database: DenoSqlite | (() => Promise); +} + +export type { DenoSqlite, DenoSqliteDialectConfig }; diff --git a/db/deno-sqlite-dialect.ts b/db/deno-sqlite-dialect.ts new file mode 100644 index 0000000..85b3679 --- /dev/null +++ b/db/deno-sqlite-dialect.ts @@ -0,0 +1,49 @@ +/// The MIT License (MIT) +/// Copyright (c) 2023 Alex Gleason +/// Copyright (c) 2022 Sami Koskimäki +/// https://gitlab.com/soapbox-pub/kysely-deno-sqlite + +import { + type DatabaseIntrospector, + type Dialect, + type DialectAdapter, + type Driver, + Kysely, + type QueryCompiler, + SqliteAdapter, + SqliteIntrospector, + SqliteQueryCompiler, + } from 'kysely'; + + import { DenoSqliteDriver } from './kysely-sqlite-driver.ts'; + + import type { DenoSqliteDialectConfig } from './deno-sqlite-dialect-config.ts'; + + class DenoSqliteDialect implements Dialect { + readonly #config: DenoSqliteDialectConfig; + + constructor(config: DenoSqliteDialectConfig) { + this.#config = Object.freeze({ ...config }); + } + + createDriver(): Driver { + return new DenoSqliteDriver(this.#config); + } + + createQueryCompiler(): QueryCompiler { + return new SqliteQueryCompiler(); + } + + createAdapter(): DialectAdapter { + return new SqliteAdapter(); + } + + // deno-lint-ignore no-explicit-any + createIntrospector(db: Kysely): DatabaseIntrospector { + return new SqliteIntrospector(db); + } + } + + export { DenoSqliteDialect }; + + \ No newline at end of file diff --git a/db/kysely-sqlite-driver.ts b/db/kysely-sqlite-driver.ts new file mode 100644 index 0000000..e855793 --- /dev/null +++ b/db/kysely-sqlite-driver.ts @@ -0,0 +1,112 @@ +/// The MIT License (MIT) +/// Copyright (c) 2023 Alex Gleason +/// Copyright (c) 2022 Sami Koskimäki +/// https://gitlab.com/soapbox-pub/kysely-deno-sqlite + +import { CompiledQuery, type DatabaseConnection, type Driver, type QueryResult } from 'kysely'; + +import type { DenoSqlite, DenoSqliteDialectConfig } from './deno-sqlite-dialect-config.ts'; + +class DenoSqliteDriver implements Driver { + readonly #config: DenoSqliteDialectConfig; + readonly #connectionMutex = new ConnectionMutex(); + + #db?: DenoSqlite; + #connection?: DatabaseConnection; + + constructor(config: DenoSqliteDialectConfig) { + this.#config = Object.freeze({ ...config }); + } + + async init(): Promise { + this.#db = typeof this.#config.database === 'function' ? await this.#config.database() : this.#config.database; + + this.#connection = new DenoSqliteConnection(this.#db); + + if (this.#config.onCreateConnection) { + await this.#config.onCreateConnection(this.#connection); + } + } + + async acquireConnection(): Promise { + // SQLite only has one single connection. We use a mutex here to wait + // until the single connection has been released. + await this.#connectionMutex.lock(); + return this.#connection!; + } + + async beginTransaction(connection: DatabaseConnection): Promise { + await connection.executeQuery(CompiledQuery.raw('begin')); + } + + async commitTransaction(connection: DatabaseConnection): Promise { + await connection.executeQuery(CompiledQuery.raw('commit')); + } + + async rollbackTransaction(connection: DatabaseConnection): Promise { + await connection.executeQuery(CompiledQuery.raw('rollback')); + } + + // deno-lint-ignore require-await + async releaseConnection(): Promise { + this.#connectionMutex.unlock(); + } + + // deno-lint-ignore require-await + async destroy(): Promise { + this.#db?.close(); + } +} + +class DenoSqliteConnection implements DatabaseConnection { + readonly #db: DenoSqlite; + + constructor(db: DenoSqlite) { + this.#db = db; + } + + executeQuery({ sql, parameters }: CompiledQuery): Promise> { + const rows = 'queryEntries' in this.#db + ? this.#db.queryEntries(sql, parameters) + : this.#db.prepare(sql).all(...parameters); + + const { changes, lastInsertRowId } = this.#db; + + return Promise.resolve({ + rows: rows as O[], + numAffectedRows: BigInt(changes), + insertId: BigInt(lastInsertRowId), + }); + } + + // deno-lint-ignore require-yield + async *streamQuery(): AsyncIterableIterator> { + throw new Error('Sqlite driver doesn\'t support streaming'); + } +} + +class ConnectionMutex { + #promise?: Promise; + #resolve?: () => void; + + async lock(): Promise { + while (this.#promise) { + await this.#promise; + } + + this.#promise = new Promise((resolve) => { + this.#resolve = resolve; + }); + } + + unlock(): void { + const resolve = this.#resolve; + + this.#promise = undefined; + this.#resolve = undefined; + + resolve?.(); + } +} + +export { DenoSqliteDriver }; diff --git a/db/type.ts b/db/type.ts new file mode 100644 index 0000000..9281cec --- /dev/null +++ b/db/type.ts @@ -0,0 +1,64 @@ +import { ColumnType, Generated, Insertable, Selectable, Updateable } from "kysely"; + +/** + * "Code" TEXT, + "Date" TEXT, + "Close" INTEGER NOT NULL, + "Diff" INTEGER NOT NULL, + "Open" INTEGER NOT NULL, + "High" INTEGER NOT NULL, + "Low" INTEGER NOT NULL, + "Volume" INTEGER NOT NULL, + */ + +export interface StockTable { + Code: string; + Date: string; + Close: number; + Diff: number; + Open: number; + High: number; + Low: number; + Volume: number; +} + +export interface KRXCorpTable{ + Name: string; + /** + * PK + */ + Code: string; + + Sector: string; + Product: string; + ListingDay: string; + ClosingMonth: string; + Representative: string; + Homepage: string; + AddressArea: string; + LastUpdate: string; +} + +export interface KOSPITable{ + Name: string; + /** + * PK + */ + Code: string; +} + +export interface KOSDAQTable{ + Name: string; + /** + * PK + */ + Code: string; +} + + +export interface Database { + stock: StockTable; + KRXCorp: KRXCorpTable; + KOSPI: KOSPITable; + KOSDAQ: KOSDAQTable; +} \ No newline at end of file diff --git a/deno.json b/deno.json index e0067de..0fc1b15 100644 --- a/deno.json +++ b/deno.json @@ -20,7 +20,9 @@ "twind": "https://esm.sh/twind@0.16.19", "twind/": "https://esm.sh/twind@0.16.19/", "$std/": "https://deno.land/std@0.203.0/", - "kysely": "npm:kysely@^0.25.0" + "kysely": "npm:kysely@^0.26.3", + "kysely/helpers/sqlite": "npm:kysely@^0.26.3/helpers/sqlite", + "sqlite": "https://deno.land/x/sqlite@v3.8/mod.ts" }, "compilerOptions": { "jsx": "react-jsx", "jsxImportSource": "preact" }, "exclude": ["**/_fresh/*"] diff --git a/fresh.gen.ts b/fresh.gen.ts index 387716d..dd1f55f 100644 --- a/fresh.gen.ts +++ b/fresh.gen.ts @@ -4,14 +4,16 @@ import * as $0 from "./routes/_404.tsx"; import * as $1 from "./routes/_app.tsx"; -import * as $2 from "./routes/api/joke.ts"; -import * as $3 from "./routes/api/kosdaq.ts"; -import * as $4 from "./routes/api/kospi.ts"; -import * as $5 from "./routes/api/pages/[name].ts"; -import * as $6 from "./routes/api/pages/index.ts"; -import * as $7 from "./routes/greet/[name].tsx"; -import * as $8 from "./routes/index.tsx"; -import * as $9 from "./routes/pages/[name].tsx"; +import * as $2 from "./routes/api/corps/[index].ts"; +import * as $3 from "./routes/api/corps/index.ts"; +import * as $4 from "./routes/api/joke.ts"; +import * as $5 from "./routes/api/kosdaq.ts"; +import * as $6 from "./routes/api/kospi.ts"; +import * as $7 from "./routes/api/pages/[name].ts"; +import * as $8 from "./routes/api/pages/index.ts"; +import * as $9 from "./routes/greet/[name].tsx"; +import * as $10 from "./routes/index.tsx"; +import * as $11 from "./routes/pages/[name].tsx"; import * as $$0 from "./islands/Counter.tsx"; import * as $$1 from "./islands/Search.tsx"; import * as $$2 from "./islands/StockList.tsx"; @@ -20,14 +22,16 @@ const manifest = { routes: { "./routes/_404.tsx": $0, "./routes/_app.tsx": $1, - "./routes/api/joke.ts": $2, - "./routes/api/kosdaq.ts": $3, - "./routes/api/kospi.ts": $4, - "./routes/api/pages/[name].ts": $5, - "./routes/api/pages/index.ts": $6, - "./routes/greet/[name].tsx": $7, - "./routes/index.tsx": $8, - "./routes/pages/[name].tsx": $9, + "./routes/api/corps/[index].ts": $2, + "./routes/api/corps/index.ts": $3, + "./routes/api/joke.ts": $4, + "./routes/api/kosdaq.ts": $5, + "./routes/api/kospi.ts": $6, + "./routes/api/pages/[name].ts": $7, + "./routes/api/pages/index.ts": $8, + "./routes/greet/[name].tsx": $9, + "./routes/index.tsx": $10, + "./routes/pages/[name].tsx": $11, }, islands: { "./islands/Counter.tsx": $$0, diff --git a/islands/Search.tsx b/islands/Search.tsx new file mode 100644 index 0000000..46717a6 --- /dev/null +++ b/islands/Search.tsx @@ -0,0 +1,5 @@ +export default function Search(){ + return
+
div
+
+} \ No newline at end of file diff --git a/routes/api/corps/[index].ts b/routes/api/corps/[index].ts new file mode 100644 index 0000000..3f91379 --- /dev/null +++ b/routes/api/corps/[index].ts @@ -0,0 +1,35 @@ +import { Handlers } from "$fresh/server.ts"; +import { db } from "../../../db/db.ts"; +import { jsonArrayFrom } from "kysely/helpers/sqlite"; + +export const handler: Handlers = { + async GET(req, ctx): Promise { + const headers = new Headers({ + "content-type": "application/json" + }); + const index = ctx.params.index; + const corp = await db.selectFrom("KRXCorp") + .selectAll([ + "KRXCorp" + ]) + .select(eb=> [ + jsonArrayFrom(eb.selectFrom("stock") + .select([ + "stock.Close", + "stock.Open", + "stock.Low", + "stock.High", + "stock.Date", + "stock.Volume", + ]) + .where("Code", "=", index) + .orderBy("Date", "desc") + .limit(100) + ).as("prices")] + ) + .where("Code", "=", index) + .executeTakeFirst(); + + return new Response(JSON.stringify(corp ?? null), {headers}); + }, +} \ No newline at end of file diff --git a/routes/api/corps/index.ts b/routes/api/corps/index.ts new file mode 100644 index 0000000..f2c31de --- /dev/null +++ b/routes/api/corps/index.ts @@ -0,0 +1,21 @@ +import { Handlers } from "$fresh/server.ts"; +import { db } from "../../../db/db.ts"; + +export const handler: Handlers = { + async GET(req, _ctx): Promise { + const headers = new Headers({ + "content-type": "application/json" + }); + const url = new URL(req.url); + const q = url.searchParams.get("q"); + const name = url.searchParams.get("name"); + const corps = await db.selectFrom("KRXCorp") + .selectAll([ + "KRXCorp" + ]) + .$if(!!q, qb=> qb.where("Name", "like", "%"+q+"%")) + .$if(!!name, qb => qb.where("Name", "=", name)) + .execute(); + return new Response(JSON.stringify(corps), {headers}); + }, +} \ No newline at end of file diff --git a/routes/api/kosdaq.ts b/routes/api/kosdaq.ts index 3216a13..b9ec4e0 100644 --- a/routes/api/kosdaq.ts +++ b/routes/api/kosdaq.ts @@ -1,17 +1,17 @@ import { Handlers } from "$fresh/server.ts"; -import {DB} from "https://deno.land/x/sqlite/mod.ts"; +import { db } from "../../db/db.ts"; export const handler: Handlers = { async GET(req, _ctx): Promise { const headers = new Headers({ "content-type": "application/json" }); - const db = new DB("stock.db"); - const conn = db.query("SELECT Code,Name FROM KOSDAQ"); - const body = conn.map(row=>({ - code: row[0], - name: row[1] - })) - return new Response(JSON.stringify(body), {headers}); + const rows = await db.selectFrom("KOSDAQ") + .select([ + "Code", + "Name" + ]) + .execute(); + return new Response(JSON.stringify(rows), {headers}); }, } \ No newline at end of file diff --git a/routes/api/kospi.ts b/routes/api/kospi.ts index 6246e2e..f8120f5 100644 --- a/routes/api/kospi.ts +++ b/routes/api/kospi.ts @@ -1,17 +1,17 @@ import { Handlers } from "$fresh/server.ts"; -import {DB} from "https://deno.land/x/sqlite/mod.ts"; +import { db } from "../../db/db.ts"; export const handler: Handlers = { async GET(req, _ctx): Promise { const headers = new Headers({ "content-type": "application/json" }); - const db = new DB("stock.db"); - const conn = db.query("SELECT Code,Name FROM KOSPI"); - const body = conn.map(row=>({ - code: row[0], - name: row[1] - })) - return new Response(JSON.stringify(body), {headers}); + const rows = await db.selectFrom("KOSPI") + .select([ + "Code", + "Name" + ]) + .execute(); + return new Response(JSON.stringify(rows), {headers}); }, } \ No newline at end of file