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