feat: add api

This commit is contained in:
monoid 2023-10-26 23:58:33 +09:00
parent ad7ab6db86
commit 10324d5799
12 changed files with 363 additions and 33 deletions

11
db/db.ts Normal file
View File

@ -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<Database>({
dialect: new DenoSqliteDialect({
database: new Sqlite("stock.db")
}),
plugins: [new ParseJSONResultsPlugin()]
});

View File

@ -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<SqliteDialectConfig, 'database'> {
database: DenoSqlite | (() => Promise<DenoSqlite>);
}
export type { DenoSqlite, DenoSqliteDialectConfig };

49
db/deno-sqlite-dialect.ts Normal file
View File

@ -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<any>): DatabaseIntrospector {
return new SqliteIntrospector(db);
}
}
export { DenoSqliteDialect };

112
db/kysely-sqlite-driver.ts Normal file
View File

@ -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<void> {
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<DatabaseConnection> {
// 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<void> {
await connection.executeQuery(CompiledQuery.raw('begin'));
}
async commitTransaction(connection: DatabaseConnection): Promise<void> {
await connection.executeQuery(CompiledQuery.raw('commit'));
}
async rollbackTransaction(connection: DatabaseConnection): Promise<void> {
await connection.executeQuery(CompiledQuery.raw('rollback'));
}
// deno-lint-ignore require-await
async releaseConnection(): Promise<void> {
this.#connectionMutex.unlock();
}
// deno-lint-ignore require-await
async destroy(): Promise<void> {
this.#db?.close();
}
}
class DenoSqliteConnection implements DatabaseConnection {
readonly #db: DenoSqlite;
constructor(db: DenoSqlite) {
this.#db = db;
}
executeQuery<O>({ sql, parameters }: CompiledQuery): Promise<QueryResult<O>> {
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<R>(): AsyncIterableIterator<QueryResult<R>> {
throw new Error('Sqlite driver doesn\'t support streaming');
}
}
class ConnectionMutex {
#promise?: Promise<void>;
#resolve?: () => void;
async lock(): Promise<void> {
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 };

64
db/type.ts Normal file
View File

@ -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;
}

View File

@ -20,7 +20,9 @@
"twind": "https://esm.sh/twind@0.16.19", "twind": "https://esm.sh/twind@0.16.19",
"twind/": "https://esm.sh/twind@0.16.19/", "twind/": "https://esm.sh/twind@0.16.19/",
"$std/": "https://deno.land/std@0.203.0/", "$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" }, "compilerOptions": { "jsx": "react-jsx", "jsxImportSource": "preact" },
"exclude": ["**/_fresh/*"] "exclude": ["**/_fresh/*"]

View File

@ -4,14 +4,16 @@
import * as $0 from "./routes/_404.tsx"; import * as $0 from "./routes/_404.tsx";
import * as $1 from "./routes/_app.tsx"; import * as $1 from "./routes/_app.tsx";
import * as $2 from "./routes/api/joke.ts"; import * as $2 from "./routes/api/corps/[index].ts";
import * as $3 from "./routes/api/kosdaq.ts"; import * as $3 from "./routes/api/corps/index.ts";
import * as $4 from "./routes/api/kospi.ts"; import * as $4 from "./routes/api/joke.ts";
import * as $5 from "./routes/api/pages/[name].ts"; import * as $5 from "./routes/api/kosdaq.ts";
import * as $6 from "./routes/api/pages/index.ts"; import * as $6 from "./routes/api/kospi.ts";
import * as $7 from "./routes/greet/[name].tsx"; import * as $7 from "./routes/api/pages/[name].ts";
import * as $8 from "./routes/index.tsx"; import * as $8 from "./routes/api/pages/index.ts";
import * as $9 from "./routes/pages/[name].tsx"; 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 $$0 from "./islands/Counter.tsx";
import * as $$1 from "./islands/Search.tsx"; import * as $$1 from "./islands/Search.tsx";
import * as $$2 from "./islands/StockList.tsx"; import * as $$2 from "./islands/StockList.tsx";
@ -20,14 +22,16 @@ const manifest = {
routes: { routes: {
"./routes/_404.tsx": $0, "./routes/_404.tsx": $0,
"./routes/_app.tsx": $1, "./routes/_app.tsx": $1,
"./routes/api/joke.ts": $2, "./routes/api/corps/[index].ts": $2,
"./routes/api/kosdaq.ts": $3, "./routes/api/corps/index.ts": $3,
"./routes/api/kospi.ts": $4, "./routes/api/joke.ts": $4,
"./routes/api/pages/[name].ts": $5, "./routes/api/kosdaq.ts": $5,
"./routes/api/pages/index.ts": $6, "./routes/api/kospi.ts": $6,
"./routes/greet/[name].tsx": $7, "./routes/api/pages/[name].ts": $7,
"./routes/index.tsx": $8, "./routes/api/pages/index.ts": $8,
"./routes/pages/[name].tsx": $9, "./routes/greet/[name].tsx": $9,
"./routes/index.tsx": $10,
"./routes/pages/[name].tsx": $11,
}, },
islands: { islands: {
"./islands/Counter.tsx": $$0, "./islands/Counter.tsx": $$0,

5
islands/Search.tsx Normal file
View File

@ -0,0 +1,5 @@
export default function Search(){
return <div>
<div>div</div>
</div>
}

View File

@ -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<Response> {
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});
},
}

21
routes/api/corps/index.ts Normal file
View File

@ -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<Response> {
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});
},
}

View File

@ -1,17 +1,17 @@
import { Handlers } from "$fresh/server.ts"; 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 = { export const handler: Handlers = {
async GET(req, _ctx): Promise<Response> { async GET(req, _ctx): Promise<Response> {
const headers = new Headers({ const headers = new Headers({
"content-type": "application/json" "content-type": "application/json"
}); });
const db = new DB("stock.db"); const rows = await db.selectFrom("KOSDAQ")
const conn = db.query("SELECT Code,Name FROM KOSDAQ"); .select([
const body = conn.map(row=>({ "Code",
code: row[0], "Name"
name: row[1] ])
})) .execute();
return new Response(JSON.stringify(body), {headers}); return new Response(JSON.stringify(rows), {headers});
}, },
} }

View File

@ -1,17 +1,17 @@
import { Handlers } from "$fresh/server.ts"; 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 = { export const handler: Handlers = {
async GET(req, _ctx): Promise<Response> { async GET(req, _ctx): Promise<Response> {
const headers = new Headers({ const headers = new Headers({
"content-type": "application/json" "content-type": "application/json"
}); });
const db = new DB("stock.db"); const rows = await db.selectFrom("KOSPI")
const conn = db.query("SELECT Code,Name FROM KOSPI"); .select([
const body = conn.map(row=>({ "Code",
code: row[0], "Name"
name: row[1] ])
})) .execute();
return new Response(JSON.stringify(body), {headers}); return new Response(JSON.stringify(rows), {headers});
}, },
} }