Kysely 및 PostgreSQL 도입 (WiP)

This commit is contained in:
static
2025-01-20 10:56:58 +09:00
parent 0002b4e5f2
commit 63eacbb1b3
10 changed files with 399 additions and 2 deletions

View File

@@ -26,6 +26,7 @@
"@types/file-saver": "^2.0.7",
"@types/ms": "^0.7.34",
"@types/node-schedule": "^2.1.7",
"@types/pg": "^8.11.10",
"autoprefixer": "^10.4.20",
"axios": "^1.7.9",
"dexie": "^4.0.10",
@@ -56,8 +57,10 @@
"argon2": "^0.41.1",
"better-sqlite3": "^11.7.2",
"drizzle-orm": "^0.33.0",
"kysely": "^0.27.5",
"ms": "^2.1.3",
"node-schedule": "^2.1.1",
"pg": "^8.13.1",
"uuid": "^11.0.4",
"zod": "^3.24.1"
},

194
pnpm-lock.yaml generated
View File

@@ -19,13 +19,19 @@ importers:
version: 11.7.2
drizzle-orm:
specifier: ^0.33.0
version: 0.33.0(@types/better-sqlite3@7.6.12)(better-sqlite3@11.7.2)
version: 0.33.0(@types/better-sqlite3@7.6.12)(@types/pg@8.11.10)(better-sqlite3@11.7.2)(kysely@0.27.5)(pg@8.13.1)
kysely:
specifier: ^0.27.5
version: 0.27.5
ms:
specifier: ^2.1.3
version: 2.1.3
node-schedule:
specifier: ^2.1.1
version: 2.1.1
pg:
specifier: ^8.13.1
version: 8.13.1
uuid:
specifier: ^11.0.4
version: 11.0.4
@@ -60,6 +66,9 @@ importers:
'@types/node-schedule':
specifier: ^2.1.7
version: 2.1.7
'@types/pg':
specifier: ^8.11.10
version: 8.11.10
autoprefixer:
specifier: ^10.4.20
version: 10.4.20(postcss@8.4.49)
@@ -872,6 +881,9 @@ packages:
'@types/node@22.10.5':
resolution: {integrity: sha512-F8Q+SeGimwOo86fiovQh8qiXfFEh2/ocYv7tU5pJ3EXMSSxk1Joj5wefpFK2fHTf/N6HKGSxIDBT9f3gCxXPkQ==}
'@types/pg@8.11.10':
resolution: {integrity: sha512-LczQUW4dbOQzsH2RQ5qoeJ6qJPdrcM/DcMLoqWQkMLMsq83J5lAX3LXjdkWdpscFy67JSOWDnh7Ny/sPFykmkg==}
'@types/resolve@1.20.2':
resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==}
@@ -1602,6 +1614,10 @@ packages:
kolorist@1.8.0:
resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==}
kysely@0.27.5:
resolution: {integrity: sha512-s7hZHcQeSNKpzCkHRm8yA+0JPLjncSWnjb+2TIElwS2JAqYr+Kv3Ess+9KFfJS0C1xcQ1i9NkNHpWwCYpHMWsA==}
engines: {node: '>=14.0.0'}
levn@0.4.1:
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
engines: {node: '>= 0.8.0'}
@@ -1749,6 +1765,9 @@ packages:
resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==}
engines: {node: '>= 6'}
obuf@1.1.2:
resolution: {integrity: sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==}
once@1.4.0:
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
@@ -1796,6 +1815,48 @@ packages:
pathe@1.1.2:
resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==}
pg-cloudflare@1.1.1:
resolution: {integrity: sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==}
pg-connection-string@2.7.0:
resolution: {integrity: sha512-PI2W9mv53rXJQEOb8xNR8lH7Hr+EKa6oJa38zsK0S/ky2er16ios1wLKhZyxzD7jUReiWokc9WK5nxSnC7W1TA==}
pg-int8@1.0.1:
resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==}
engines: {node: '>=4.0.0'}
pg-numeric@1.0.2:
resolution: {integrity: sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw==}
engines: {node: '>=4'}
pg-pool@3.7.0:
resolution: {integrity: sha512-ZOBQForurqh4zZWjrgSwwAtzJ7QiRX0ovFkZr2klsen3Nm0aoh33Ls0fzfv3imeH/nw/O27cjdz5kzYJfeGp/g==}
peerDependencies:
pg: '>=8.0'
pg-protocol@1.7.0:
resolution: {integrity: sha512-hTK/mE36i8fDDhgDFjy6xNOG+LCorxLG3WO17tku+ij6sVHXh1jQUJ8hYAnRhNla4QVD2H8er/FOjc/+EgC6yQ==}
pg-types@2.2.0:
resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==}
engines: {node: '>=4'}
pg-types@4.0.2:
resolution: {integrity: sha512-cRL3JpS3lKMGsKaWndugWQoLOCoP+Cic8oseVcbr0qhPzYD5DWXK+RZ9LY9wxRf7RQia4SCwQlXk0q6FCPrVng==}
engines: {node: '>=10'}
pg@8.13.1:
resolution: {integrity: sha512-OUir1A0rPNZlX//c7ksiu7crsGZTKSOXJPgtNiHGIlC9H0lO+NC6ZDYksSgBYY/thSWhnSRBv8w1lieNNGATNQ==}
engines: {node: '>= 8.0.0'}
peerDependencies:
pg-native: '>=3.0.1'
peerDependenciesMeta:
pg-native:
optional: true
pgpass@1.0.5:
resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==}
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
@@ -1883,6 +1944,41 @@ packages:
resolution: {integrity: sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==}
engines: {node: ^10 || ^12 || >=14}
postgres-array@2.0.0:
resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==}
engines: {node: '>=4'}
postgres-array@3.0.2:
resolution: {integrity: sha512-6faShkdFugNQCLwucjPcY5ARoW1SlbnrZjmGl0IrrqewpvxvhSLHimCVzqeuULCbG0fQv7Dtk1yDbG3xv7Veog==}
engines: {node: '>=12'}
postgres-bytea@1.0.0:
resolution: {integrity: sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==}
engines: {node: '>=0.10.0'}
postgres-bytea@3.0.0:
resolution: {integrity: sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw==}
engines: {node: '>= 6'}
postgres-date@1.0.7:
resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==}
engines: {node: '>=0.10.0'}
postgres-date@2.1.0:
resolution: {integrity: sha512-K7Juri8gtgXVcDfZttFKVmhglp7epKb1K4pgrkLxehjqkrgPhfG6OO8LHLkfaqkbpjNRnra018XwAr1yQFWGcA==}
engines: {node: '>=12'}
postgres-interval@1.2.0:
resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==}
engines: {node: '>=0.10.0'}
postgres-interval@3.0.0:
resolution: {integrity: sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw==}
engines: {node: '>=12'}
postgres-range@1.1.4:
resolution: {integrity: sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w==}
prebuild-install@7.1.2:
resolution: {integrity: sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ==}
engines: {node: '>=10'}
@@ -2065,6 +2161,10 @@ packages:
resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
engines: {node: '>=0.10.0'}
split2@4.2.0:
resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==}
engines: {node: '>= 10.x'}
string-width@4.2.3:
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
engines: {node: '>=8'}
@@ -2298,6 +2398,10 @@ packages:
wrappy@1.0.2:
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
xtend@4.0.2:
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
engines: {node: '>=0.4'}
yaml@1.10.2:
resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==}
engines: {node: '>= 6'}
@@ -2843,6 +2947,12 @@ snapshots:
dependencies:
undici-types: 6.20.0
'@types/pg@8.11.10':
dependencies:
'@types/node': 22.10.5
pg-protocol: 1.7.0
pg-types: 4.0.2
'@types/resolve@1.20.2': {}
'@typescript-eslint/eslint-plugin@8.19.1(@typescript-eslint/parser@8.19.1(eslint@9.17.0(jiti@1.21.7))(typescript@5.7.3))(eslint@9.17.0(jiti@1.21.7))(typescript@5.7.3)':
@@ -3138,10 +3248,13 @@ snapshots:
transitivePeerDependencies:
- supports-color
drizzle-orm@0.33.0(@types/better-sqlite3@7.6.12)(better-sqlite3@11.7.2):
drizzle-orm@0.33.0(@types/better-sqlite3@7.6.12)(@types/pg@8.11.10)(better-sqlite3@11.7.2)(kysely@0.27.5)(pg@8.13.1):
optionalDependencies:
'@types/better-sqlite3': 7.6.12
'@types/pg': 8.11.10
better-sqlite3: 11.7.2
kysely: 0.27.5
pg: 8.13.1
eastasianwidth@0.2.0: {}
@@ -3554,6 +3667,8 @@ snapshots:
kolorist@1.8.0: {}
kysely@0.27.5: {}
levn@0.4.1:
dependencies:
prelude-ls: 1.2.1
@@ -3668,6 +3783,8 @@ snapshots:
object-hash@3.0.0: {}
obuf@1.1.2: {}
once@1.4.0:
dependencies:
wrappy: 1.0.2
@@ -3714,6 +3831,53 @@ snapshots:
pathe@1.1.2: {}
pg-cloudflare@1.1.1:
optional: true
pg-connection-string@2.7.0: {}
pg-int8@1.0.1: {}
pg-numeric@1.0.2: {}
pg-pool@3.7.0(pg@8.13.1):
dependencies:
pg: 8.13.1
pg-protocol@1.7.0: {}
pg-types@2.2.0:
dependencies:
pg-int8: 1.0.1
postgres-array: 2.0.0
postgres-bytea: 1.0.0
postgres-date: 1.0.7
postgres-interval: 1.2.0
pg-types@4.0.2:
dependencies:
pg-int8: 1.0.1
pg-numeric: 1.0.2
postgres-array: 3.0.2
postgres-bytea: 3.0.0
postgres-date: 2.1.0
postgres-interval: 3.0.0
postgres-range: 1.1.4
pg@8.13.1:
dependencies:
pg-connection-string: 2.7.0
pg-pool: 3.7.0(pg@8.13.1)
pg-protocol: 1.7.0
pg-types: 2.2.0
pgpass: 1.0.5
optionalDependencies:
pg-cloudflare: 1.1.1
pgpass@1.0.5:
dependencies:
split2: 4.2.0
picocolors@1.1.1: {}
picomatch@2.3.1: {}
@@ -3782,6 +3946,28 @@ snapshots:
picocolors: 1.1.1
source-map-js: 1.2.1
postgres-array@2.0.0: {}
postgres-array@3.0.2: {}
postgres-bytea@1.0.0: {}
postgres-bytea@3.0.0:
dependencies:
obuf: 1.1.2
postgres-date@1.0.7: {}
postgres-date@2.1.0: {}
postgres-interval@1.2.0:
dependencies:
xtend: 4.0.2
postgres-interval@3.0.0: {}
postgres-range@1.1.4: {}
prebuild-install@7.1.2:
dependencies:
detect-libc: 2.0.3
@@ -3930,6 +4116,8 @@ snapshots:
source-map@0.6.1: {}
split2@4.2.0: {}
string-width@4.2.3:
dependencies:
emoji-regex: 8.0.0
@@ -4174,6 +4362,8 @@ snapshots:
wrappy@1.0.2: {}
xtend@4.0.2: {}
yaml@1.10.2: {}
yaml@2.7.0: {}

