@pineliner/odb-client 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +316 -0
- package/dist/core/client.d.ts +110 -0
- package/dist/core/client.d.ts.map +1 -0
- package/dist/core/http-client.d.ts +37 -0
- package/dist/core/http-client.d.ts.map +1 -0
- package/dist/core/sql-parser.d.ts +70 -0
- package/dist/core/sql-parser.d.ts.map +1 -0
- package/dist/core/transaction.d.ts +60 -0
- package/dist/core/transaction.d.ts.map +1 -0
- package/dist/index.cjs +1052 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +992 -0
- package/dist/service/service-client.d.ts +151 -0
- package/dist/service/service-client.d.ts.map +1 -0
- package/dist/types.d.ts +75 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +44 -0
- package/src/core/client.ts +277 -0
- package/src/core/http-client.ts +200 -0
- package/src/core/sql-parser.ts +330 -0
- package/src/core/transaction.ts +425 -0
- package/src/index.ts +51 -0
- package/src/service/service-client.ts +316 -0
- package/src/types.ts +127 -0
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import type { ODBLiteConfig, ODBLiteResponse, QueryResult } from '../types.ts'
|
|
2
|
+
import { ConnectionError, QueryError } from '../types.ts'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* HTTP client for communicating with ODBLite service
|
|
6
|
+
*/
|
|
7
|
+
export class HTTPClient {
|
|
8
|
+
private config: ODBLiteConfig
|
|
9
|
+
|
|
10
|
+
constructor(config: ODBLiteConfig) {
|
|
11
|
+
this.config = {
|
|
12
|
+
timeout: 30000,
|
|
13
|
+
retries: 3,
|
|
14
|
+
...config
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Ensure baseUrl doesn't end with slash
|
|
18
|
+
if (typeof this.config.baseUrl === 'string') {
|
|
19
|
+
this.config.baseUrl = this.config.baseUrl.replace(/\/$/, '')
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Execute a query against the ODBLite service
|
|
25
|
+
*/
|
|
26
|
+
async query<T = any>(sql: string, params: any[] = []): Promise<QueryResult<T>> {
|
|
27
|
+
if (!this.config.databaseId) {
|
|
28
|
+
throw new ConnectionError('No database ID configured. Use setDatabase() first.')
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const url = `${this.config.baseUrl}/query/${this.config.databaseId}`
|
|
32
|
+
const body = {
|
|
33
|
+
sql,
|
|
34
|
+
params
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const response = await this.makeRequest(url, {
|
|
39
|
+
method: 'POST',
|
|
40
|
+
headers: {
|
|
41
|
+
'Content-Type': 'application/json',
|
|
42
|
+
Authorization: `Bearer ${this.config.apiKey}`
|
|
43
|
+
},
|
|
44
|
+
body: JSON.stringify(body)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
const data: ODBLiteResponse<T> = await response.json()
|
|
48
|
+
|
|
49
|
+
if (!data.success) {
|
|
50
|
+
throw new QueryError(data.error || 'Query failed', sql, params)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Handle nested data structure: { success: true, data: { rows: [...] } }
|
|
54
|
+
let rows: T[] = []
|
|
55
|
+
if (data.data && typeof data.data === 'object' && 'rows' in data.data) {
|
|
56
|
+
rows = (data.data as any).rows
|
|
57
|
+
} else if (Array.isArray(data.data)) {
|
|
58
|
+
rows = data.data
|
|
59
|
+
} else if (Array.isArray(data.rows)) {
|
|
60
|
+
rows = data.rows
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
rows: rows,
|
|
65
|
+
rowsAffected: data.rowsAffected || (data.data as any)?.rowsAffected || 0,
|
|
66
|
+
executionTime: data.executionTime || (data.data as any)?.executionTime || 0,
|
|
67
|
+
databaseName: data.databaseName || (data.data as any)?.databaseName
|
|
68
|
+
}
|
|
69
|
+
} catch (error) {
|
|
70
|
+
if (error instanceof QueryError || error instanceof ConnectionError) {
|
|
71
|
+
throw error
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
throw new QueryError(error instanceof Error ? error.message : 'Unknown error occurred', sql, params, error instanceof Error ? error : undefined)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Check database health
|
|
80
|
+
*/
|
|
81
|
+
async ping(): Promise<boolean> {
|
|
82
|
+
if (!this.config.databaseId) {
|
|
83
|
+
return false
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
const url = `${this.config.baseUrl}/api/db/${this.config.databaseId}/health`
|
|
88
|
+
const response = await this.makeRequest(url, {
|
|
89
|
+
method: 'GET',
|
|
90
|
+
headers: {
|
|
91
|
+
Authorization: `Bearer ${this.config.apiKey}`
|
|
92
|
+
}
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
const data = await response.json()
|
|
96
|
+
return data.status === 'healthy'
|
|
97
|
+
} catch {
|
|
98
|
+
return false
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Get database information
|
|
104
|
+
*/
|
|
105
|
+
async getDatabaseInfo(): Promise<any> {
|
|
106
|
+
if (!this.config.databaseId) {
|
|
107
|
+
throw new ConnectionError('No database ID configured')
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const url = `${this.config.baseUrl}/query/${this.config.databaseId}`
|
|
111
|
+
const response = await this.makeRequest(url, {
|
|
112
|
+
method: 'GET',
|
|
113
|
+
headers: {
|
|
114
|
+
Authorization: `Bearer ${this.config.apiKey}`
|
|
115
|
+
}
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
const data = await response.json()
|
|
119
|
+
if (!data.success) {
|
|
120
|
+
throw new ConnectionError(data.error || 'Failed to get database info')
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return data
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Set the database ID for subsequent queries
|
|
128
|
+
*/
|
|
129
|
+
setDatabase(databaseId: string): void {
|
|
130
|
+
this.config.databaseId = databaseId
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Make HTTP request with retry logic
|
|
135
|
+
*/
|
|
136
|
+
private async makeRequest(url: string, options: RequestInit): Promise<Response> {
|
|
137
|
+
let lastError: Error | undefined
|
|
138
|
+
|
|
139
|
+
for (let attempt = 1; attempt <= (this.config.retries || 3); attempt++) {
|
|
140
|
+
try {
|
|
141
|
+
const controller = new AbortController()
|
|
142
|
+
const timeoutId = setTimeout(() => controller.abort(), this.config.timeout)
|
|
143
|
+
|
|
144
|
+
const response = await fetch(url, {
|
|
145
|
+
...options,
|
|
146
|
+
signal: controller.signal
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
clearTimeout(timeoutId)
|
|
150
|
+
|
|
151
|
+
if (!response.ok) {
|
|
152
|
+
const errorText = await response.text()
|
|
153
|
+
let errorData
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
errorData = JSON.parse(errorText)
|
|
157
|
+
} catch {
|
|
158
|
+
errorData = { error: errorText }
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
throw new ConnectionError(`HTTP ${response.status}: ${errorData.error || response.statusText}`)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return response
|
|
165
|
+
} catch (error) {
|
|
166
|
+
lastError = error instanceof Error ? error : new Error('Unknown error')
|
|
167
|
+
|
|
168
|
+
// Don't retry on client errors (4xx)
|
|
169
|
+
if (error instanceof ConnectionError && error.message.includes('HTTP 4')) {
|
|
170
|
+
throw error
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Wait before retry (exponential backoff)
|
|
174
|
+
if (attempt < (this.config.retries || 3)) {
|
|
175
|
+
const delay = Math.min(1000 * 2 ** (attempt - 1), 10000)
|
|
176
|
+
await new Promise((resolve) => setTimeout(resolve, delay))
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
throw new ConnectionError(`Failed after ${this.config.retries} attempts: ${lastError?.message}`, lastError)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Create a new HTTPClient with updated config
|
|
186
|
+
*/
|
|
187
|
+
configure(updates: Partial<ODBLiteConfig>): HTTPClient {
|
|
188
|
+
return new HTTPClient({
|
|
189
|
+
...this.config,
|
|
190
|
+
...updates
|
|
191
|
+
})
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Get current configuration
|
|
196
|
+
*/
|
|
197
|
+
getConfig(): Readonly<ODBLiteConfig> {
|
|
198
|
+
return { ...this.config }
|
|
199
|
+
}
|
|
200
|
+
}
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
import type { SQLFragment, PreparedQuery, QueryParameter } from '../types.ts';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SQL template string parser that converts postgres.js-style template strings
|
|
5
|
+
* into LibSQL-compatible parameterized queries
|
|
6
|
+
*/
|
|
7
|
+
export class SQLParser {
|
|
8
|
+
/**
|
|
9
|
+
* Parse input that can be template strings, objects, or arrays
|
|
10
|
+
* Context-aware like postgres.js
|
|
11
|
+
*/
|
|
12
|
+
static parse(sql: TemplateStringsArray | any, values?: any[]): PreparedQuery {
|
|
13
|
+
// Handle template string arrays (have the raw property or look like template strings)
|
|
14
|
+
if (Array.isArray(sql) && (('raw' in sql) || (typeof sql[0] === 'string' && values !== undefined))) {
|
|
15
|
+
return this.parseTemplateString(sql as unknown as TemplateStringsArray, values || []);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Handle direct object/array inputs (context detection)
|
|
19
|
+
return this.parseContextualInput(sql, values);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Parse contextual input (objects, arrays, etc.)
|
|
24
|
+
* Uses heuristics to detect the intended context
|
|
25
|
+
*/
|
|
26
|
+
private static parseContextualInput(input: any, values?: any[], context?: string): PreparedQuery {
|
|
27
|
+
if (Array.isArray(input)) {
|
|
28
|
+
// Check if this is an array of objects (for INSERT VALUES)
|
|
29
|
+
if (input.length > 0 && input[0] && typeof input[0] === 'object' && input[0].constructor === Object) {
|
|
30
|
+
const insertResult = this.insertValues(input);
|
|
31
|
+
return {
|
|
32
|
+
sql: insertResult.text,
|
|
33
|
+
params: insertResult.values
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Array of primitives - assume it's for IN clause
|
|
38
|
+
const placeholders = input.map(() => '?').join(', ');
|
|
39
|
+
return {
|
|
40
|
+
sql: `(${placeholders})`,
|
|
41
|
+
params: input.map(v => this.convertValue(v))
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (input && typeof input === 'object' && input.constructor === Object) {
|
|
46
|
+
// Plain object - try to detect context based on structure and usage patterns
|
|
47
|
+
const entries = Object.entries(input).filter(([, value]) => value !== undefined);
|
|
48
|
+
|
|
49
|
+
if (entries.length === 0) {
|
|
50
|
+
return { sql: '', params: [] };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Analyze the object structure to determine intent
|
|
54
|
+
const hasNullValues = entries.some(([, value]) => value === null);
|
|
55
|
+
const hasArrayValues = entries.some(([, value]) => Array.isArray(value));
|
|
56
|
+
const hasComplexValues = entries.some(([, value]) =>
|
|
57
|
+
value !== null &&
|
|
58
|
+
typeof value === 'object' &&
|
|
59
|
+
!Array.isArray(value)
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
// Strong indicators for WHERE clauses:
|
|
63
|
+
// - null values (for IS NULL checks)
|
|
64
|
+
// - array values (for IN clauses)
|
|
65
|
+
// - complex nested objects
|
|
66
|
+
if (hasNullValues || hasArrayValues || hasComplexValues) {
|
|
67
|
+
const whereResult = this.where(input);
|
|
68
|
+
return {
|
|
69
|
+
sql: whereResult.text,
|
|
70
|
+
params: whereResult.values
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// For simple objects with primitive values, we need to guess context
|
|
75
|
+
// This is inherently ambiguous - could be SET or VALUES
|
|
76
|
+
// We'll default to a flexible format that works for both
|
|
77
|
+
|
|
78
|
+
// If single object, more likely to be SET clause
|
|
79
|
+
if (entries.length <= 3 && !Array.isArray(input)) {
|
|
80
|
+
// Return SET format by default
|
|
81
|
+
return this.buildSetClause(input);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// For larger objects or arrays of objects, likely INSERT VALUES
|
|
85
|
+
const insertResult = this.insertValues(input);
|
|
86
|
+
return {
|
|
87
|
+
sql: insertResult.text,
|
|
88
|
+
params: insertResult.values
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Fallback for other types
|
|
93
|
+
return {
|
|
94
|
+
sql: '?',
|
|
95
|
+
params: [this.convertValue(input)]
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Build a SET clause for UPDATE statements
|
|
101
|
+
*/
|
|
102
|
+
private static buildSetClause(data: Record<string, any>): PreparedQuery {
|
|
103
|
+
const entries = Object.entries(data).filter(([, value]) => value !== undefined);
|
|
104
|
+
|
|
105
|
+
if (entries.length === 0) {
|
|
106
|
+
return { sql: '', params: [] };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const setClauses = entries.map(([key]) => `${this.escapeIdentifier(key)} = ?`).join(', ');
|
|
110
|
+
const values = entries.map(([, value]) => this.convertValue(value));
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
sql: setClauses,
|
|
114
|
+
params: values
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Parse template string SQL into LibSQL format
|
|
120
|
+
*/
|
|
121
|
+
private static parseTemplateString(sql: TemplateStringsArray, values: any[]): PreparedQuery {
|
|
122
|
+
const fragments: string[] = [...sql];
|
|
123
|
+
const params: any[] = [];
|
|
124
|
+
|
|
125
|
+
let query = '';
|
|
126
|
+
let paramIndex = 1;
|
|
127
|
+
|
|
128
|
+
for (let i = 0; i < fragments.length; i++) {
|
|
129
|
+
query += fragments[i];
|
|
130
|
+
|
|
131
|
+
if (i < values.length) {
|
|
132
|
+
const value = values[i];
|
|
133
|
+
|
|
134
|
+
// Handle different value types using context detection
|
|
135
|
+
if (Array.isArray(value)) {
|
|
136
|
+
// Handle IN clauses: sql`SELECT * FROM users WHERE id IN ${[1, 2, 3]}`
|
|
137
|
+
const placeholders = value.map(() => `?`).join(', ');
|
|
138
|
+
query += `(${placeholders})`;
|
|
139
|
+
params.push(...value.map(v => this.convertValue(v)));
|
|
140
|
+
} else if (value && typeof value === 'object' && value.constructor === Object) {
|
|
141
|
+
// Use context detection for objects
|
|
142
|
+
const contextResult = this.parseContextualInput(value);
|
|
143
|
+
query += contextResult.sql;
|
|
144
|
+
params.push(...contextResult.params);
|
|
145
|
+
} else if (typeof value === 'string' && value.startsWith('__RAW__')) {
|
|
146
|
+
// Handle raw SQL: sql`SELECT * FROM ${raw('users')}`
|
|
147
|
+
query += value.slice(7); // Remove __RAW__ prefix
|
|
148
|
+
} else {
|
|
149
|
+
// Regular parameter
|
|
150
|
+
query += '?';
|
|
151
|
+
params.push(this.convertValue(value));
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
sql: query.trim(),
|
|
158
|
+
params
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Create a raw SQL fragment (not parameterized)
|
|
164
|
+
*/
|
|
165
|
+
static raw(text: string): string {
|
|
166
|
+
return `__RAW__${text}`;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Create an identifier (table name, column name, etc.)
|
|
171
|
+
*/
|
|
172
|
+
static identifier(name: string): string {
|
|
173
|
+
return this.escapeIdentifier(name);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Escape SQL identifiers (table names, column names)
|
|
178
|
+
*/
|
|
179
|
+
private static escapeIdentifier(identifier: string): string {
|
|
180
|
+
// SQLite uses double quotes for identifiers
|
|
181
|
+
return `"${identifier.replace(/"/g, '""')}"`;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Convert JavaScript values to LibSQL-compatible values
|
|
186
|
+
*/
|
|
187
|
+
private static convertValue(value: any): any {
|
|
188
|
+
if (value === null || value === undefined) {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (value instanceof Date) {
|
|
193
|
+
// Convert Date to ISO string for SQLite
|
|
194
|
+
return value.toISOString();
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (value instanceof Buffer) {
|
|
198
|
+
// Convert Buffer to Uint8Array for LibSQL
|
|
199
|
+
return new Uint8Array(value);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (typeof value === 'boolean') {
|
|
203
|
+
// SQLite uses 0/1 for booleans
|
|
204
|
+
return value ? 1 : 0;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (typeof value === 'bigint') {
|
|
208
|
+
// Convert BigInt to string to avoid precision loss
|
|
209
|
+
return value.toString();
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (typeof value === 'object') {
|
|
213
|
+
// Serialize objects as JSON
|
|
214
|
+
return JSON.stringify(value);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return value;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Create a SQL fragment for building complex queries
|
|
222
|
+
*/
|
|
223
|
+
static fragment(sql: TemplateStringsArray, ...values: any[]): SQLFragment {
|
|
224
|
+
const parsed = this.parse(sql, values);
|
|
225
|
+
return {
|
|
226
|
+
text: parsed.sql,
|
|
227
|
+
values: parsed.params
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Join multiple SQL fragments
|
|
233
|
+
*/
|
|
234
|
+
static join(fragments: SQLFragment[], separator = ' '): SQLFragment {
|
|
235
|
+
const text = fragments.map(f => f.text).join(separator);
|
|
236
|
+
const values = fragments.flatMap(f => f.values);
|
|
237
|
+
|
|
238
|
+
return { text, values };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Helper for building WHERE clauses from objects
|
|
243
|
+
*/
|
|
244
|
+
static where(conditions: Record<string, any>): SQLFragment {
|
|
245
|
+
const entries = Object.entries(conditions).filter(([, value]) => value !== undefined);
|
|
246
|
+
|
|
247
|
+
if (entries.length === 0) {
|
|
248
|
+
return { text: '', values: [] };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const clauses = entries.map(([key, value]) => {
|
|
252
|
+
if (value === null) {
|
|
253
|
+
return `${this.escapeIdentifier(key)} IS NULL`;
|
|
254
|
+
}
|
|
255
|
+
if (Array.isArray(value)) {
|
|
256
|
+
const placeholders = value.map(() => '?').join(', ');
|
|
257
|
+
return `${this.escapeIdentifier(key)} IN (${placeholders})`;
|
|
258
|
+
}
|
|
259
|
+
return `${this.escapeIdentifier(key)} = ?`;
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
const values = entries.flatMap(([, value]) => {
|
|
263
|
+
if (value === null) return [];
|
|
264
|
+
if (Array.isArray(value)) return value.map(v => this.convertValue(v));
|
|
265
|
+
return [this.convertValue(value)];
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
return {
|
|
269
|
+
text: clauses.join(' AND '),
|
|
270
|
+
values
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Helper for building INSERT VALUES from objects
|
|
276
|
+
*/
|
|
277
|
+
static insertValues(data: Record<string, any> | Record<string, any>[]): SQLFragment {
|
|
278
|
+
const records = Array.isArray(data) ? data : [data];
|
|
279
|
+
|
|
280
|
+
if (records.length === 0) {
|
|
281
|
+
throw new Error('No data provided for insert');
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const keys = Object.keys(records[0]);
|
|
285
|
+
const columns = keys.map(key => this.escapeIdentifier(key)).join(', ');
|
|
286
|
+
|
|
287
|
+
const valueClauses = records.map(record => {
|
|
288
|
+
const placeholders = keys.map(() => '?').join(', ');
|
|
289
|
+
return `(${placeholders})`;
|
|
290
|
+
}).join(', ');
|
|
291
|
+
|
|
292
|
+
const values = records.flatMap(record =>
|
|
293
|
+
keys.map(key => this.convertValue(record[key]))
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
return {
|
|
297
|
+
text: `(${columns}) VALUES ${valueClauses}`,
|
|
298
|
+
values
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Helper for building UPDATE SET clauses from objects
|
|
304
|
+
*/
|
|
305
|
+
static updateSet(data: Record<string, any>): SQLFragment {
|
|
306
|
+
const entries = Object.entries(data).filter(([, value]) => value !== undefined);
|
|
307
|
+
|
|
308
|
+
if (entries.length === 0) {
|
|
309
|
+
throw new Error('No data provided for update');
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const setClauses = entries.map(([key]) => `${this.escapeIdentifier(key)} = ?`).join(', ');
|
|
313
|
+
const values = entries.map(([, value]) => this.convertValue(value));
|
|
314
|
+
|
|
315
|
+
return {
|
|
316
|
+
text: setClauses,
|
|
317
|
+
values
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Export convenience functions
|
|
323
|
+
export const sql = SQLParser.parse.bind(SQLParser);
|
|
324
|
+
export const raw = SQLParser.raw.bind(SQLParser);
|
|
325
|
+
export const identifier = SQLParser.identifier.bind(SQLParser);
|
|
326
|
+
export const fragment = SQLParser.fragment.bind(SQLParser);
|
|
327
|
+
export const join = SQLParser.join.bind(SQLParser);
|
|
328
|
+
export const where = SQLParser.where.bind(SQLParser);
|
|
329
|
+
export const insertValues = SQLParser.insertValues.bind(SQLParser);
|
|
330
|
+
export const updateSet = SQLParser.updateSet.bind(SQLParser);
|