Skip to content

Commit cd7e059

Browse files
committed
feat: update data source imports and enhance SQLite adapter with new features
1 parent e9010b0 commit cd7e059

95 files changed

Lines changed: 3426 additions & 1468 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 378 additions & 60 deletions
Large diffs are not rendered by default.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { defineConfig } from 'drizzle-kit'
2+
3+
export default defineConfig({
4+
dialect: 'mysql',
5+
schema: './src/meta/schema/mysql.ts',
6+
out: './src/meta/drizzle/mysql',
7+
breakpoints: true,
8+
dbCredentials: {
9+
host: process.env.STARQUERY_META_MYSQL_HOST ?? '127.0.0.1',
10+
port: Number(process.env.STARQUERY_META_MYSQL_PORT ?? '3307'),
11+
user: process.env.STARQUERY_META_MYSQL_USER ?? 'pastefy',
12+
password: process.env.STARQUERY_META_MYSQL_PASSWORD ?? 'pastefy',
13+
database: process.env.STARQUERY_META_MYSQL_DATABASE ?? 'pastefy',
14+
},
15+
})
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import path from 'node:path'
2+
import { defineConfig } from 'drizzle-kit'
3+
4+
export default defineConfig({
5+
dialect: 'sqlite',
6+
schema: './src/meta/schema/sqlite.ts',
7+
out: './src/meta/drizzle/sqlite',
8+
breakpoints: true,
9+
dbCredentials: {
10+
url: process.env.STARQUERY_META_SQLITE_PATH ?? path.resolve(process.cwd(), '.starquery', 'starquery-meta.sqlite'),
11+
},
12+
})

packages/backend/eslint.config.mjs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import js from '@eslint/js'
2+
import tseslint from '@typescript-eslint/eslint-plugin'
3+
import tsParser from '@typescript-eslint/parser'
4+
5+
export default [
6+
{
7+
ignores: ['node_modules/**', 'dist/**', '.vite/**', '.forge/**', 'build/**'],
8+
},
9+
js.configs.recommended,
10+
{
11+
files: ['src/**/*.ts'],
12+
languageOptions: {
13+
parser: tsParser,
14+
parserOptions: {
15+
project: './tsconfig.json',
16+
tsconfigRootDir: import.meta.dirname,
17+
sourceType: 'module',
18+
},
19+
ecmaVersion: 'latest',
20+
sourceType: 'module',
21+
globals: {
22+
console: 'readonly',
23+
process: 'readonly',
24+
Buffer: 'readonly',
25+
URL: 'readonly',
26+
fetch: 'readonly',
27+
WritableStream: 'readonly',
28+
window: 'readonly',
29+
crypto: 'readonly',
30+
setTimeout: 'readonly',
31+
clearTimeout: 'readonly',
32+
},
33+
},
34+
plugins: {
35+
'@typescript-eslint': tseslint,
36+
},
37+
rules: {
38+
'no-undef': 'off',
39+
'no-unused-vars': 'off',
40+
'@typescript-eslint/no-unused-vars': [
41+
'error',
42+
{
43+
argsIgnorePattern: '^_',
44+
varsIgnorePattern: '^_',
45+
caughtErrorsIgnorePattern: '^_',
46+
},
47+
],
48+
'@typescript-eslint/consistent-type-imports': [
49+
'error',
50+
{
51+
prefer: 'type-imports',
52+
fixStyle: 'inline-type-imports',
53+
},
54+
],
55+
},
56+
},
57+
]

packages/backend/package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,11 @@
88
"test": "echo \"Error: no test specified\" && exit 1",
99
"start": "tsx src/index.ts",
1010
"dev": "tsx src/index.ts",
11-
"db:migrate": "tsx src/meta/migrate.ts"
11+
"db:migrate": "tsx src/meta/migrate.ts",
12+
"db:generate:mysql": "drizzle-kit generate --config drizzle.meta.mysql.config.ts",
13+
"db:generate:sqlite": "drizzle-kit generate --config drizzle.meta.sqlite.config.ts",
14+
"typecheck": "tsc -p tsconfig.json",
15+
"lint": "eslint src --ext .ts"
1216
},
1317
"keywords": [],
1418
"author": "",

packages/backend/src/adapters/database/sql/sqlite-adapter/SqliteAdapter.ts