View File

@@ -0,0 +1,15 @@
import { Kysely, PostgresDialect } from "kysely";
import { Pool } from "pg";
import type { Database } from "./schema";
const dialect = new PostgresDialect({
pool: new Pool({
// TODO
}),
});
const db = new Kysely<Database>({ dialect });
// TODO: Migration
export default db;

View File

@@ -6,6 +6,7 @@ import {
foreignKey,
unique,
} from "drizzle-orm/sqlite-core";
import type { ColumnType, Generated } from "kysely";
import { user } from "./user";
export const client = sqliteTable(
@@ -59,3 +60,32 @@ export const userClientChallenge = sqliteTable(
}),
}),
);
interface ClientTable {
id: Generated<number>;
encryption_public_key: string; // Base64
signature_public_key: string; // Base64
}
interface UserClientTable {
user_id: number;
client_id: number;
state: "challenging" | "pending" | "active";
}
interface UserClientChallengeTable {
id: Generated<number>;
user_id: number;
client_id: number;
answer: string; // Base64
allowed_ip: string;
expires_at: ColumnType<Date, Date, never>;
}
declare module "./index" {
interface Database {
client: ClientTable;
user_client: UserClientTable;
user_client_challenge: UserClientChallengeTable;
}
}

View File

@@ -1,4 +1,5 @@
import { sqliteTable, text, integer, foreignKey } from "drizzle-orm/sqlite-core";
import type { ColumnType, Generated, JSONColumnType } from "kysely";
import { hsk } from "./hsk";
import { mek } from "./mek";
import { user } from "./user";
@@ -86,3 +87,61 @@ export const fileLog = sqliteTable("file_log", {
action: text("action", { enum: ["create", "rename"] }).notNull(),
newName: ciphertext("new_name"),
});
type Ciphertext = JSONColumnType<{
ciphertext: string; // Base64
iv: string; // Base64
}>;
interface DirectoryTable {
id: Generated<number>;
parent_id: number | null;
user_id: number;
master_encryption_key_version: number;
encrypted_data_encryption_key: string; // Base64
data_encryption_key_version: Date;
encrypted_name: Ciphertext;
}
interface DirectoryLogTable {
id: Generated<number>;
directory_id: number;
timestamp: ColumnType<Date, Date, never>;
action: "create" | "rename";
new_name: Ciphertext | null;
}
interface FileTable {
id: Generated<number>;
parent_id: number | null;
user_id: number;
path: string;
master_encryption_key_version: number;
encrypted_data_encryption_key: string; // Base64
data_encryption_key_version: Date;
hmac_secret_key_version: number | null;
content_hmac: string | null; // Base64
content_type: string;
encrypted_content_iv: string; // Base64
encrypted_content_hash: string; // Base64
encrypted_name: Ciphertext;
encrypted_created_at: Ciphertext | null;
encrypted_last_modified_at: Ciphertext;
}
interface FileLogTable {
id: Generated<number>;
file_id: number;
timestamp: ColumnType<Date, Date, never>;
action: "create" | "rename";
new_name: Ciphertext | null;
}
declare module "./index" {
interface Database {
directory: DirectoryTable;
directory_log: DirectoryLogTable;
file: FileTable;
file_log: FileLogTable;
}
}

