better-auth-studio 1.1.1-beta.6 → 1.1.1-beta.8
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 +21 -21
- 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 +1 -0
- package/dist/providers/events/helpers.d.ts.map +1 -1
- package/dist/providers/events/helpers.js +295 -110
- 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 +49 -13
- 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 +59 -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,33 +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
|
+
// Validate client is provided
|
|
4
|
+
if (!client) {
|
|
5
|
+
throw new Error('Postgres client is required. Provide a pg Pool, Client, or Prisma client instance.');
|
|
6
|
+
}
|
|
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);
|
|
28
|
+
}
|
|
3
29
|
// Ensure table exists
|
|
4
30
|
const ensureTable = async () => {
|
|
5
31
|
if (!client)
|
|
6
32
|
return;
|
|
7
33
|
try {
|
|
8
|
-
// Support
|
|
9
|
-
const queryFn = client.query || (typeof client === 'function' ? client : null);
|
|
10
|
-
if (!queryFn) {
|
|
11
|
-
console.warn(`⚠️ Postgres client doesn't support query method. Table ${schema}.${tableName} must be created manually.`);
|
|
12
|
-
return;
|
|
13
|
-
}
|
|
14
|
-
// Support Prisma client ($executeRaw) or standard pg client (query)
|
|
34
|
+
// Support Prisma client ($executeRaw), Drizzle instance ($client), or standard pg client (query)
|
|
15
35
|
let executeQuery;
|
|
16
|
-
if (client.$executeRaw) {
|
|
36
|
+
if (clientType === 'prisma' || client.$executeRaw) {
|
|
17
37
|
// Prisma client
|
|
18
38
|
executeQuery = async (query) => {
|
|
19
39
|
return await client.$executeRawUnsafe(query);
|
|
20
40
|
};
|
|
21
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
|
+
}
|
|
22
66
|
else if (client.query) {
|
|
23
|
-
// Standard pg client
|
|
24
67
|
executeQuery = async (query) => {
|
|
25
68
|
return await client.query(query);
|
|
26
69
|
};
|
|
27
70
|
}
|
|
28
71
|
else {
|
|
29
|
-
|
|
30
|
-
return;
|
|
72
|
+
throw new Error(`Postgres client doesn't support $executeRaw or query method. Available properties: ${Object.keys(client).join(', ')}`);
|
|
31
73
|
}
|
|
32
74
|
// Use CREATE TABLE IF NOT EXISTS (simpler and more reliable)
|
|
33
75
|
const createTableQuery = `
|
|
@@ -60,123 +102,277 @@ export function createPostgresProvider(options) {
|
|
|
60
102
|
try {
|
|
61
103
|
await executeQuery(indexQuery);
|
|
62
104
|
}
|
|
63
|
-
catch (err) {
|
|
64
|
-
// Index might already exist, ignore
|
|
65
|
-
}
|
|
105
|
+
catch (err) { }
|
|
66
106
|
}
|
|
67
|
-
console.log(`✅ Ensured ${schema}.${tableName} table exists for events`);
|
|
68
107
|
}
|
|
69
108
|
catch (error) {
|
|
70
|
-
// If table already exists, that's fine
|
|
71
109
|
if (error?.message?.includes('already exists') || error?.code === '42P07') {
|
|
72
110
|
return;
|
|
73
111
|
}
|
|
74
112
|
console.error(`Failed to ensure ${schema}.${tableName} table:`, error);
|
|
75
|
-
// Don't throw - allow provider to work even if table creation fails
|
|
76
113
|
}
|
|
77
114
|
};
|
|
78
|
-
// Track if table creation is in progress or completed
|
|
79
115
|
let tableEnsured = false;
|
|
80
|
-
let
|
|
116
|
+
let tableEnsuringPromise = null;
|
|
81
117
|
const ensureTableSync = async () => {
|
|
82
|
-
if (tableEnsured
|
|
118
|
+
if (tableEnsured) {
|
|
83
119
|
return;
|
|
84
|
-
tableEnsuring = true;
|
|
85
|
-
try {
|
|
86
|
-
await ensureTable();
|
|
87
|
-
tableEnsured = true;
|
|
88
|
-
}
|
|
89
|
-
catch (error) {
|
|
90
|
-
console.error('Failed to ensure table:', error);
|
|
91
120
|
}
|
|
92
|
-
|
|
93
|
-
|
|
121
|
+
if (tableEnsuringPromise) {
|
|
122
|
+
return tableEnsuringPromise;
|
|
94
123
|
}
|
|
124
|
+
tableEnsuringPromise = (async () => {
|
|
125
|
+
try {
|
|
126
|
+
await ensureTable();
|
|
127
|
+
tableEnsured = true;
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
console.error('Failed to ensure table:', error);
|
|
131
|
+
tableEnsuringPromise = null;
|
|
132
|
+
throw error;
|
|
133
|
+
}
|
|
134
|
+
finally {
|
|
135
|
+
tableEnsuringPromise = null;
|
|
136
|
+
}
|
|
137
|
+
})();
|
|
138
|
+
return tableEnsuringPromise;
|
|
95
139
|
};
|
|
96
|
-
// Call ensureTable asynchronously (don't block initialization)
|
|
97
140
|
ensureTableSync().catch(console.error);
|
|
98
141
|
return {
|
|
99
142
|
async ingest(event) {
|
|
100
|
-
|
|
101
|
-
if (
|
|
102
|
-
await ensureTableSync();
|
|
103
|
-
}
|
|
104
|
-
// Support Prisma client ($executeRaw) or standard pg client (query/Pool)
|
|
105
|
-
if (client.$executeRaw) {
|
|
106
|
-
// Prisma client - use $executeRawUnsafe for parameterized queries
|
|
143
|
+
await ensureTableSync();
|
|
144
|
+
if (clientType === 'prisma' || client.$executeRaw) {
|
|
107
145
|
const query = `
|
|
108
146
|
INSERT INTO ${schema}.${tableName}
|
|
109
147
|
(id, type, timestamp, status, user_id, session_id, organization_id, metadata, ip_address, user_agent, source, display_message, display_severity)
|
|
110
148
|
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'})
|
|
111
149
|
`;
|
|
112
|
-
|
|
150
|
+
try {
|
|
151
|
+
await client.$executeRawUnsafe(query);
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
console.error(`Failed to insert event (${event.type}) into ${schema}.${tableName}:`, error);
|
|
155
|
+
if (error.code === '42P01' || error.meta?.code === '42P01' || error.code === 'P2010') {
|
|
156
|
+
tableEnsured = false;
|
|
157
|
+
await ensureTableSync();
|
|
158
|
+
try {
|
|
159
|
+
await client.$executeRawUnsafe(query);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
catch (retryError) {
|
|
163
|
+
console.error(`Retry after table creation also failed:`, retryError);
|
|
164
|
+
throw retryError;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
throw error;
|
|
168
|
+
}
|
|
113
169
|
}
|
|
114
|
-
else if (
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
+
}
|
|
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;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
else {
|
|
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.');
|
|
133
267
|
}
|
|
134
268
|
},
|
|
135
269
|
async ingestBatch(events) {
|
|
136
270
|
if (events.length === 0)
|
|
137
271
|
return;
|
|
138
|
-
|
|
139
|
-
|
|
272
|
+
await ensureTableSync();
|
|
273
|
+
// Support Prisma client ($executeRaw), Drizzle instance, or standard pg client (query)
|
|
274
|
+
if (clientType === 'prisma' || client.$executeRaw) {
|
|
140
275
|
// Prisma client - use $executeRawUnsafe for batch inserts
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
.
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
276
|
+
const CHUNK_SIZE = 500; // Reasonable chunk size for string-based queries
|
|
277
|
+
for (let i = 0; i < events.length; i += CHUNK_SIZE) {
|
|
278
|
+
const chunk = events.slice(i, i + CHUNK_SIZE);
|
|
279
|
+
const values = chunk
|
|
280
|
+
.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'})`)
|
|
281
|
+
.join(', ');
|
|
282
|
+
const query = `
|
|
283
|
+
INSERT INTO ${schema}.${tableName}
|
|
284
|
+
(id, type, timestamp, status, user_id, session_id, organization_id, metadata, ip_address, user_agent, source, display_message, display_severity)
|
|
285
|
+
VALUES ${values}
|
|
286
|
+
`;
|
|
287
|
+
try {
|
|
288
|
+
await client.$executeRawUnsafe(query);
|
|
289
|
+
}
|
|
290
|
+
catch (error) {
|
|
291
|
+
console.error(`Failed to insert batch chunk (${chunk.length} events):`, error);
|
|
292
|
+
throw error;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
150
295
|
}
|
|
151
|
-
else if (
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
+
}
|
|
322
|
+
}
|
|
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
|
+
}
|
|
368
|
+
}
|
|
369
|
+
throw error;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
else {
|
|
375
|
+
throw new Error('Postgres client does not support $executeRaw or query method. Make sure you are passing a valid pg Pool, Client, or Prisma client.');
|
|
180
376
|
}
|
|
181
377
|
},
|
|
182
378
|
async query(options) {
|
|
@@ -310,7 +506,6 @@ export function createPostgresProvider(options) {
|
|
|
310
506
|
};
|
|
311
507
|
}
|
|
312
508
|
catch (error) {
|
|
313
|
-
// If table doesn't exist, return empty result instead of throwing
|
|
314
509
|
if (error?.message?.includes('does not exist') || error?.code === '42P01') {
|
|
315
510
|
return {
|
|
316
511
|
events: [],
|
|
@@ -386,7 +581,6 @@ export function createClickHouseProvider(options) {
|
|
|
386
581
|
tableEnsuring = false;
|
|
387
582
|
}
|
|
388
583
|
};
|
|
389
|
-
// Call ensureTable asynchronously (don't block initialization)
|
|
390
584
|
ensureTableSync().catch(console.error);
|
|
391
585
|
const ingestBatchFn = async (events) => {
|
|
392
586
|
if (events.length === 0)
|
|
@@ -420,7 +614,6 @@ export function createClickHouseProvider(options) {
|
|
|
420
614
|
console.log(`✅ Inserted ${rows.length} event(s) into ClickHouse ${tableFullName}`);
|
|
421
615
|
}
|
|
422
616
|
else {
|
|
423
|
-
// Fallback: use INSERT query
|
|
424
617
|
const values = rows
|
|
425
618
|
.map((row) => `('${row.id}', '${row.type}', '${new Date(row.timestamp).toISOString().replace('T', ' ').slice(0, 19)}', '${row.status || 'success'}', ${row.user_id ? `'${row.user_id.replace(/'/g, "''")}'` : 'NULL'}, ${row.session_id ? `'${row.session_id.replace(/'/g, "''")}'` : 'NULL'}, ${row.organization_id ? `'${row.organization_id.replace(/'/g, "''")}'` : 'NULL'}, '${row.metadata.replace(/'/g, "''")}', ${row.ip_address ? `'${row.ip_address.replace(/'/g, "''")}'` : 'NULL'}, ${row.user_agent ? `'${row.user_agent.replace(/'/g, "''")}'` : 'NULL'}, '${row.source}', ${row.display_message ? `'${row.display_message.replace(/'/g, "''")}'` : 'NULL'}, ${row.display_severity ? `'${row.display_severity}'` : 'NULL'})`)
|
|
426
619
|
.join(', ');
|
|
@@ -482,7 +675,6 @@ export function createClickHouseProvider(options) {
|
|
|
482
675
|
tableExists = false;
|
|
483
676
|
}
|
|
484
677
|
}
|
|
485
|
-
// If table doesn't exist, try to create it
|
|
486
678
|
if (!tableExists) {
|
|
487
679
|
const createTableQuery = `
|
|
488
680
|
CREATE TABLE IF NOT EXISTS ${tableFullName} (
|
|
@@ -512,7 +704,6 @@ export function createClickHouseProvider(options) {
|
|
|
512
704
|
}
|
|
513
705
|
}
|
|
514
706
|
else {
|
|
515
|
-
// Table exists, check if status column exists
|
|
516
707
|
try {
|
|
517
708
|
const checkColumnQuery = `
|
|
518
709
|
SELECT count() as exists
|
|
@@ -557,7 +748,6 @@ export function createClickHouseProvider(options) {
|
|
|
557
748
|
}
|
|
558
749
|
catch (alterError) {
|
|
559
750
|
console.warn(`Failed to add status column to ${tableFullName}:`, alterError);
|
|
560
|
-
// Continue anyway - we'll handle missing column in query
|
|
561
751
|
}
|
|
562
752
|
}
|
|
563
753
|
}
|
|
@@ -588,7 +778,6 @@ export function createClickHouseProvider(options) {
|
|
|
588
778
|
}
|
|
589
779
|
const whereClause = whereClauses.length > 0 ? `WHERE ${whereClauses.join(' AND ')}` : '';
|
|
590
780
|
const orderDirection = sort === 'desc' ? 'DESC' : 'ASC';
|
|
591
|
-
// Try to query with status column first, fallback if it doesn't exist
|
|
592
781
|
let query = `
|
|
593
782
|
SELECT id, type, timestamp, status, user_id, session_id, organization_id,
|
|
594
783
|
metadata, ip_address, user_agent, source, display_message, display_severity
|
|
@@ -613,12 +802,10 @@ export function createClickHouseProvider(options) {
|
|
|
613
802
|
}
|
|
614
803
|
}
|
|
615
804
|
catch (error) {
|
|
616
|
-
// If error is about missing status column, retry without it
|
|
617
805
|
if (error?.message?.includes('Unknown expression identifier') &&
|
|
618
806
|
error?.message?.includes('status')) {
|
|
619
807
|
console.warn(`Status column not found in ${tableFullName}, querying without it`);
|
|
620
808
|
hasStatusColumn = false;
|
|
621
|
-
// Retry query without status column
|
|
622
809
|
query = `
|
|
623
810
|
SELECT id, type, timestamp, user_id, session_id, organization_id,
|
|
624
811
|
metadata, ip_address, user_agent, source, display_message, display_severity
|
|
@@ -659,14 +846,13 @@ export function createClickHouseProvider(options) {
|
|
|
659
846
|
throw error;
|
|
660
847
|
}
|
|
661
848
|
}
|
|
662
|
-
// Handle case where result might be an array or object
|
|
663
849
|
const rows = Array.isArray(result) ? result : result?.data || [];
|
|
664
850
|
const hasMore = rows.length > limit;
|
|
665
851
|
const events = rows.slice(0, limit).map((row) => ({
|
|
666
852
|
id: row.id,
|
|
667
853
|
type: row.type,
|
|
668
854
|
timestamp: new Date(row.timestamp),
|
|
669
|
-
status: hasStatusColumn ? row.status || 'success' : 'success',
|
|
855
|
+
status: hasStatusColumn ? row.status || 'success' : 'success',
|
|
670
856
|
userId: row.user_id || undefined,
|
|
671
857
|
sessionId: row.session_id || undefined,
|
|
672
858
|
organizationId: row.organization_id || undefined,
|
|
@@ -710,7 +896,6 @@ export function createHttpProvider(options) {
|
|
|
710
896
|
}
|
|
711
897
|
export function createStorageProvider(options) {
|
|
712
898
|
const { adapter, tableName = 'auth_events' } = options;
|
|
713
|
-
// Ensure table exists (for Prisma/Drizzle adapters)
|
|
714
899
|
const ensureTable = async () => {
|
|
715
900
|
if (!adapter)
|
|
716
901
|
return;
|