better-auth-studio 1.1.1-beta.7 → 1.1.1-beta.9
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/dist/adapters/astro.d.ts.map +1 -1
- package/dist/adapters/astro.js +4 -0
- package/dist/adapters/astro.js.map +1 -1
- package/dist/adapters/elysia.d.ts.map +1 -1
- package/dist/adapters/elysia.js +4 -0
- package/dist/adapters/elysia.js.map +1 -1
- package/dist/adapters/express.d.ts.map +1 -1
- package/dist/adapters/express.js +4 -0
- package/dist/adapters/express.js.map +1 -1
- package/dist/adapters/hono.d.ts.map +1 -1
- package/dist/adapters/hono.js +0 -1
- package/dist/adapters/hono.js.map +1 -1
- package/dist/adapters/nextjs.d.ts.map +1 -1
- package/dist/adapters/nextjs.js +4 -0
- package/dist/adapters/nextjs.js.map +1 -1
- package/dist/adapters/nuxt.d.ts +6 -6
- package/dist/adapters/nuxt.d.ts.map +1 -1
- package/dist/adapters/nuxt.js +63 -44
- package/dist/adapters/nuxt.js.map +1 -1
- package/dist/adapters/remix.d.ts.map +1 -1
- package/dist/adapters/remix.js +4 -0
- package/dist/adapters/remix.js.map +1 -1
- package/dist/adapters/solid-start.d.ts.map +1 -1
- package/dist/adapters/solid-start.js +4 -0
- package/dist/adapters/solid-start.js.map +1 -1
- package/dist/adapters/svelte-kit.d.ts.map +1 -1
- package/dist/adapters/svelte-kit.js +4 -0
- package/dist/adapters/svelte-kit.js.map +1 -1
- package/dist/adapters/tanstack-start.d.ts.map +1 -1
- package/dist/adapters/tanstack-start.js +4 -0
- package/dist/adapters/tanstack-start.js.map +1 -1
- package/dist/core/handler.d.ts +5 -0
- package/dist/core/handler.d.ts.map +1 -1
- package/dist/core/handler.js +33 -22
- package/dist/core/handler.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/providers/events/helpers.d.ts +5 -0
- package/dist/providers/events/helpers.d.ts.map +1 -1
- package/dist/providers/events/helpers.js +459 -124
- package/dist/providers/events/helpers.js.map +1 -1
- package/dist/types/handler.d.ts +1 -1
- package/dist/types/handler.d.ts.map +1 -1
- package/dist/utils/event-ingestion.d.ts.map +1 -1
- package/dist/utils/event-ingestion.js +28 -28
- package/dist/utils/event-ingestion.js.map +1 -1
- package/dist/utils/hook-injector.d.ts +0 -3
- package/dist/utils/hook-injector.d.ts.map +1 -1
- package/dist/utils/hook-injector.js +65 -4
- package/dist/utils/hook-injector.js.map +1 -1
- package/dist/utils/server-init.d.ts +10 -0
- package/dist/utils/server-init.d.ts.map +1 -0
- package/dist/utils/server-init.js +40 -0
- package/dist/utils/server-init.js.map +1 -0
- package/package.json +1 -1
|
@@ -1,41 +1,75 @@
|
|
|
1
1
|
export function createPostgresProvider(options) {
|
|
2
|
-
const { client, tableName = 'auth_events', schema = 'public' } = options;
|
|
2
|
+
const { client, tableName = 'auth_events', schema = 'public', clientType } = options;
|
|
3
3
|
// Validate client is provided
|
|
4
4
|
if (!client) {
|
|
5
5
|
throw new Error('Postgres client is required. Provide a pg Pool, Client, or Prisma client instance.');
|
|
6
6
|
}
|
|
7
|
-
|
|
8
|
-
if (
|
|
9
|
-
|
|
7
|
+
let actualClient = client;
|
|
8
|
+
if (clientType === 'drizzle') {
|
|
9
|
+
if (client.$client) {
|
|
10
|
+
actualClient = client.$client;
|
|
11
|
+
}
|
|
12
|
+
else if (client.client) {
|
|
13
|
+
// Fallback for older Drizzle versions
|
|
14
|
+
actualClient = client.client;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
const hasQuery = actualClient?.query || client?.query;
|
|
18
|
+
const hasExecuteRaw = client?.$executeRaw;
|
|
19
|
+
const hasExecute = client?.execute;
|
|
20
|
+
const hasUnsafe = actualClient?.unsafe || client?.$client?.unsafe || client?.unsafe;
|
|
21
|
+
if (!hasQuery && !hasExecuteRaw && !hasExecute && !hasUnsafe) {
|
|
22
|
+
const errorMessage = clientType === 'prisma'
|
|
23
|
+
? 'Invalid Prisma client. Client must have a `$executeRaw` method.'
|
|
24
|
+
: clientType === 'drizzle'
|
|
25
|
+
? 'Invalid Drizzle client. Drizzle instance must wrap a postgres-js (with `unsafe` method) or pg (with `query` method) client.'
|
|
26
|
+
: 'Invalid Postgres client. Client must have either a `query` method (pg Pool/Client) or `$executeRaw` method (Prisma client).';
|
|
27
|
+
throw new Error(errorMessage);
|
|
10
28
|
}
|
|
11
29
|
// Ensure table exists
|
|
12
30
|
const ensureTable = async () => {
|
|
13
31
|
if (!client)
|
|
14
32
|
return;
|
|
15
33
|
try {
|
|
16
|
-
// Support
|
|
17
|
-
const queryFn = client.query || (typeof client === 'function' ? client : null);
|
|
18
|
-
if (!queryFn) {
|
|
19
|
-
console.warn(`⚠️ Postgres client doesn't support query method. Table ${schema}.${tableName} must be created manually.`);
|
|
20
|
-
return;
|
|
21
|
-
}
|
|
22
|
-
// Support Prisma client ($executeRaw) or standard pg client (query)
|
|
34
|
+
// Support Prisma client ($executeRaw), Drizzle instance ($client), or standard pg client (query)
|
|
23
35
|
let executeQuery;
|
|
24
|
-
if (client.$executeRaw) {
|
|
36
|
+
if (clientType === 'prisma' || client.$executeRaw) {
|
|
25
37
|
// Prisma client
|
|
26
38
|
executeQuery = async (query) => {
|
|
27
39
|
return await client.$executeRawUnsafe(query);
|
|
28
40
|
};
|
|
29
41
|
}
|
|
42
|
+
else if (clientType === 'drizzle') {
|
|
43
|
+
// Drizzle client - use $client which exposes the underlying client
|
|
44
|
+
if (actualClient?.unsafe) {
|
|
45
|
+
// postgres-js client via db.$client.unsafe()
|
|
46
|
+
executeQuery = async (query) => {
|
|
47
|
+
return await actualClient.unsafe(query);
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
else if (actualClient?.query) {
|
|
51
|
+
// pg Pool/Client via db.$client.query()
|
|
52
|
+
executeQuery = async (query) => {
|
|
53
|
+
return await actualClient.query(query);
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
throw new Error(`Drizzle client.$client doesn't expose 'unsafe' (postgres-js) or 'query' (pg) method. Available methods: ${Object.keys(actualClient || {}).join(', ')}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
else if (actualClient?.query) {
|
|
61
|
+
// Standard pg client (Pool or Client)
|
|
62
|
+
executeQuery = async (query) => {
|
|
63
|
+
return await actualClient.query(query);
|
|
64
|
+
};
|
|
65
|
+
}
|
|
30
66
|
else if (client.query) {
|
|
31
|
-
// Standard pg client
|
|
32
67
|
executeQuery = async (query) => {
|
|
33
68
|
return await client.query(query);
|
|
34
69
|
};
|
|
35
70
|
}
|
|
36
71
|
else {
|
|
37
|
-
|
|
38
|
-
return;
|
|
72
|
+
throw new Error(`Postgres client doesn't support $executeRaw or query method. Available properties: ${Object.keys(client).join(', ')}`);
|
|
39
73
|
}
|
|
40
74
|
// Use CREATE TABLE IF NOT EXISTS (simpler and more reliable)
|
|
41
75
|
const createTableQuery = `
|
|
@@ -68,22 +102,16 @@ export function createPostgresProvider(options) {
|
|
|
68
102
|
try {
|
|
69
103
|
await executeQuery(indexQuery);
|
|
70
104
|
}
|
|
71
|
-
catch (err) {
|
|
72
|
-
// Index might already exist, ignore
|
|
73
|
-
}
|
|
105
|
+
catch (err) { }
|
|
74
106
|
}
|
|
75
|
-
console.log(`✅ Ensured ${schema}.${tableName} table exists for events`);
|
|
76
107
|
}
|
|
77
108
|
catch (error) {
|
|
78
|
-
// If table already exists, that's fine
|
|
79
109
|
if (error?.message?.includes('already exists') || error?.code === '42P07') {
|
|
80
110
|
return;
|
|
81
111
|
}
|
|
82
112
|
console.error(`Failed to ensure ${schema}.${tableName} table:`, error);
|
|
83
|
-
// Don't throw - allow provider to work even if table creation fails
|
|
84
113
|
}
|
|
85
114
|
};
|
|
86
|
-
// Track if table creation is in progress or completed
|
|
87
115
|
let tableEnsured = false;
|
|
88
116
|
let tableEnsuringPromise = null;
|
|
89
117
|
const ensureTableSync = async () => {
|
|
@@ -112,12 +140,8 @@ export function createPostgresProvider(options) {
|
|
|
112
140
|
ensureTableSync().catch(console.error);
|
|
113
141
|
return {
|
|
114
142
|
async ingest(event) {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
}
|
|
118
|
-
// Support Prisma client ($executeRaw) or standard pg client (query/Pool)
|
|
119
|
-
if (client.$executeRaw) {
|
|
120
|
-
// Prisma client - use $executeRawUnsafe for parameterized queries
|
|
143
|
+
await ensureTableSync();
|
|
144
|
+
if (clientType === 'prisma' || client.$executeRaw) {
|
|
121
145
|
const query = `
|
|
122
146
|
INSERT INTO ${schema}.${tableName}
|
|
123
147
|
(id, type, timestamp, status, user_id, session_id, organization_id, metadata, ip_address, user_agent, source, display_message, display_severity)
|
|
@@ -128,52 +152,11 @@ export function createPostgresProvider(options) {
|
|
|
128
152
|
}
|
|
129
153
|
catch (error) {
|
|
130
154
|
console.error(`Failed to insert event (${event.type}) into ${schema}.${tableName}:`, error);
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
}
|
|
134
|
-
else if (client.query) {
|
|
135
|
-
// Standard pg client (Pool or Client) - use parameterized queries for safety
|
|
136
|
-
try {
|
|
137
|
-
await client.query(`INSERT INTO ${schema}.${tableName}
|
|
138
|
-
(id, type, timestamp, status, user_id, session_id, organization_id, metadata, ip_address, user_agent, source, display_message, display_severity)
|
|
139
|
-
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)`, [
|
|
140
|
-
event.id,
|
|
141
|
-
event.type,
|
|
142
|
-
event.timestamp,
|
|
143
|
-
event.status || 'success',
|
|
144
|
-
event.userId || null,
|
|
145
|
-
event.sessionId || null,
|
|
146
|
-
event.organizationId || null,
|
|
147
|
-
JSON.stringify(event.metadata || {}),
|
|
148
|
-
event.ipAddress || null,
|
|
149
|
-
event.userAgent || null,
|
|
150
|
-
event.source,
|
|
151
|
-
event.display?.message || null,
|
|
152
|
-
event.display?.severity || null,
|
|
153
|
-
]);
|
|
154
|
-
}
|
|
155
|
-
catch (error) {
|
|
156
|
-
console.error(`Failed to insert event (${event.type}) into ${schema}.${tableName}:`, error);
|
|
157
|
-
if (error.code === '42P01') {
|
|
155
|
+
if (error.code === '42P01' || error.meta?.code === '42P01' || error.code === 'P2010') {
|
|
156
|
+
tableEnsured = false;
|
|
158
157
|
await ensureTableSync();
|
|
159
158
|
try {
|
|
160
|
-
await client
|
|
161
|
-
(id, type, timestamp, status, user_id, session_id, organization_id, metadata, ip_address, user_agent, source, display_message, display_severity)
|
|
162
|
-
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)`, [
|
|
163
|
-
event.id,
|
|
164
|
-
event.type,
|
|
165
|
-
event.timestamp,
|
|
166
|
-
event.status || 'success',
|
|
167
|
-
event.userId || null,
|
|
168
|
-
event.sessionId || null,
|
|
169
|
-
event.organizationId || null,
|
|
170
|
-
JSON.stringify(event.metadata || {}),
|
|
171
|
-
event.ipAddress || null,
|
|
172
|
-
event.userAgent || null,
|
|
173
|
-
event.source,
|
|
174
|
-
event.display?.message || null,
|
|
175
|
-
event.display?.severity || null,
|
|
176
|
-
]);
|
|
159
|
+
await client.$executeRawUnsafe(query);
|
|
177
160
|
return;
|
|
178
161
|
}
|
|
179
162
|
catch (retryError) {
|
|
@@ -181,26 +164,114 @@ export function createPostgresProvider(options) {
|
|
|
181
164
|
throw retryError;
|
|
182
165
|
}
|
|
183
166
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
167
|
+
throw error;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
else if ((actualClient && (actualClient.query || actualClient.unsafe)) ||
|
|
171
|
+
client.query ||
|
|
172
|
+
client.unsafe) {
|
|
173
|
+
// Standard pg client (Pool/Client with query) or postgres-js client (with unsafe) or Drizzle with underlying client
|
|
174
|
+
const queryClient = actualClient || client;
|
|
175
|
+
const useUnsafe = queryClient.unsafe && !queryClient.query; // Use unsafe if postgres-js (has unsafe but no query)
|
|
176
|
+
if (useUnsafe) {
|
|
177
|
+
// postgres-js client - use unsafe() for raw SQL
|
|
178
|
+
const query = `
|
|
179
|
+
INSERT INTO ${schema}.${tableName}
|
|
180
|
+
(id, type, timestamp, status, user_id, session_id, organization_id, metadata, ip_address, user_agent, source, display_message, display_severity)
|
|
181
|
+
VALUES ('${event.id}'::uuid, '${event.type}', '${event.timestamp.toISOString()}', '${event.status || 'success'}', ${event.userId ? `'${event.userId.replace(/'/g, "''")}'` : 'NULL'}, ${event.sessionId ? `'${event.sessionId.replace(/'/g, "''")}'` : 'NULL'}, ${event.organizationId ? `'${event.organizationId.replace(/'/g, "''")}'` : 'NULL'}, '${JSON.stringify(event.metadata || {}).replace(/'/g, "''")}'::jsonb, ${event.ipAddress ? `'${event.ipAddress.replace(/'/g, "''")}'` : 'NULL'}, ${event.userAgent ? `'${event.userAgent.replace(/'/g, "''")}'` : 'NULL'}, '${event.source}', ${event.display?.message ? `'${event.display.message.replace(/'/g, "''")}'` : 'NULL'}, ${event.display?.severity ? `'${event.display.severity}'` : 'NULL'})
|
|
182
|
+
`;
|
|
183
|
+
try {
|
|
184
|
+
await queryClient.unsafe(query);
|
|
185
|
+
}
|
|
186
|
+
catch (error) {
|
|
187
|
+
console.error(`Failed to insert event (${event.type}) into ${schema}.${tableName}:`, error);
|
|
188
|
+
if (error.code === '42P01') {
|
|
189
|
+
tableEnsured = false;
|
|
190
|
+
await ensureTableSync();
|
|
191
|
+
try {
|
|
192
|
+
await queryClient.unsafe(query);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
catch (retryError) {
|
|
196
|
+
console.error(`Retry after table creation also failed:`, retryError);
|
|
197
|
+
throw retryError;
|
|
198
|
+
}
|
|
189
199
|
}
|
|
200
|
+
throw error;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
// pg Pool/Client - use parameterized queries
|
|
205
|
+
try {
|
|
206
|
+
await queryClient.query(`INSERT INTO ${schema}.${tableName}
|
|
207
|
+
(id, type, timestamp, status, user_id, session_id, organization_id, metadata, ip_address, user_agent, source, display_message, display_severity)
|
|
208
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)`, [
|
|
209
|
+
event.id,
|
|
210
|
+
event.type,
|
|
211
|
+
event.timestamp,
|
|
212
|
+
event.status || 'success',
|
|
213
|
+
event.userId || null,
|
|
214
|
+
event.sessionId || null,
|
|
215
|
+
event.organizationId || null,
|
|
216
|
+
JSON.stringify(event.metadata || {}),
|
|
217
|
+
event.ipAddress || null,
|
|
218
|
+
event.userAgent || null,
|
|
219
|
+
event.source,
|
|
220
|
+
event.display?.message || null,
|
|
221
|
+
event.display?.severity || null,
|
|
222
|
+
]);
|
|
223
|
+
}
|
|
224
|
+
catch (error) {
|
|
225
|
+
console.error(`Failed to insert event (${event.type}) into ${schema}.${tableName}:`, error);
|
|
226
|
+
if (error.code === '42P01') {
|
|
227
|
+
tableEnsured = false;
|
|
228
|
+
await ensureTableSync();
|
|
229
|
+
try {
|
|
230
|
+
await queryClient.query(`INSERT INTO ${schema}.${tableName}
|
|
231
|
+
(id, type, timestamp, status, user_id, session_id, organization_id, metadata, ip_address, user_agent, source, display_message, display_severity)
|
|
232
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)`, [
|
|
233
|
+
event.id,
|
|
234
|
+
event.type,
|
|
235
|
+
event.timestamp,
|
|
236
|
+
event.status || 'success',
|
|
237
|
+
event.userId || null,
|
|
238
|
+
event.sessionId || null,
|
|
239
|
+
event.organizationId || null,
|
|
240
|
+
JSON.stringify(event.metadata || {}),
|
|
241
|
+
event.ipAddress || null,
|
|
242
|
+
event.userAgent || null,
|
|
243
|
+
event.source,
|
|
244
|
+
event.display?.message || null,
|
|
245
|
+
event.display?.severity || null,
|
|
246
|
+
]);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
catch (retryError) {
|
|
250
|
+
console.error(`Retry after table creation also failed:`, retryError);
|
|
251
|
+
throw retryError;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
if (error.code === 'ECONNREFUSED' ||
|
|
255
|
+
error.code === 'ETIMEDOUT' ||
|
|
256
|
+
error.message?.includes('Connection terminated')) {
|
|
257
|
+
if (client.end) {
|
|
258
|
+
console.warn(`⚠️ Connection error with pg Pool. The pool will retry automatically on next query.`);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
throw error;
|
|
190
262
|
}
|
|
191
|
-
throw error;
|
|
192
263
|
}
|
|
193
264
|
}
|
|
194
265
|
else {
|
|
195
|
-
throw new Error('Postgres client does not support $executeRaw or
|
|
266
|
+
throw new Error('Postgres client does not support $executeRaw, query, or unsafe method. Make sure you are passing a valid pg Pool, Client, Prisma client, or Drizzle instance.');
|
|
196
267
|
}
|
|
197
268
|
},
|
|
198
269
|
async ingestBatch(events) {
|
|
199
270
|
if (events.length === 0)
|
|
200
271
|
return;
|
|
201
272
|
await ensureTableSync();
|
|
202
|
-
// Support Prisma client ($executeRaw) or standard pg client (query)
|
|
203
|
-
if (client.$executeRaw) {
|
|
273
|
+
// Support Prisma client ($executeRaw), Drizzle instance, or standard pg client (query)
|
|
274
|
+
if (clientType === 'prisma' || client.$executeRaw) {
|
|
204
275
|
// Prisma client - use $executeRawUnsafe for batch inserts
|
|
205
276
|
const CHUNK_SIZE = 500; // Reasonable chunk size for string-based queries
|
|
206
277
|
for (let i = 0; i < events.length; i += CHUNK_SIZE) {
|
|
@@ -222,52 +293,81 @@ export function createPostgresProvider(options) {
|
|
|
222
293
|
}
|
|
223
294
|
}
|
|
224
295
|
}
|
|
225
|
-
else if (
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
JSON.stringify(event.metadata || {}),
|
|
252
|
-
event.ipAddress || null,
|
|
253
|
-
event.userAgent || null,
|
|
254
|
-
event.source,
|
|
255
|
-
event.display?.message || null,
|
|
256
|
-
event.display?.severity || null,
|
|
257
|
-
]);
|
|
258
|
-
try {
|
|
259
|
-
await client.query(query, params);
|
|
296
|
+
else if ((actualClient && (actualClient.query || actualClient.unsafe)) ||
|
|
297
|
+
client.query ||
|
|
298
|
+
client.unsafe) {
|
|
299
|
+
// Standard pg client (Pool/Client with query) or postgres-js client (with unsafe) or Drizzle with underlying client
|
|
300
|
+
const batchQueryClient = actualClient || client;
|
|
301
|
+
const useUnsafe = batchQueryClient.unsafe && !batchQueryClient.query; // Use unsafe if postgres-js
|
|
302
|
+
if (useUnsafe) {
|
|
303
|
+
// postgres-js client - use unsafe() for raw SQL (similar to Prisma)
|
|
304
|
+
const CHUNK_SIZE = 500; // Reasonable chunk size for string-based queries
|
|
305
|
+
for (let i = 0; i < events.length; i += CHUNK_SIZE) {
|
|
306
|
+
const chunk = events.slice(i, i + CHUNK_SIZE);
|
|
307
|
+
const values = chunk
|
|
308
|
+
.map((event) => `('${event.id}', '${event.type}', '${event.timestamp.toISOString()}', '${event.status || 'success'}', ${event.userId ? `'${event.userId.replace(/'/g, "''")}'` : 'NULL'}, ${event.sessionId ? `'${event.sessionId.replace(/'/g, "''")}'` : 'NULL'}, ${event.organizationId ? `'${event.organizationId.replace(/'/g, "''")}'` : 'NULL'}, '${JSON.stringify(event.metadata || {}).replace(/'/g, "''")}'::jsonb, ${event.ipAddress ? `'${event.ipAddress.replace(/'/g, "''")}'` : 'NULL'}, ${event.userAgent ? `'${event.userAgent.replace(/'/g, "''")}'` : 'NULL'}, '${event.source}', ${event.display?.message ? `'${event.display.message.replace(/'/g, "''")}'` : 'NULL'}, ${event.display?.severity ? `'${event.display.severity}'` : 'NULL'})`)
|
|
309
|
+
.join(', ');
|
|
310
|
+
const query = `
|
|
311
|
+
INSERT INTO ${schema}.${tableName}
|
|
312
|
+
(id, type, timestamp, status, user_id, session_id, organization_id, metadata, ip_address, user_agent, source, display_message, display_severity)
|
|
313
|
+
VALUES ${values}
|
|
314
|
+
`;
|
|
315
|
+
try {
|
|
316
|
+
await batchQueryClient.unsafe(query);
|
|
317
|
+
}
|
|
318
|
+
catch (error) {
|
|
319
|
+
console.error(`Failed to insert batch chunk (${chunk.length} events):`, error);
|
|
320
|
+
throw error;
|
|
321
|
+
}
|
|
260
322
|
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
// pg Pool/Client - use parameterized queries
|
|
326
|
+
const PARAMS_PER_EVENT = 13;
|
|
327
|
+
const MAX_PARAMS = 65535;
|
|
328
|
+
const CHUNK_SIZE = Math.floor(MAX_PARAMS / PARAMS_PER_EVENT) - 1; // ~5000, but use 1000 for safety
|
|
329
|
+
for (let chunkStart = 0; chunkStart < events.length; chunkStart += CHUNK_SIZE) {
|
|
330
|
+
const chunk = events.slice(chunkStart, chunkStart + CHUNK_SIZE);
|
|
331
|
+
const values = chunk
|
|
332
|
+
.map((_, i) => {
|
|
333
|
+
const base = i * PARAMS_PER_EVENT;
|
|
334
|
+
return `($${base + 1}, $${base + 2}, $${base + 3}, $${base + 4}, $${base + 5}, $${base + 6}, $${base + 7}, $${base + 8}, $${base + 9}, $${base + 10}, $${base + 11}, $${base + 12}, $${base + 13})`;
|
|
335
|
+
})
|
|
336
|
+
.join(', ');
|
|
337
|
+
const query = `
|
|
338
|
+
INSERT INTO ${schema}.${tableName}
|
|
339
|
+
(id, type, timestamp, status, user_id, session_id, organization_id, metadata, ip_address, user_agent, source, display_message, display_severity)
|
|
340
|
+
VALUES ${values}
|
|
341
|
+
`;
|
|
342
|
+
const params = chunk.flatMap((event) => [
|
|
343
|
+
event.id,
|
|
344
|
+
event.type,
|
|
345
|
+
event.timestamp,
|
|
346
|
+
event.status || 'success',
|
|
347
|
+
event.userId || null,
|
|
348
|
+
event.sessionId || null,
|
|
349
|
+
event.organizationId || null,
|
|
350
|
+
JSON.stringify(event.metadata || {}),
|
|
351
|
+
event.ipAddress || null,
|
|
352
|
+
event.userAgent || null,
|
|
353
|
+
event.source,
|
|
354
|
+
event.display?.message || null,
|
|
355
|
+
event.display?.severity || null,
|
|
356
|
+
]);
|
|
357
|
+
try {
|
|
358
|
+
await batchQueryClient.query(query, params);
|
|
359
|
+
}
|
|
360
|
+
catch (error) {
|
|
361
|
+
console.error(`Failed to insert batch chunk (${chunk.length} events) into ${schema}.${tableName}:`, error);
|
|
362
|
+
if (error.code === 'ECONNREFUSED' ||
|
|
363
|
+
error.code === 'ETIMEDOUT' ||
|
|
364
|
+
error.message?.includes('Connection terminated')) {
|
|
365
|
+
if (client.end) {
|
|
366
|
+
console.warn(`⚠️ Connection error with pg Pool. The pool will retry automatically on next query.`);
|
|
367
|
+
}
|
|
268
368
|
}
|
|
369
|
+
throw error;
|
|
269
370
|
}
|
|
270
|
-
throw error;
|
|
271
371
|
}
|
|
272
372
|
}
|
|
273
373
|
}
|
|
@@ -418,6 +518,241 @@ export function createPostgresProvider(options) {
|
|
|
418
518
|
},
|
|
419
519
|
};
|
|
420
520
|
}
|
|
521
|
+
export function createSqliteProvider(options) {
|
|
522
|
+
const { client, tableName = 'auth_events' } = options;
|
|
523
|
+
// Validate client is provided
|
|
524
|
+
if (!client) {
|
|
525
|
+
throw new Error('SQLite client is required. Provide a better-sqlite3 Database instance.');
|
|
526
|
+
}
|
|
527
|
+
// Handle case where client might be a function (lazy initialization) or already initialized
|
|
528
|
+
const actualClient = typeof client === 'function' ? client() : client;
|
|
529
|
+
// If client initialization failed (native module not found), try to provide helpful error
|
|
530
|
+
if (!actualClient) {
|
|
531
|
+
throw new Error('SQLite client initialization failed. Make sure better-sqlite3 is properly installed and built. ' +
|
|
532
|
+
'Run: pnpm rebuild better-sqlite3 or npm rebuild better-sqlite3');
|
|
533
|
+
}
|
|
534
|
+
// Validate client has required methods (better-sqlite3 Database)
|
|
535
|
+
const hasExec = typeof actualClient.exec === 'function';
|
|
536
|
+
const hasPrepare = typeof actualClient.prepare === 'function';
|
|
537
|
+
if (!hasExec || !hasPrepare) {
|
|
538
|
+
// If methods don't exist, it might be an initialization error - provide helpful message
|
|
539
|
+
if (actualClient instanceof Error) {
|
|
540
|
+
throw new Error(`SQLite client initialization error: ${actualClient.message}. ` +
|
|
541
|
+
'Make sure better-sqlite3 native module is built. Run: pnpm rebuild better-sqlite3');
|
|
542
|
+
}
|
|
543
|
+
throw new Error('Invalid SQLite client. Client must have `exec` and `prepare` methods (better-sqlite3 Database instance). ' +
|
|
544
|
+
'If using better-sqlite3, make sure the native module is built: pnpm rebuild better-sqlite3');
|
|
545
|
+
}
|
|
546
|
+
// Ensure table exists
|
|
547
|
+
const ensureTable = async () => {
|
|
548
|
+
if (!client)
|
|
549
|
+
return;
|
|
550
|
+
try {
|
|
551
|
+
// Use CREATE TABLE IF NOT EXISTS (SQLite doesn't support schemas like Postgres)
|
|
552
|
+
const createTableQuery = `
|
|
553
|
+
CREATE TABLE IF NOT EXISTS ${tableName} (
|
|
554
|
+
id TEXT PRIMARY KEY,
|
|
555
|
+
type TEXT NOT NULL,
|
|
556
|
+
timestamp TEXT NOT NULL DEFAULT (datetime('now')),
|
|
557
|
+
status TEXT NOT NULL DEFAULT 'success',
|
|
558
|
+
user_id TEXT,
|
|
559
|
+
session_id TEXT,
|
|
560
|
+
organization_id TEXT,
|
|
561
|
+
metadata TEXT DEFAULT '{}',
|
|
562
|
+
ip_address TEXT,
|
|
563
|
+
user_agent TEXT,
|
|
564
|
+
source TEXT DEFAULT 'app',
|
|
565
|
+
display_message TEXT,
|
|
566
|
+
display_severity TEXT,
|
|
567
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
568
|
+
);
|
|
569
|
+
`;
|
|
570
|
+
actualClient.exec(createTableQuery);
|
|
571
|
+
const indexQueries = [
|
|
572
|
+
`CREATE INDEX IF NOT EXISTS idx_${tableName}_user_id ON ${tableName}(user_id)`,
|
|
573
|
+
`CREATE INDEX IF NOT EXISTS idx_${tableName}_type ON ${tableName}(type)`,
|
|
574
|
+
`CREATE INDEX IF NOT EXISTS idx_${tableName}_timestamp ON ${tableName}(timestamp DESC)`,
|
|
575
|
+
`CREATE INDEX IF NOT EXISTS idx_${tableName}_id_timestamp ON ${tableName}(id, timestamp DESC)`,
|
|
576
|
+
];
|
|
577
|
+
for (const indexQuery of indexQueries) {
|
|
578
|
+
try {
|
|
579
|
+
actualClient.exec(indexQuery);
|
|
580
|
+
}
|
|
581
|
+
catch (err) {
|
|
582
|
+
// Index might already exist, ignore
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
catch (error) {
|
|
587
|
+
if (error?.message?.includes('already exists')) {
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
console.error(`Failed to ensure ${tableName} table:`, error);
|
|
591
|
+
}
|
|
592
|
+
};
|
|
593
|
+
let tableEnsured = false;
|
|
594
|
+
let tableEnsuringPromise = null;
|
|
595
|
+
const ensureTableSync = async () => {
|
|
596
|
+
if (tableEnsured) {
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
if (tableEnsuringPromise) {
|
|
600
|
+
return tableEnsuringPromise;
|
|
601
|
+
}
|
|
602
|
+
tableEnsuringPromise = (async () => {
|
|
603
|
+
try {
|
|
604
|
+
await ensureTable();
|
|
605
|
+
tableEnsured = true;
|
|
606
|
+
}
|
|
607
|
+
catch (error) {
|
|
608
|
+
tableEnsuringPromise = null;
|
|
609
|
+
throw error;
|
|
610
|
+
}
|
|
611
|
+
finally {
|
|
612
|
+
tableEnsuringPromise = null;
|
|
613
|
+
}
|
|
614
|
+
})();
|
|
615
|
+
return tableEnsuringPromise;
|
|
616
|
+
};
|
|
617
|
+
ensureTableSync().catch(console.error);
|
|
618
|
+
return {
|
|
619
|
+
async ingest(event) {
|
|
620
|
+
await ensureTableSync();
|
|
621
|
+
try {
|
|
622
|
+
const stmt = actualClient.prepare(`
|
|
623
|
+
INSERT INTO ${tableName}
|
|
624
|
+
(id, type, timestamp, status, user_id, session_id, organization_id, metadata, ip_address, user_agent, source, display_message, display_severity)
|
|
625
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
626
|
+
`);
|
|
627
|
+
stmt.run(event.id, event.type, event.timestamp.toISOString(), event.status || 'success', event.userId || null, event.sessionId || null, event.organizationId || null, JSON.stringify(event.metadata || {}), event.ipAddress || null, event.userAgent || null, event.source, event.display?.message || null, event.display?.severity || null);
|
|
628
|
+
}
|
|
629
|
+
catch (error) {
|
|
630
|
+
console.error(`Failed to insert event (${event.type}) into ${tableName}:`, error);
|
|
631
|
+
// If table doesn't exist, try to create it and retry
|
|
632
|
+
if (error.message?.includes('no such table')) {
|
|
633
|
+
tableEnsured = false;
|
|
634
|
+
await ensureTableSync();
|
|
635
|
+
try {
|
|
636
|
+
const stmt = actualClient.prepare(`
|
|
637
|
+
INSERT INTO ${tableName}
|
|
638
|
+
(id, type, timestamp, status, user_id, session_id, organization_id, metadata, ip_address, user_agent, source, display_message, display_severity)
|
|
639
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
640
|
+
`);
|
|
641
|
+
stmt.run(event.id, event.type, event.timestamp.toISOString(), event.status || 'success', event.userId || null, event.sessionId || null, event.organizationId || null, JSON.stringify(event.metadata || {}), event.ipAddress || null, event.userAgent || null, event.source, event.display?.message || null, event.display?.severity || null);
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
catch (retryError) {
|
|
645
|
+
console.error(`Retry after table creation also failed:`, retryError);
|
|
646
|
+
throw retryError;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
throw error;
|
|
650
|
+
}
|
|
651
|
+
},
|
|
652
|
+
async ingestBatch(events) {
|
|
653
|
+
if (events.length === 0)
|
|
654
|
+
return;
|
|
655
|
+
await ensureTableSync();
|
|
656
|
+
try {
|
|
657
|
+
const transaction = actualClient.transaction((events) => {
|
|
658
|
+
const stmt = actualClient.prepare(`
|
|
659
|
+
INSERT INTO ${tableName}
|
|
660
|
+
(id, type, timestamp, status, user_id, session_id, organization_id, metadata, ip_address, user_agent, source, display_message, display_severity)
|
|
661
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
662
|
+
`);
|
|
663
|
+
for (const event of events) {
|
|
664
|
+
stmt.run(event.id, event.type, event.timestamp.toISOString(), event.status || 'success', event.userId || null, event.sessionId || null, event.organizationId || null, JSON.stringify(event.metadata || {}), event.ipAddress || null, event.userAgent || null, event.source, event.display?.message || null, event.display?.severity || null);
|
|
665
|
+
}
|
|
666
|
+
});
|
|
667
|
+
transaction(events);
|
|
668
|
+
}
|
|
669
|
+
catch (error) {
|
|
670
|
+
console.error(`Failed to insert batch (${events.length} events):`, error);
|
|
671
|
+
if (error.message?.includes('no such table')) {
|
|
672
|
+
tableEnsured = false;
|
|
673
|
+
await ensureTableSync();
|
|
674
|
+
try {
|
|
675
|
+
const transaction = actualClient.transaction((events) => {
|
|
676
|
+
const stmt = actualClient.prepare(`
|
|
677
|
+
INSERT INTO ${tableName}
|
|
678
|
+
(id, type, timestamp, status, user_id, session_id, organization_id, metadata, ip_address, user_agent, source, display_message, display_severity)
|
|
679
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
680
|
+
`);
|
|
681
|
+
for (const event of events) {
|
|
682
|
+
stmt.run(event.id, event.type, event.timestamp.toISOString(), event.status || 'success', event.userId || null, event.sessionId || null, event.organizationId || null, JSON.stringify(event.metadata || {}), event.ipAddress || null, event.userAgent || null, event.source, event.display?.message || null, event.display?.severity || null);
|
|
683
|
+
}
|
|
684
|
+
});
|
|
685
|
+
transaction(events);
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
catch (retryError) {
|
|
689
|
+
console.error(`Retry after table creation also failed:`, retryError);
|
|
690
|
+
throw retryError;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
throw error;
|
|
694
|
+
}
|
|
695
|
+
},
|
|
696
|
+
async query(options) {
|
|
697
|
+
const { limit = 20, after, sort = 'desc', type, userId } = options;
|
|
698
|
+
await ensureTableSync();
|
|
699
|
+
try {
|
|
700
|
+
let query = `SELECT * FROM ${tableName} WHERE 1=1`;
|
|
701
|
+
const params = [];
|
|
702
|
+
if (type) {
|
|
703
|
+
query += ` AND type = ?`;
|
|
704
|
+
params.push(type);
|
|
705
|
+
}
|
|
706
|
+
if (userId) {
|
|
707
|
+
query += ` AND user_id = ?`;
|
|
708
|
+
params.push(userId);
|
|
709
|
+
}
|
|
710
|
+
if (after) {
|
|
711
|
+
query += ` AND id > ?`;
|
|
712
|
+
params.push(after);
|
|
713
|
+
}
|
|
714
|
+
query += ` ORDER BY timestamp ${sort === 'desc' ? 'DESC' : 'ASC'}`;
|
|
715
|
+
query += ` LIMIT ?`;
|
|
716
|
+
params.push(limit + 1); // Fetch one extra to check if there are more
|
|
717
|
+
const stmt = actualClient.prepare(query);
|
|
718
|
+
const rows = stmt.all(...params);
|
|
719
|
+
const hasMore = rows.length > limit;
|
|
720
|
+
const events = (hasMore ? rows.slice(0, limit) : rows).map((row) => ({
|
|
721
|
+
id: row.id,
|
|
722
|
+
type: row.type,
|
|
723
|
+
timestamp: new Date(row.timestamp),
|
|
724
|
+
status: row.status,
|
|
725
|
+
userId: row.user_id || undefined,
|
|
726
|
+
sessionId: row.session_id || undefined,
|
|
727
|
+
organizationId: row.organization_id || undefined,
|
|
728
|
+
metadata: typeof row.metadata === 'string' ? JSON.parse(row.metadata) : row.metadata || {},
|
|
729
|
+
ipAddress: row.ip_address || undefined,
|
|
730
|
+
userAgent: row.user_agent || undefined,
|
|
731
|
+
source: row.source || 'app',
|
|
732
|
+
display: row.display_message || row.display_severity
|
|
733
|
+
? {
|
|
734
|
+
message: row.display_message || undefined,
|
|
735
|
+
severity: row.display_severity || undefined,
|
|
736
|
+
}
|
|
737
|
+
: undefined,
|
|
738
|
+
}));
|
|
739
|
+
return {
|
|
740
|
+
events,
|
|
741
|
+
hasMore,
|
|
742
|
+
nextCursor: hasMore && events.length > 0 ? events[events.length - 1].id : null,
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
catch (error) {
|
|
746
|
+
if (error.message?.includes('no such table')) {
|
|
747
|
+
await ensureTableSync();
|
|
748
|
+
return { events: [], hasMore: false, nextCursor: null };
|
|
749
|
+
}
|
|
750
|
+
console.error(`Failed to query events from ${tableName}:`, error);
|
|
751
|
+
throw error;
|
|
752
|
+
}
|
|
753
|
+
},
|
|
754
|
+
};
|
|
755
|
+
}
|
|
421
756
|
export function createClickHouseProvider(options) {
|
|
422
757
|
const { client, table = 'auth_events', database } = options;
|
|
423
758
|
const ensureTable = async () => {
|