View File

@@ -1,4 +1,5 @@
import { sqliteTable, text, integer, primaryKey, foreignKey } from "drizzle-orm/sqlite-core";
import type { ColumnType, Generated } from "kysely";
import { client } from "./client";
import { mek } from "./mek";
import { user } from "./user";
@@ -42,3 +43,27 @@ export const hskLog = sqliteTable(
}),
}),
);
interface HskTable {
user_id: number;
version: number;
state: "active";
master_encryption_key_version: number;
encrypted_key: string; // Base64
}
interface HskLogTable {
id: Generated<number>;
user_id: number;
hmac_secret_key_version: number;
timestamp: ColumnType<Date, Date, never>;
action: "create";
action_by: number | null;
}
declare module "./index" {
interface Database {
hmac_secret_key: HskTable;
hmac_secret_key_log: HskLogTable;
}
}

View File

@@ -4,3 +4,5 @@ export * from "./hsk";
export * from "./mek";
export * from "./session";
export * from "./user";
export interface Database {}

View File

@@ -1,4 +1,5 @@
import { sqliteTable, text, integer, primaryKey, foreignKey } from "drizzle-orm/sqlite-core";
import type { ColumnType, Generated } from "kysely";
import { client } from "./client";
import { user } from "./user";
@@ -58,3 +59,34 @@ export const clientMek = sqliteTable(
}),
}),
);
interface MekTable {
user_id: number;
version: number;
state: "active" | "retired" | "dead";
}
interface MekLogTable {
id: Generated<number>;
user_id: number;
master_encryption_key_version: number;
timestamp: ColumnType<Date, Date, never>;
action: "create";
action_by: number | null;
}
interface ClientMekTable {
user_id: number;
client_id: number;
version: number;
encrypted_key: string; // Base64
encrypted_key_signature: string; // Base64
}
declare module "./index" {
interface Database {
master_encryption_key: MekTable;
master_encryption_key_log: MekLogTable;
client_master_encryption_key: ClientMekTable;
}
}