Lines changed: 69 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { DatabaseSync } from 'node:sqlite'
1+
import { DatabaseSync, type SQLInputValue, type SQLOutputValue } from 'node:sqlite'
22
import {
33
DefaultSQLAdapter,
44
type QueryResult,
@@ -29,7 +29,7 @@ export class SqliteAdapter extends DefaultSQLAdapter {
2929

3030
private buildWhereClause(row: Record<string, unknown>, keys: string[]) {
3131
const clauses: string[] = []
32-
const params: unknown[] = []
32+
const params: SQLInputValue[] = []
3333

3434
for (const key of keys) {
3535
assertIdentifier(key)
@@ -39,7 +39,7 @@ export class SqliteAdapter extends DefaultSQLAdapter {
3939
clauses.push(`${this.quoteIdentifier(key)} IS NULL`)
4040
} else {
4141
clauses.push(`${this.quoteIdentifier(key)} = ?`)
42-
params.push(value)
42+
params.push(this.toSqlInputValue(value))
4343
}
4444
}
4545

@@ -59,6 +59,58 @@ export class SqliteAdapter extends DefaultSQLAdapter {
5959
return details.primaryKeys.length ? details.primaryKeys : details.columns.map((column) => column.name)
6060
}
6161

62+
private toSqlInputValue(value: unknown): SQLInputValue {
63+
if (value === null || value === undefined) {
64+
return null
65+
}
66+
67+
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'bigint') {
68+
return value
69+
}
70+
71+
if (ArrayBuffer.isView(value)) {
72+
return value as SQLInputValue
73+
}
74+
75+
if (typeof value === 'boolean') {
76+
return value ? 1 : 0
77+
}
78+
79+
if (value instanceof Date) {
80+
return value.toISOString()
81+
}
82+
83+
if (typeof value === 'object') {
84+
return JSON.stringify(value)
85+
}
86+
87+
return String(value)
88+
}
89+
90+
private toSqlParams(values: unknown[]) {
91+
return values.map((value) => this.toSqlInputValue(value))
92+
}
93+
94+
private normalizeOutputValue(value: SQLOutputValue): unknown {
95+
return value
96+
}
97+
98+
private normalizeRunCount(value: number | bigint | undefined) {
99+
if (typeof value === 'bigint') {
100+
return Number(value)
101+
}
102+
103+
return value ?? 0
104+
}
105+
106+
private normalizeInsertId(value: number | bigint | undefined) {
107+
if (typeof value === 'bigint') {
108+
return value <= BigInt(Number.MAX_SAFE_INTEGER) ? Number(value) : value.toString()
109+
}
110+
111+
return value
112+
}
113+
62114
async connect() {
63115
this.db = new DatabaseSync(this.options.filePath)
64116
}
@@ -159,10 +211,17 @@ export class SqliteAdapter extends DefaultSQLAdapter {
159211
async execute(sqlText: string, params: unknown[] = []): Promise<QueryResult> {
160212
const trimmed = sqlText.trim().toLowerCase()
161213
const statement = this.db.prepare(sqlText)
214+
const sqlParams = this.toSqlParams(params)
162215

163216
if (trimmed.startsWith('select') || trimmed.startsWith('pragma') || trimmed.startsWith('with')) {
164-
const rows = statement.all(...params) as Record<string, unknown>[]
165-
const columns = statement.columns().map((column) => column.name)
217+
const rows = statement
218+
.all(...sqlParams)
219+
.map((row) =>
220+
Object.fromEntries(
221+
Object.entries(row).map(([key, value]) => [key, this.normalizeOutputValue(value)]),
222+
),
223+
) as Record<string, unknown>[]
224+
const columns = rows.length ? Object.keys(rows[0] ?? {}) : []
166225

167226
return {
168227
type: 'SELECT',
@@ -171,12 +230,12 @@ export class SqliteAdapter extends DefaultSQLAdapter {
171230
}
172231
}
173232

174-
const result = statement.run(...params)
233+
const result = statement.run(...sqlParams)
175234
return {
176235
type: 'RESULT',
177236
result: {
178-
affectedRows: result.changes,
179-
insertId: result.lastInsertRowid,
237+
affectedRows: this.normalizeRunCount(result.changes),
238+
insertId: this.normalizeInsertId(result.lastInsertRowid),
180239
},
181240
}
182241
}
@@ -265,7 +324,7 @@ export class SqliteAdapter extends DefaultSQLAdapter {
265324
.prepare(
266325
`INSERT INTO ${this.quoteIdentifier(input.table)} (${keys.map((key) => this.quoteIdentifier(key)).join(', ')}) VALUES (${keys.map(() => '?').join(', ')})`,
267326
)
268-
.run(...keys.map((key) => row[key]))
327+
.run(...this.toSqlParams(keys.map((key) => row[key])))
269328
}
270329

271330
for (const row of updatedRows) {
@@ -277,7 +336,7 @@ export class SqliteAdapter extends DefaultSQLAdapter {
277336
.prepare(
278337
`UPDATE ${this.quoteIdentifier(input.table)} SET ${changeKeys.map((key) => `${this.quoteIdentifier(key)} = ?`).join(', ')} WHERE ${where.sql}`,
279338
)
280-
.run(...changeKeys.map((key) => row.changes[key]), ...where.params)
339+
.run(...this.toSqlParams(changeKeys.map((key) => row.changes[key])), ...where.params)
281340
}
282341

283342
for (const row of deletedRows) {

packages/backend/src/auth/request.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,5 @@ import type { Request } from 'express'
22
import type { AuthPrincipal } from './types.ts'
33

44
export type AuthenticatedRequest = Request & {
5-
auth: AuthPrincipal
5+
auth?: AuthPrincipal
66
}
7-
Lines changed: 1 addition & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -1,97 +1 @@
1-
import type { DataSourceConfig, DataSourceType } from './types.ts'
2-
3-
function requireString(config: Record<string, unknown>, key: string, label = key) {
4-
const value = config[key]
5-
if (typeof value !== 'string' || !value.trim()) {
6-
throw new Error(`Datasource config requires ${label}`)
7-
}
8-
9-
return value.trim()
10-
}
11-
12-
function optionalString(config: Record<string, unknown>, key: string) {
13-
const value = config[key]
14-
if (value === undefined || value === null || value === '') {
15-
return undefined
16-
}
17-
18-
return String(value).trim() || undefined
19-
}
20-
21-
function requirePort(config: Record<string, unknown>, fallback: number, key = 'port') {
22-
const value = config[key]
23-
const nextValue = Number(value ?? fallback)
24-
if (!Number.isFinite(nextValue) || nextValue <= 0) {
25-
throw new Error(`Datasource config requires a valid ${key}`)
26-
}
27-
28-
return nextValue
29-
}
30-
31-
function optionalBoolean(config: Record<string, unknown>, key: string, fallback = false) {
32-
const value = config[key]
33-
if (value === undefined || value === null || value === '') {
34-
return fallback
35-
}
36-
37-
if (typeof value === 'boolean') {
38-
return value
39-
}
40-
41-
if (typeof value === 'string') {
42-
return value.toLowerCase() === 'true'
43-
}
44-
45-
return Boolean(value)
46-
}
47-
48-
export function normalizeDataSourceConfig(type: DataSourceType, config: Record<string, unknown>): DataSourceConfig {
49-
if (type === 'mysql' || type === 'postgres') {
50-
return {
51-
host: requireString(config, 'host'),
52-
port: requirePort(config, type === 'postgres' ? 5432 : 3306),
53-
user: requireString(config, 'user'),
54-
password: requireString(config, 'password'),
55-
database: requireString(config, 'database'),
56-
}
57-
}
58-
59-
if (type === 'sqlite') {
60-
return {
61-
filePath: requireString(config, 'filePath'),
62-
}
63-
}
64-
65-
if (type === 'elasticsearch') {
66-
const apiKey = optionalString(config, 'apiKey')
67-
const password = optionalString(config, 'password')
68-
69-
return {
70-
node: requireString(config, 'node'),
71-
username: optionalString(config, 'username'),
72-
password,
73-
apiKey,
74-
index: optionalString(config, 'index'),
75-
}
76-
}
77-
78-
if (type === 's3' || type === 'minio') {
79-
return {
80-
endPoint: requireString(config, 'endPoint'),
81-
port: requirePort(config, 9000),
82-
useSSL: optionalBoolean(config, 'useSSL', type === 's3'),
83-
accessKey: requireString(config, 'accessKey'),
84-
secretKey: requireString(config, 'secretKey'),
85-
region: optionalString(config, 'region'),
86-
bucket: optionalString(config, 'bucket'),
87-
sessionToken: optionalString(config, 'sessionToken'),
88-
pathStyle: optionalBoolean(config, 'pathStyle', type === 'minio'),
89-
}
90-
}
91-
92-
throw new Error(`Unsupported datasource type: ${type}`)
93-
}
94-
95-
export function validateDataSourceConfig(type: DataSourceType, config: Record<string, unknown>) {
96-
normalizeDataSourceConfig(type, config)
97-
}
1+
export { normalizeDataSourceConfig, validateDataSourceConfig } from './registry.ts'

0 commit comments

Comments
 (0)