feat(db): add SQLite foundation for FCM registration storage
Introduce better-sqlite3 and a schema-init module so we can migrate off JSON storage without changing runtime behavior yet.
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
# ---- build stage: install everything, type-check + compile to JS ----
|
||||
FROM node:22-alpine AS build
|
||||
|
||||
RUN apk add --no-cache python3 make g++
|
||||
|
||||
RUN corepack enable && corepack prepare pnpm@11.4.0 --activate
|
||||
|
||||
WORKDIR /app
|
||||
@@ -15,6 +17,8 @@ RUN pnpm build
|
||||
# ---- runtime stage: prod deps + compiled JS only, no tsx/esbuild ----
|
||||
FROM node:22-alpine AS runtime
|
||||
|
||||
RUN apk add --no-cache python3 make g++
|
||||
|
||||
RUN corepack enable && corepack prepare pnpm@11.4.0 --activate
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
@@ -12,18 +12,20 @@
|
||||
"dependencies": {
|
||||
"@peculiar/asn1-ecc": "^2.7.0",
|
||||
"@peculiar/asn1-schema": "^2.7.0",
|
||||
"better-sqlite3": "^13.0.1",
|
||||
"cbor-x": "^1.6.4",
|
||||
"cors": "^2.8.6",
|
||||
"dotenv": "^16.6.1",
|
||||
"did-jwt": "^7.4.7",
|
||||
"did-resolver": "^4.1.0",
|
||||
"dotenv": "^16.6.1",
|
||||
"express": "^5.2.1",
|
||||
"firebase-admin": "^13.10.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/better-sqlite3": "^7.6.13",
|
||||
"@types/cors": "^2.8.19",
|
||||
"@types/express": "^5.0.6",
|
||||
"@types/node": "^22.19.19",
|
||||
"@types/cors": "^2.8.19",
|
||||
"tsx": "^4.22.3",
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
|
||||
27
pnpm-lock.yaml
generated
27
pnpm-lock.yaml
generated
@@ -14,6 +14,9 @@ importers:
|
||||
'@peculiar/asn1-schema':
|
||||
specifier: ^2.7.0
|
||||
version: 2.7.0
|
||||
better-sqlite3:
|
||||
specifier: ^13.0.1
|
||||
version: 13.0.1
|
||||
cbor-x:
|
||||
specifier: ^1.6.4
|
||||
version: 1.6.4
|
||||
@@ -36,6 +39,9 @@ importers:
|
||||
specifier: ^13.10.0
|
||||
version: 13.10.0
|
||||
devDependencies:
|
||||
'@types/better-sqlite3':
|
||||
specifier: ^7.6.13
|
||||
version: 7.6.13
|
||||
'@types/cors':
|
||||
specifier: ^2.8.19
|
||||
version: 2.8.19
|
||||
@@ -382,6 +388,9 @@ packages:
|
||||
resolution: {integrity: sha512-HqmEUIGRJ5fSXchkVgR5F7qn48bDBzv0kWj/Kfu5e6uci4UlEeng4331LnBkWffb++Ei3FOVLxo8JJWMFBDMeQ==}
|
||||
engines: {node: '>= 10'}
|
||||
|
||||
'@types/better-sqlite3@7.6.13':
|
||||
resolution: {integrity: sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==}
|
||||
|
||||
'@types/body-parser@1.19.6':
|
||||
resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==}
|
||||
|
||||
@@ -474,6 +483,10 @@ packages:
|
||||
base64-js@1.5.1:
|
||||
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
|
||||
|
||||
better-sqlite3@13.0.1:
|
||||
resolution: {integrity: sha512-LYpmOXdkpQYf4wmlxkdzW01XGlOXNIbjLg45yNkh0FQ4814VbK9PdOFmhZpYbej+EZtR/i3FDdhEG98HqZdgnA==}
|
||||
engines: {node: '>=22'}
|
||||
|
||||
bignumber.js@9.3.1:
|
||||
resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==}
|
||||
|
||||
@@ -924,6 +937,10 @@ packages:
|
||||
resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
node-addon-api@8.9.0:
|
||||
resolution: {integrity: sha512-ekZMeaaIzSQTSpr7X2X3iJM7lTzgnx8ahAG9pJfT/7+14mlEM8ZYQ9cgCDvSSRbReFK0oHli3WrZdCiRsgAT9Q==}
|
||||
engines: {node: ^18 || ^20 || >= 21}
|
||||
|
||||
node-domexception@1.0.0:
|
||||
resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
|
||||
engines: {node: '>=10.5.0'}
|
||||
@@ -1492,6 +1509,10 @@ snapshots:
|
||||
'@tootallnate/once@2.0.1':
|
||||
optional: true
|
||||
|
||||
'@types/better-sqlite3@7.6.13':
|
||||
dependencies:
|
||||
'@types/node': 22.19.19
|
||||
|
||||
'@types/body-parser@1.19.6':
|
||||
dependencies:
|
||||
'@types/connect': 3.4.38
|
||||
@@ -1607,6 +1628,10 @@ snapshots:
|
||||
|
||||
base64-js@1.5.1: {}
|
||||
|
||||
better-sqlite3@13.0.1:
|
||||
dependencies:
|
||||
node-addon-api: 8.9.0
|
||||
|
||||
bignumber.js@9.3.1: {}
|
||||
|
||||
body-parser@2.2.2:
|
||||
@@ -2210,6 +2235,8 @@ snapshots:
|
||||
|
||||
negotiator@1.0.0: {}
|
||||
|
||||
node-addon-api@8.9.0: {}
|
||||
|
||||
node-domexception@1.0.0: {}
|
||||
|
||||
node-fetch@2.7.0:
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
strictDepBuilds: false
|
||||
onlyBuiltDependencies:
|
||||
- "@firebase/util"
|
||||
- better-sqlite3
|
||||
- cbor-extract
|
||||
- esbuild
|
||||
- protobufjs
|
||||
allowBuilds:
|
||||
'@firebase/util': set this to true or false
|
||||
better-sqlite3: set this to true or false
|
||||
cbor-extract: set this to true or false
|
||||
esbuild: set this to true or false
|
||||
protobufjs: set this to true or false
|
||||
|
||||
61
src/db/sqlite.ts
Normal file
61
src/db/sqlite.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { mkdirSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import Database from "better-sqlite3";
|
||||
|
||||
const dataDir =
|
||||
process.env.FCM_TOKEN_DATA_DIR ?? path.join(process.cwd(), "data");
|
||||
const dbFile = path.join(dataDir, "fcm-tokens.sqlite");
|
||||
|
||||
const SCHEMA_SQL = `
|
||||
CREATE TABLE IF NOT EXISTS fcm_registrations (
|
||||
id TEXT PRIMARY KEY NOT NULL,
|
||||
user_id TEXT NOT NULL,
|
||||
device_id TEXT NOT NULL,
|
||||
fcm_token TEXT NOT NULL,
|
||||
platform TEXT NOT NULL,
|
||||
test_mode INTEGER,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
last_notified_at INTEGER
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_fcm_registrations_user_device
|
||||
ON fcm_registrations (user_id, device_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_fcm_registrations_user_id
|
||||
ON fcm_registrations (user_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_fcm_registrations_device_id
|
||||
ON fcm_registrations (device_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_fcm_registrations_fcm_token
|
||||
ON fcm_registrations (fcm_token);
|
||||
`;
|
||||
|
||||
let database: Database.Database | null = null;
|
||||
|
||||
function ensureSchema(connection: Database.Database): void {
|
||||
connection.exec(SCHEMA_SQL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a singleton SQLite connection with schema initialized.
|
||||
* Foundation for the SQLite-backed notification storage.
|
||||
*/
|
||||
export function getDatabase(): Database.Database {
|
||||
if (database === null) {
|
||||
mkdirSync(dataDir, { recursive: true });
|
||||
database = new Database(dbFile);
|
||||
database.pragma("journal_mode = WAL");
|
||||
ensureSchema(database);
|
||||
}
|
||||
return database;
|
||||
}
|
||||
|
||||
/** Closes the singleton connection. Intended for tests and graceful shutdown. */
|
||||
export function closeDatabase(): void {
|
||||
if (database !== null) {
|
||||
database.close();
|
||||
database = null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user