View File

@@ -1,4 +1,5 @@
import { sqliteTable, text, integer, unique } from "drizzle-orm/sqlite-core";
import type { ColumnType, Generated } from "kysely";
import { client } from "./client";
import { user } from "./user";
@@ -33,3 +34,29 @@ export const sessionUpgradeChallenge = sqliteTable("session_upgrade_challenge",
allowedIp: text("allowed_ip").notNull(),
expiresAt: integer("expires_at", { mode: "timestamp_ms" }).notNull(),
});
interface SessionTable {
id: string;
user_id: number;
client_id: number | null;
created_at: ColumnType<Date, Date, never>;
last_used_at: Date;
last_used_by_ip: string | null;
last_used_by_agent: string | null;
}
interface SessionUpgradeChallengeTable {
id: Generated<number>;
session_id: string;
client_id: number;
answer: string; // Base64
allowed_ip: string;
expires_at: ColumnType<Date, Date, never>;
}
declare module "./index" {
interface Database {
session: SessionTable;
session_upgrade_challenge: SessionUpgradeChallengeTable;
}
}

View File

@@ -1,4 +1,5 @@
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
import type { Generated } from "kysely";
export const user = sqliteTable("user", {
id: integer("id").primaryKey({ autoIncrement: true }),
@@ -6,3 +7,16 @@ export const user = sqliteTable("user", {
password: text("password").notNull(),
nickname: text("nickname").notNull(),
});
interface UserTable {
id: Generated<number>;
email: string;
nickname: string;
password: string;
}
declare module "./index" {
interface Database {
user: UserTable;
}
}