@mastra/cloudflare-d1 0.1.5-alpha.2

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/index.js ADDED
@@ -0,0 +1,938 @@
1
+ import { MastraStorage, TABLE_THREADS, TABLE_MESSAGES, TABLE_WORKFLOW_SNAPSHOT, TABLE_TRACES, TABLE_EVALS } from '@mastra/core/storage';
2
+ import Cloudflare from 'cloudflare';
3
+
4
+ // src/storage/index.ts
5
+
6
+ // src/storage/sql-builder.ts
7
+ var SqlBuilder = class {
8
+ sql = "";
9
+ params = [];
10
+ whereAdded = false;
11
+ // Basic query building
12
+ select(columns) {
13
+ if (!columns || Array.isArray(columns) && columns.length === 0) {
14
+ this.sql = "SELECT *";
15
+ } else {
16
+ this.sql = `SELECT ${Array.isArray(columns) ? columns.join(", ") : columns}`;
17
+ }
18
+ return this;
19
+ }
20
+ from(table) {
21
+ this.sql += ` FROM ${table}`;
22
+ return this;
23
+ }
24
+ /**
25
+ * Add a WHERE clause to the query
26
+ * @param condition The condition to add
27
+ * @param params Parameters to bind to the condition
28
+ */
29
+ where(condition, ...params) {
30
+ this.sql += ` WHERE ${condition}`;
31
+ this.params.push(...params);
32
+ this.whereAdded = true;
33
+ return this;
34
+ }
35
+ /**
36
+ * Add a WHERE clause if it hasn't been added yet, otherwise add an AND clause
37
+ * @param condition The condition to add
38
+ * @param params Parameters to bind to the condition
39
+ */
40
+ whereAnd(condition, ...params) {
41
+ if (this.whereAdded) {
42
+ return this.andWhere(condition, ...params);
43
+ } else {
44
+ return this.where(condition, ...params);
45
+ }
46
+ }
47
+ andWhere(condition, ...params) {
48
+ this.sql += ` AND ${condition}`;
49
+ this.params.push(...params);
50
+ return this;
51
+ }
52
+ orWhere(condition, ...params) {
53
+ this.sql += ` OR ${condition}`;
54
+ this.params.push(...params);
55
+ return this;
56
+ }
57
+ orderBy(column, direction = "ASC") {
58
+ this.sql += ` ORDER BY ${column} ${direction}`;
59
+ return this;
60
+ }
61
+ limit(count) {
62
+ this.sql += ` LIMIT ?`;
63
+ this.params.push(count);
64
+ return this;
65
+ }
66
+ offset(count) {
67
+ this.sql += ` OFFSET ?`;
68
+ this.params.push(count);
69
+ return this;
70
+ }
71
+ /**
72
+ * Insert a row, or update specific columns on conflict (upsert).
73
+ * @param table Table name
74
+ * @param columns Columns to insert
75
+ * @param values Values to insert
76
+ * @param conflictColumns Columns to check for conflict (usually PK or UNIQUE)
77
+ * @param updateMap Object mapping columns to update to their new value (e.g. { name: 'excluded.name' })
78
+ */
79
+ insert(table, columns, values, conflictColumns, updateMap) {
80
+ const placeholders = columns.map(() => "?").join(", ");
81
+ if (conflictColumns && updateMap) {
82
+ const updateClause = Object.entries(updateMap).map(([col, expr]) => `${col} = ${expr}`).join(", ");
83
+ this.sql = `INSERT INTO ${table} (${columns.join(", ")}) VALUES (${placeholders}) ON CONFLICT(${conflictColumns.join(", ")}) DO UPDATE SET ${updateClause}`;
84
+ this.params.push(...values);
85
+ return this;
86
+ }
87
+ this.sql = `INSERT INTO ${table} (${columns.join(", ")}) VALUES (${placeholders})`;
88
+ this.params.push(...values);
89
+ return this;
90
+ }
91
+ // Update operations
92
+ update(table, columns, values) {
93
+ const setClause = columns.map((col) => `${col} = ?`).join(", ");
94
+ this.sql = `UPDATE ${table} SET ${setClause}`;
95
+ this.params.push(...values);
96
+ return this;
97
+ }
98
+ // Delete operations
99
+ delete(table) {
100
+ this.sql = `DELETE FROM ${table}`;
101
+ return this;
102
+ }
103
+ /**
104
+ * Create a table if it doesn't exist
105
+ * @param table The table name
106
+ * @param columnDefinitions The column definitions as an array of strings
107
+ * @param tableConstraints Optional constraints for the table
108
+ * @returns The builder instance
109
+ */
110
+ createTable(table, columnDefinitions, tableConstraints) {
111
+ const columns = columnDefinitions.join(", ");
112
+ const constraints = tableConstraints && tableConstraints.length > 0 ? ", " + tableConstraints.join(", ") : "";
113
+ this.sql = `CREATE TABLE IF NOT EXISTS ${table} (${columns}${constraints})`;
114
+ return this;
115
+ }
116
+ /**
117
+ * Check if an index exists in the database
118
+ * @param indexName The name of the index to check
119
+ * @param tableName The table the index is on
120
+ * @returns The builder instance
121
+ */
122
+ checkIndexExists(indexName, tableName) {
123
+ this.sql = `SELECT name FROM sqlite_master WHERE type='index' AND name=? AND tbl_name=?`;
124
+ this.params.push(indexName, tableName);
125
+ return this;
126
+ }
127
+ /**
128
+ * Create an index if it doesn't exist
129
+ * @param indexName The name of the index to create
130
+ * @param tableName The table to create the index on
131
+ * @param columnName The column to index
132
+ * @param indexType Optional index type (e.g., 'UNIQUE')
133
+ * @returns The builder instance
134
+ */
135
+ createIndex(indexName, tableName, columnName, indexType = "") {
136
+ this.sql = `CREATE ${indexType ? indexType + " " : ""}INDEX IF NOT EXISTS ${indexName} ON ${tableName}(${columnName})`;
137
+ return this;
138
+ }
139
+ // Raw SQL with params
140
+ raw(sql, ...params) {
141
+ this.sql = sql;
142
+ this.params.push(...params);
143
+ return this;
144
+ }
145
+ /**
146
+ * Add a LIKE condition to the query
147
+ * @param column The column to check
148
+ * @param value The value to match (will be wrapped with % for LIKE)
149
+ * @param exact If true, will not add % wildcards
150
+ */
151
+ like(column, value, exact = false) {
152
+ const likeValue = exact ? value : `%${value}%`;
153
+ if (this.whereAdded) {
154
+ this.sql += ` AND ${column} LIKE ?`;
155
+ } else {
156
+ this.sql += ` WHERE ${column} LIKE ?`;
157
+ this.whereAdded = true;
158
+ }
159
+ this.params.push(likeValue);
160
+ return this;
161
+ }
162
+ /**
163
+ * Add a JSON LIKE condition for searching in JSON fields
164
+ * @param column The JSON column to search in
165
+ * @param key The JSON key to match
166
+ * @param value The value to match
167
+ */
168
+ jsonLike(column, key, value) {
169
+ const jsonPattern = `%"${key}":"${value}"%`;
170
+ if (this.whereAdded) {
171
+ this.sql += ` AND ${column} LIKE ?`;
172
+ } else {
173
+ this.sql += ` WHERE ${column} LIKE ?`;
174
+ this.whereAdded = true;
175
+ }
176
+ this.params.push(jsonPattern);
177
+ return this;
178
+ }
179
+ /**
180
+ * Get the built query
181
+ * @returns Object containing the SQL string and parameters array
182
+ */
183
+ build() {
184
+ return {
185
+ sql: this.sql,
186
+ params: this.params
187
+ };
188
+ }
189
+ /**
190
+ * Reset the builder for reuse
191
+ * @returns The reset builder instance
192
+ */
193
+ reset() {
194
+ this.sql = "";
195
+ this.params = [];
196
+ this.whereAdded = false;
197
+ return this;
198
+ }
199
+ };
200
+ function createSqlBuilder() {
201
+ return new SqlBuilder();
202
+ }
203
+
204
+ // src/storage/index.ts
205
+ function isArrayOfRecords(value) {
206
+ return value && Array.isArray(value) && value.length > 0;
207
+ }
208
+ var D1Store = class extends MastraStorage {
209
+ client;
210
+ accountId;
211
+ databaseId;
212
+ binding;
213
+ // D1Database binding
214
+ tablePrefix;
215
+ /**
216
+ * Creates a new D1Store instance
217
+ * @param config Configuration for D1 access (either REST API or Workers Binding API)
218
+ */
219
+ constructor(config) {
220
+ super({ name: "D1" });
221
+ this.tablePrefix = config.tablePrefix || "";
222
+ if ("binding" in config) {
223
+ if (!config.binding) {
224
+ throw new Error("D1 binding is required when using Workers Binding API");
225
+ }
226
+ this.binding = config.binding;
227
+ this.logger.info("Using D1 Workers Binding API");
228
+ } else {
229
+ if (!config.accountId || !config.databaseId || !config.apiToken) {
230
+ throw new Error("accountId, databaseId, and apiToken are required when using REST API");
231
+ }
232
+ this.accountId = config.accountId;
233
+ this.databaseId = config.databaseId;
234
+ this.client = new Cloudflare({
235
+ apiToken: config.apiToken
236
+ });
237
+ this.logger.info("Using D1 REST API");
238
+ }
239
+ }
240
+ // Helper method to get the full table name with prefix
241
+ getTableName(tableName) {
242
+ return `${this.tablePrefix}${tableName}`;
243
+ }
244
+ formatSqlParams(params) {
245
+ return params.map((p) => p === void 0 || p === null ? null : p);
246
+ }
247
+ // Helper method to create SQL indexes for better query performance
248
+ async createIndexIfNotExists(tableName, columnName, indexType = "") {
249
+ const fullTableName = this.getTableName(tableName);
250
+ const indexName = `idx_${tableName}_${columnName}`;
251
+ try {
252
+ const checkQuery = createSqlBuilder().checkIndexExists(indexName, fullTableName);
253
+ const { sql: checkSql, params: checkParams } = checkQuery.build();
254
+ const indexExists = await this.executeQuery({
255
+ sql: checkSql,
256
+ params: checkParams,
257
+ first: true
258
+ });
259
+ if (!indexExists) {
260
+ const createQuery = createSqlBuilder().createIndex(indexName, fullTableName, columnName, indexType);
261
+ const { sql: createSql, params: createParams } = createQuery.build();
262
+ await this.executeQuery({ sql: createSql, params: createParams });
263
+ this.logger.debug(`Created index ${indexName} on ${fullTableName}(${columnName})`);
264
+ }
265
+ } catch (error) {
266
+ this.logger.error(`Error creating index on ${fullTableName}(${columnName}):`, {
267
+ message: error instanceof Error ? error.message : String(error)
268
+ });
269
+ }
270
+ }
271
+ async executeWorkersBindingQuery({
272
+ sql,
273
+ params = [],
274
+ first = false
275
+ }) {
276
+ if (!this.binding) {
277
+ throw new Error("Workers binding is not configured");
278
+ }
279
+ try {
280
+ const statement = this.binding.prepare(sql);
281
+ const formattedParams = this.formatSqlParams(params);
282
+ let result;
283
+ if (formattedParams.length > 0) {
284
+ if (first) {
285
+ result = await statement.bind(...formattedParams).first();
286
+ if (!result) return null;
287
+ return result;
288
+ } else {
289
+ result = await statement.bind(...formattedParams).all();
290
+ const results = result.results || [];
291
+ if (result.meta) {
292
+ this.logger.debug("Query metadata", { meta: result.meta });
293
+ }
294
+ return results;
295
+ }
296
+ } else {
297
+ if (first) {
298
+ result = await statement.first();
299
+ if (!result) return null;
300
+ return result;
301
+ } else {
302
+ result = await statement.all();
303
+ const results = result.results || [];
304
+ if (result.meta) {
305
+ this.logger.debug("Query metadata", { meta: result.meta });
306
+ }
307
+ return results;
308
+ }
309
+ }
310
+ } catch (workerError) {
311
+ this.logger.error("Workers Binding API error", {
312
+ message: workerError instanceof Error ? workerError.message : String(workerError),
313
+ sql
314
+ });
315
+ throw new Error(`D1 Workers API error: ${workerError.message}`);
316
+ }
317
+ }
318
+ async executeRestQuery({
319
+ sql,
320
+ params = [],
321
+ first = false
322
+ }) {
323
+ if (!this.client || !this.accountId || !this.databaseId) {
324
+ throw new Error("Missing required REST API configuration");
325
+ }
326
+ try {
327
+ const response = await this.client.d1.database.query(this.databaseId, {
328
+ account_id: this.accountId,
329
+ sql,
330
+ params: this.formatSqlParams(params)
331
+ });
332
+ const result = response.result || [];
333
+ const results = result.flatMap((r) => r.results || []);
334
+ if (first) {
335
+ const firstResult = isArrayOfRecords(results) && results.length > 0 ? results[0] : null;
336
+ if (!firstResult) return null;
337
+ return firstResult;
338
+ }
339
+ return results;
340
+ } catch (restError) {
341
+ this.logger.error("REST API error", {
342
+ message: restError instanceof Error ? restError.message : String(restError),
343
+ sql
344
+ });
345
+ throw new Error(`D1 REST API error: ${restError.message}`);
346
+ }
347
+ }
348
+ /**
349
+ * Execute a SQL query against the D1 database
350
+ * @param options Query options including SQL, parameters, and whether to return only the first result
351
+ * @returns Query results as an array or a single object if first=true
352
+ */
353
+ async executeQuery(options) {
354
+ const { sql, params = [], first = false } = options;
355
+ try {
356
+ this.logger.debug("Executing SQL query", { sql, params, first });
357
+ if (this.binding) {
358
+ return this.executeWorkersBindingQuery({ sql, params, first });
359
+ } else if (this.client && this.accountId && this.databaseId) {
360
+ return this.executeRestQuery({ sql, params, first });
361
+ } else {
362
+ throw new Error("No valid D1 configuration provided");
363
+ }
364
+ } catch (error) {
365
+ this.logger.error("Error executing SQL query", {
366
+ message: error instanceof Error ? error.message : String(error),
367
+ sql,
368
+ params,
369
+ first
370
+ });
371
+ throw new Error(`D1 query error: ${error.message}`);
372
+ }
373
+ }
374
+ // Helper to convert storage type to SQL type
375
+ getSqlType(type) {
376
+ switch (type) {
377
+ case "text":
378
+ return "TEXT";
379
+ case "timestamp":
380
+ return "TIMESTAMP";
381
+ case "integer":
382
+ return "INTEGER";
383
+ case "bigint":
384
+ return "INTEGER";
385
+ // SQLite doesn't have a separate BIGINT type
386
+ case "jsonb":
387
+ return "TEXT";
388
+ // Store JSON as TEXT in SQLite
389
+ default:
390
+ return "TEXT";
391
+ }
392
+ }
393
+ ensureDate(date) {
394
+ if (!date) return void 0;
395
+ return date instanceof Date ? date : new Date(date);
396
+ }
397
+ serializeDate(date) {
398
+ if (!date) return void 0;
399
+ const dateObj = this.ensureDate(date);
400
+ return dateObj?.toISOString();
401
+ }
402
+ // Helper to serialize objects to JSON strings
403
+ serializeValue(value) {
404
+ if (value === null || value === void 0) return null;
405
+ if (value instanceof Date) {
406
+ return this.serializeDate(value);
407
+ }
408
+ if (typeof value === "object") {
409
+ return JSON.stringify(value);
410
+ }
411
+ return value;
412
+ }
413
+ // Helper to deserialize JSON strings to objects
414
+ deserializeValue(value, type) {
415
+ if (value === null || value === void 0) return null;
416
+ if (type === "date" && typeof value === "string") {
417
+ return new Date(value);
418
+ }
419
+ if (type === "jsonb" && typeof value === "string") {
420
+ try {
421
+ return JSON.parse(value);
422
+ } catch {
423
+ return value;
424
+ }
425
+ }
426
+ if (typeof value === "string" && (value.startsWith("{") || value.startsWith("["))) {
427
+ try {
428
+ return JSON.parse(value);
429
+ } catch {
430
+ return value;
431
+ }
432
+ }
433
+ return value;
434
+ }
435
+ async createTable({
436
+ tableName,
437
+ schema
438
+ }) {
439
+ const fullTableName = this.getTableName(tableName);
440
+ const columnDefinitions = Object.entries(schema).map(([colName, colDef]) => {
441
+ const type = this.getSqlType(colDef.type);
442
+ const nullable = colDef.nullable === false ? "NOT NULL" : "";
443
+ const primaryKey = colDef.primaryKey ? "PRIMARY KEY" : "";
444
+ return `${colName} ${type} ${nullable} ${primaryKey}`.trim();
445
+ });
446
+ const tableConstraints = [];
447
+ if (tableName === TABLE_WORKFLOW_SNAPSHOT) {
448
+ tableConstraints.push("UNIQUE (workflow_name, run_id)");
449
+ }
450
+ const query = createSqlBuilder().createTable(fullTableName, columnDefinitions, tableConstraints);
451
+ const { sql, params } = query.build();
452
+ try {
453
+ await this.executeQuery({ sql, params });
454
+ this.logger.debug(`Created table ${fullTableName}`);
455
+ } catch (error) {
456
+ this.logger.error(`Error creating table ${fullTableName}:`, {
457
+ message: error instanceof Error ? error.message : String(error)
458
+ });
459
+ throw new Error(`Failed to create table ${fullTableName}: ${error}`);
460
+ }
461
+ }
462
+ async clearTable({ tableName }) {
463
+ const fullTableName = this.getTableName(tableName);
464
+ try {
465
+ const query = createSqlBuilder().delete(fullTableName);
466
+ const { sql, params } = query.build();
467
+ await this.executeQuery({ sql, params });
468
+ this.logger.debug(`Cleared table ${fullTableName}`);
469
+ } catch (error) {
470
+ this.logger.error(`Error clearing table ${fullTableName}:`, {
471
+ message: error instanceof Error ? error.message : String(error)
472
+ });
473
+ throw new Error(`Failed to clear table ${fullTableName}: ${error}`);
474
+ }
475
+ }
476
+ async processRecord(record) {
477
+ const processedRecord = {};
478
+ for (const [key, value] of Object.entries(record)) {
479
+ processedRecord[key] = this.serializeValue(value);
480
+ }
481
+ return processedRecord;
482
+ }
483
+ async insert({ tableName, record }) {
484
+ const fullTableName = this.getTableName(tableName);
485
+ const processedRecord = await this.processRecord(record);
486
+ const columns = Object.keys(processedRecord);
487
+ const values = Object.values(processedRecord);
488
+ const query = createSqlBuilder().insert(fullTableName, columns, values);
489
+ const { sql, params } = query.build();
490
+ try {
491
+ await this.executeQuery({ sql, params });
492
+ } catch (error) {
493
+ this.logger.error(`Error inserting into ${fullTableName}:`, { error });
494
+ throw new Error(`Failed to insert into ${fullTableName}: ${error}`);
495
+ }
496
+ }
497
+ async load({ tableName, keys }) {
498
+ const fullTableName = this.getTableName(tableName);
499
+ const query = createSqlBuilder().select("*").from(fullTableName);
500
+ let firstKey = true;
501
+ for (const [key, value] of Object.entries(keys)) {
502
+ if (firstKey) {
503
+ query.where(`${key} = ?`, value);
504
+ firstKey = false;
505
+ } else {
506
+ query.andWhere(`${key} = ?`, value);
507
+ }
508
+ }
509
+ query.limit(1);
510
+ const { sql, params } = query.build();
511
+ try {
512
+ const result = await this.executeQuery({ sql, params, first: true });
513
+ if (!result) return null;
514
+ const processedResult = {};
515
+ for (const [key, value] of Object.entries(result)) {
516
+ processedResult[key] = this.deserializeValue(value);
517
+ }
518
+ return processedResult;
519
+ } catch (error) {
520
+ this.logger.error(`Error loading from ${fullTableName}:`, {
521
+ message: error instanceof Error ? error.message : String(error)
522
+ });
523
+ return null;
524
+ }
525
+ }
526
+ async getThreadById({ threadId }) {
527
+ const thread = await this.load({
528
+ tableName: TABLE_THREADS,
529
+ keys: { id: threadId }
530
+ });
531
+ if (!thread) return null;
532
+ try {
533
+ return {
534
+ ...thread,
535
+ createdAt: this.ensureDate(thread.createdAt),
536
+ updatedAt: this.ensureDate(thread.updatedAt),
537
+ metadata: typeof thread.metadata === "string" ? JSON.parse(thread.metadata || "{}") : thread.metadata || {}
538
+ };
539
+ } catch (error) {
540
+ this.logger.error(`Error processing thread ${threadId}:`, {
541
+ message: error instanceof Error ? error.message : String(error)
542
+ });
543
+ return null;
544
+ }
545
+ }
546
+ async getThreadsByResourceId({ resourceId }) {
547
+ const fullTableName = this.getTableName(TABLE_THREADS);
548
+ try {
549
+ const query = createSqlBuilder().select("*").from(fullTableName).where("resourceId = ?", resourceId);
550
+ const { sql, params } = query.build();
551
+ const results = await this.executeQuery({ sql, params });
552
+ return (isArrayOfRecords(results) ? results : []).map((thread) => ({
553
+ ...thread,
554
+ createdAt: this.ensureDate(thread.createdAt),
555
+ updatedAt: this.ensureDate(thread.updatedAt),
556
+ metadata: typeof thread.metadata === "string" ? JSON.parse(thread.metadata || "{}") : thread.metadata || {}
557
+ }));
558
+ } catch (error) {
559
+ this.logger.error(`Error getting threads by resourceId ${resourceId}:`, {
560
+ message: error instanceof Error ? error.message : String(error)
561
+ });
562
+ return [];
563
+ }
564
+ }
565
+ async saveThread({ thread }) {
566
+ const fullTableName = this.getTableName(TABLE_THREADS);
567
+ const threadToSave = {
568
+ id: thread.id,
569
+ resourceId: thread.resourceId,
570
+ title: thread.title,
571
+ metadata: thread.metadata ? JSON.stringify(thread.metadata) : null,
572
+ createdAt: thread.createdAt,
573
+ updatedAt: thread.updatedAt
574
+ };
575
+ const processedRecord = await this.processRecord(threadToSave);
576
+ const columns = Object.keys(processedRecord);
577
+ const values = Object.values(processedRecord);
578
+ const updateMap = {
579
+ resourceId: "excluded.resourceId",
580
+ title: "excluded.title",
581
+ metadata: "excluded.metadata",
582
+ createdAt: "excluded.createdAt",
583
+ updatedAt: "excluded.updatedAt"
584
+ };
585
+ const query = createSqlBuilder().insert(fullTableName, columns, values, ["id"], updateMap);
586
+ const { sql, params } = query.build();
587
+ try {
588
+ await this.executeQuery({ sql, params });
589
+ return thread;
590
+ } catch (error) {
591
+ this.logger.error(`Error saving thread to ${fullTableName}:`, { error });
592
+ throw error;
593
+ }
594
+ }
595
+ async updateThread({
596
+ id,
597
+ title,
598
+ metadata
599
+ }) {
600
+ const thread = await this.getThreadById({ threadId: id });
601
+ if (!thread) {
602
+ throw new Error(`Thread ${id} not found`);
603
+ }
604
+ const fullTableName = this.getTableName(TABLE_THREADS);
605
+ const mergedMetadata = {
606
+ ...typeof thread.metadata === "string" ? JSON.parse(thread.metadata) : thread.metadata,
607
+ ...metadata
608
+ };
609
+ const columns = ["title", "metadata", "updatedAt"];
610
+ const values = [title, JSON.stringify(mergedMetadata), (/* @__PURE__ */ new Date()).toISOString()];
611
+ const query = createSqlBuilder().update(fullTableName, columns, values).where("id = ?", id);
612
+ const { sql, params } = query.build();
613
+ try {
614
+ await this.executeQuery({ sql, params });
615
+ return {
616
+ ...thread,
617
+ title,
618
+ metadata: {
619
+ ...typeof thread.metadata === "string" ? JSON.parse(thread.metadata) : thread.metadata,
620
+ ...metadata
621
+ },
622
+ updatedAt: /* @__PURE__ */ new Date()
623
+ };
624
+ } catch (error) {
625
+ this.logger.error("Error updating thread:", { error });
626
+ throw error;
627
+ }
628
+ }
629
+ async deleteThread({ threadId }) {
630
+ const fullTableName = this.getTableName(TABLE_THREADS);
631
+ try {
632
+ const deleteThreadQuery = createSqlBuilder().delete(fullTableName).where("id = ?", threadId);
633
+ const { sql: threadSql, params: threadParams } = deleteThreadQuery.build();
634
+ await this.executeQuery({ sql: threadSql, params: threadParams });
635
+ const messagesTableName = this.getTableName(TABLE_MESSAGES);
636
+ const deleteMessagesQuery = createSqlBuilder().delete(messagesTableName).where("thread_id = ?", threadId);
637
+ const { sql: messagesSql, params: messagesParams } = deleteMessagesQuery.build();
638
+ await this.executeQuery({ sql: messagesSql, params: messagesParams });
639
+ } catch (error) {
640
+ this.logger.error(`Error deleting thread ${threadId}:`, {
641
+ message: error instanceof Error ? error.message : String(error)
642
+ });
643
+ throw new Error(`Failed to delete thread ${threadId}: ${error}`);
644
+ }
645
+ }
646
+ // Thread and message management methods
647
+ async saveMessages({ messages }) {
648
+ if (messages.length === 0) return [];
649
+ try {
650
+ const now = /* @__PURE__ */ new Date();
651
+ for (const [i, message] of messages.entries()) {
652
+ if (!message.id) throw new Error(`Message at index ${i} missing id`);
653
+ if (!message.threadId) throw new Error(`Message at index ${i} missing threadId`);
654
+ if (!message.content) throw new Error(`Message at index ${i} missing content`);
655
+ if (!message.role) throw new Error(`Message at index ${i} missing role`);
656
+ const thread = await this.getThreadById({ threadId: message.threadId });
657
+ if (!thread) {
658
+ throw new Error(`Thread ${message.threadId} not found`);
659
+ }
660
+ }
661
+ const messagesToInsert = messages.map((message) => {
662
+ const createdAt = message.createdAt ? new Date(message.createdAt) : now;
663
+ return {
664
+ id: message.id,
665
+ thread_id: message.threadId,
666
+ content: typeof message.content === "string" ? message.content : JSON.stringify(message.content),
667
+ createdAt: createdAt.toISOString(),
668
+ role: message.role,
669
+ type: message.type
670
+ };
671
+ });
672
+ await this.batchInsert({
673
+ tableName: TABLE_MESSAGES,
674
+ records: messagesToInsert
675
+ });
676
+ this.logger.debug(`Saved ${messages.length} messages`);
677
+ return messages;
678
+ } catch (error) {
679
+ this.logger.error("Error saving messages:", { message: error instanceof Error ? error.message : String(error) });
680
+ throw error;
681
+ }
682
+ }
683
+ async getMessages({ threadId, selectBy }) {
684
+ const fullTableName = this.getTableName(TABLE_MESSAGES);
685
+ const limit = typeof selectBy?.last === "number" ? selectBy.last : 40;
686
+ const include = selectBy?.include || [];
687
+ const messages = [];
688
+ try {
689
+ if (include.length) {
690
+ const prevMax = Math.max(...include.map((i) => i.withPreviousMessages || 0));
691
+ const nextMax = Math.max(...include.map((i) => i.withNextMessages || 0));
692
+ const includeIds = include.map((i) => i.id);
693
+ const sql2 = `
694
+ WITH ordered_messages AS (
695
+ SELECT
696
+ *,
697
+ ROW_NUMBER() OVER (ORDER BY createdAt DESC) AS row_num
698
+ FROM ${fullTableName}
699
+ WHERE thread_id = ?
700
+ )
701
+ SELECT
702
+ m.id,
703
+ m.content,
704
+ m.role,
705
+ m.type,
706
+ m.createdAt,
707
+ m.thread_id AS "threadId"
708
+ FROM ordered_messages m
709
+ WHERE m.id IN (${includeIds.map(() => "?").join(",")})
710
+ OR EXISTS (
711
+ SELECT 1 FROM ordered_messages target
712
+ WHERE target.id IN (${includeIds.map(() => "?").join(",")})
713
+ AND (
714
+ (m.row_num <= target.row_num + ? AND m.row_num > target.row_num)
715
+ OR
716
+ (m.row_num >= target.row_num - ? AND m.row_num < target.row_num)
717
+ )
718
+ )
719
+ ORDER BY m.createdAt DESC
720
+ `;
721
+ const params2 = [
722
+ threadId,
723
+ ...includeIds,
724
+ // for m.id IN (...)
725
+ ...includeIds,
726
+ // for target.id IN (...)
727
+ prevMax,
728
+ nextMax
729
+ ];
730
+ const includeResult = await this.executeQuery({ sql: sql2, params: params2 });
731
+ if (Array.isArray(includeResult)) messages.push(...includeResult);
732
+ }
733
+ const excludeIds = messages.map((m) => m.id);
734
+ let query = createSqlBuilder().select(["id", "content", "role", "type", '"createdAt"', 'thread_id AS "threadId"']).from(fullTableName).where("thread_id = ?", threadId).andWhere(`id NOT IN (${excludeIds.map(() => "?").join(",")})`, ...excludeIds).orderBy("createdAt", "DESC").limit(limit);
735
+ const { sql, params } = query.build();
736
+ const result = await this.executeQuery({ sql, params });
737
+ if (Array.isArray(result)) messages.push(...result);
738
+ messages.sort((a, b) => {
739
+ const aRecord = a;
740
+ const bRecord = b;
741
+ const timeA = new Date(aRecord.createdAt).getTime();
742
+ const timeB = new Date(bRecord.createdAt).getTime();
743
+ return timeA - timeB;
744
+ });
745
+ const processedMessages = messages.map((message) => {
746
+ const processedMsg = {};
747
+ for (const [key, value] of Object.entries(message)) {
748
+ processedMsg[key] = this.deserializeValue(value);
749
+ }
750
+ return processedMsg;
751
+ });
752
+ this.logger.debug(`Retrieved ${messages.length} messages for thread ${threadId}`);
753
+ return processedMessages;
754
+ } catch (error) {
755
+ this.logger.error("Error retrieving messages for thread", {
756
+ threadId,
757
+ message: error instanceof Error ? error.message : String(error)
758
+ });
759
+ return [];
760
+ }
761
+ }
762
+ async persistWorkflowSnapshot({
763
+ workflowName,
764
+ runId,
765
+ snapshot
766
+ }) {
767
+ const fullTableName = this.getTableName(TABLE_WORKFLOW_SNAPSHOT);
768
+ const now = (/* @__PURE__ */ new Date()).toISOString();
769
+ const currentSnapshot = await this.load({
770
+ tableName: TABLE_WORKFLOW_SNAPSHOT,
771
+ keys: { workflow_name: workflowName, run_id: runId }
772
+ });
773
+ const persisting = currentSnapshot ? {
774
+ ...currentSnapshot,
775
+ snapshot: JSON.stringify(snapshot),
776
+ updatedAt: now
777
+ } : {
778
+ workflow_name: workflowName,
779
+ run_id: runId,
780
+ snapshot,
781
+ createdAt: now,
782
+ updatedAt: now
783
+ };
784
+ const processedRecord = await this.processRecord(persisting);
785
+ const columns = Object.keys(processedRecord);
786
+ const values = Object.values(processedRecord);
787
+ const updateMap = {
788
+ snapshot: "excluded.snapshot",
789
+ updatedAt: "excluded.updatedAt"
790
+ };
791
+ this.logger.debug("Persisting workflow snapshot", { workflowName, runId });
792
+ const query = createSqlBuilder().insert(fullTableName, columns, values, ["workflow_name", "run_id"], updateMap);
793
+ const { sql, params } = query.build();
794
+ try {
795
+ await this.executeQuery({ sql, params });
796
+ } catch (error) {
797
+ this.logger.error("Error persisting workflow snapshot:", {
798
+ message: error instanceof Error ? error.message : String(error)
799
+ });
800
+ throw error;
801
+ }
802
+ }
803
+ async loadWorkflowSnapshot(params) {
804
+ const { workflowName, runId } = params;
805
+ this.logger.debug("Loading workflow snapshot", { workflowName, runId });
806
+ const d = await this.load({
807
+ tableName: TABLE_WORKFLOW_SNAPSHOT,
808
+ keys: {
809
+ workflow_name: workflowName,
810
+ run_id: runId
811
+ }
812
+ });
813
+ return d ? d.snapshot : null;
814
+ }
815
+ /**
816
+ * Insert multiple records in a batch operation
817
+ * @param tableName The table to insert into
818
+ * @param records The records to insert
819
+ */
820
+ async batchInsert({ tableName, records }) {
821
+ if (records.length === 0) return;
822
+ const fullTableName = this.getTableName(tableName);
823
+ try {
824
+ const batchSize = 50;
825
+ for (let i = 0; i < records.length; i += batchSize) {
826
+ const batch = records.slice(i, i + batchSize);
827
+ const recordsToInsert = batch;
828
+ if (recordsToInsert.length > 0) {
829
+ const firstRecord = recordsToInsert[0];
830
+ const columns = Object.keys(firstRecord || {});
831
+ for (const record of recordsToInsert) {
832
+ const values = columns.map((col) => {
833
+ if (!record) return null;
834
+ const value = typeof col === "string" ? record[col] : null;
835
+ return this.serializeValue(value);
836
+ });
837
+ const query = createSqlBuilder().insert(fullTableName, columns, values);
838
+ const { sql, params } = query.build();
839
+ await this.executeQuery({ sql, params });
840
+ }
841
+ }
842
+ this.logger.debug(
843
+ `Processed batch ${Math.floor(i / batchSize) + 1} of ${Math.ceil(records.length / batchSize)}`
844
+ );
845
+ }
846
+ this.logger.debug(`Successfully batch inserted ${records.length} records into ${tableName}`);
847
+ } catch (error) {
848
+ this.logger.error(`Error batch inserting into ${tableName}:`, {
849
+ message: error instanceof Error ? error.message : String(error)
850
+ });
851
+ throw new Error(`Failed to batch insert into ${tableName}: ${error}`);
852
+ }
853
+ }
854
+ async getTraces({
855
+ name,
856
+ scope,
857
+ page,
858
+ perPage,
859
+ attributes
860
+ }) {
861
+ const fullTableName = this.getTableName(TABLE_TRACES);
862
+ try {
863
+ const query = createSqlBuilder().select("*").from(fullTableName).where("1=1");
864
+ if (name) {
865
+ query.andWhere("name LIKE ?", `%${name}%`);
866
+ }
867
+ if (scope) {
868
+ query.andWhere("scope = ?", scope);
869
+ }
870
+ if (attributes && Object.keys(attributes).length > 0) {
871
+ for (const [key, value] of Object.entries(attributes)) {
872
+ query.jsonLike("attributes", key, value);
873
+ }
874
+ }
875
+ query.orderBy("startTime", "DESC").limit(perPage).offset((page - 1) * perPage);
876
+ const { sql, params } = query.build();
877
+ const results = await this.executeQuery({ sql, params });
878
+ return isArrayOfRecords(results) ? results.map((trace) => ({
879
+ ...trace,
880
+ attributes: this.deserializeValue(trace.attributes, "jsonb"),
881
+ status: this.deserializeValue(trace.status, "jsonb"),
882
+ events: this.deserializeValue(trace.events, "jsonb"),
883
+ links: this.deserializeValue(trace.links, "jsonb"),
884
+ other: this.deserializeValue(trace.other, "jsonb")
885
+ })) : [];
886
+ } catch (error) {
887
+ this.logger.error("Error getting traces:", { message: error instanceof Error ? error.message : String(error) });
888
+ return [];
889
+ }
890
+ }
891
+ async getEvalsByAgentName(agentName, type) {
892
+ const fullTableName = this.getTableName(TABLE_EVALS);
893
+ try {
894
+ let query = createSqlBuilder().select("*").from(fullTableName).where("agent_name = ?", agentName);
895
+ if (type === "test") {
896
+ query = query.andWhere("test_info IS NOT NULL AND json_extract(test_info, '$.testPath') IS NOT NULL");
897
+ } else if (type === "live") {
898
+ query = query.andWhere("(test_info IS NULL OR json_extract(test_info, '$.testPath') IS NULL)");
899
+ }
900
+ query.orderBy("created_at", "DESC");
901
+ const { sql, params } = query.build();
902
+ const results = await this.executeQuery({ sql, params });
903
+ return isArrayOfRecords(results) ? results.map((row) => {
904
+ const result = this.deserializeValue(row.result);
905
+ const testInfo = row.test_info ? this.deserializeValue(row.test_info) : void 0;
906
+ return {
907
+ input: row.input || "",
908
+ output: row.output || "",
909
+ result,
910
+ agentName: row.agent_name || "",
911
+ metricName: row.metric_name || "",
912
+ instructions: row.instructions || "",
913
+ runId: row.run_id || "",
914
+ globalRunId: row.global_run_id || "",
915
+ createdAt: row.created_at || "",
916
+ testInfo
917
+ };
918
+ }) : [];
919
+ } catch (error) {
920
+ this.logger.error(`Error getting evals for agent ${agentName}:`, {
921
+ message: error instanceof Error ? error.message : String(error)
922
+ });
923
+ return [];
924
+ }
925
+ }
926
+ getWorkflowRuns(_args) {
927
+ throw new Error("Method not implemented.");
928
+ }
929
+ /**
930
+ * Close the database connection
931
+ * No explicit cleanup needed for D1 in either REST or Workers Binding mode
932
+ */
933
+ async close() {
934
+ this.logger.debug("Closing D1 connection");
935
+ }
936
+ };
937
+
938
+ export { D1Store };