@axiom-lattice/local-stores 1.0.1

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.
@@ -0,0 +1,259 @@
1
+ /**
2
+ * Shared SQLite database connection for local stores using sql.js (WASM).
3
+ *
4
+ * sql.js is a pure JavaScript/WASM SQLite implementation that requires
5
+ * no native dependencies. The database is persisted to a file on disk.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * import { initDatabase, closeDatabase } from "@axiom-lattice/local-stores";
10
+ *
11
+ * await initDatabase({ dbPath: "~/.axiom/lattice.db" });
12
+ * // ... create stores using getDatabase() ...
13
+ * await closeDatabase();
14
+ * ```
15
+ */
16
+
17
+ import initSqlJs, { type Database as SqlJsDatabase, type SqlJsStatic, type BindParams } from "sql.js";
18
+ import * as path from "path";
19
+ import * as os from "os";
20
+ import * as fs from "fs";
21
+
22
+ export interface LocalStoreOptions {
23
+ /**
24
+ * Path to the SQLite database file.
25
+ * Supports `~` for home directory expansion.
26
+ * @default "~/.axiom/lattice.db"
27
+ */
28
+ dbPath?: string;
29
+ }
30
+
31
+ let _SQL: SqlJsStatic | null = null;
32
+ let _db: DatabaseWrapper | null = null;
33
+
34
+ function expandHome(filePath: string): string {
35
+ if (filePath.startsWith("~")) {
36
+ return path.join(os.homedir(), filePath.slice(1));
37
+ }
38
+ return filePath;
39
+ }
40
+
41
+ /**
42
+ * A thin wrapper around sql.js that provides an API similar to better-sqlite3.
43
+ */
44
+ export class DatabaseWrapper {
45
+ private db: SqlJsDatabase;
46
+ private dbPath: string;
47
+
48
+ constructor(sql: SqlJsStatic, dbPath: string) {
49
+ this.dbPath = dbPath;
50
+ if (fs.existsSync(dbPath)) {
51
+ const buffer = fs.readFileSync(dbPath);
52
+ this.db = new sql.Database(buffer);
53
+ } else {
54
+ this.db = new sql.Database();
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Execute a SQL statement and return the wrapper for chaining (run).
60
+ * Automatically persists changes to disk.
61
+ */
62
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
63
+ run(sql: string, ...params: any[]): RunResult {
64
+ this.db.run(sql, params);
65
+ this.save();
66
+ return new RunResult(this.db);
67
+ }
68
+
69
+ /**
70
+ * Prepare and execute a query returning all matching rows as objects.
71
+ */
72
+ prepare(sql: string): StatementWrapper {
73
+ return new StatementWrapper(this.db, sql, this);
74
+ }
75
+
76
+ /**
77
+ * Execute raw SQL (for DDL statements).
78
+ */
79
+ exec(sql: string): void {
80
+ this.db.exec(sql);
81
+ }
82
+
83
+ /**
84
+ * Persist the database to disk.
85
+ */
86
+ save(): void {
87
+ const data = this.db.export();
88
+ const buffer = Buffer.from(data);
89
+ fs.writeFileSync(this.dbPath, buffer);
90
+ }
91
+
92
+ /**
93
+ * Close the database.
94
+ */
95
+ close(): void {
96
+ this.db.close();
97
+ }
98
+
99
+ /**
100
+ * Get the underlying sql.js database instance.
101
+ */
102
+ getRawDb(): SqlJsDatabase {
103
+ return this.db;
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Wraps a prepared statement with a better-sqlite3-like API.
109
+ */
110
+ export class StatementWrapper {
111
+ private db: SqlJsDatabase;
112
+ private sql: string;
113
+ private parent: DatabaseWrapper | null;
114
+
115
+ constructor(db: SqlJsDatabase, sql: string, parent?: DatabaseWrapper) {
116
+ this.db = db;
117
+ this.sql = sql;
118
+ this.parent = parent || null;
119
+ }
120
+
121
+ /**
122
+ * Execute and return all rows as objects.
123
+ */
124
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
125
+ all(...params: any[]): unknown[] {
126
+ const stmt = this.db.prepare(this.sql);
127
+ try {
128
+ if (params.length > 0) {
129
+ stmt.bind(params as BindParams);
130
+ }
131
+ const rows: Record<string, unknown>[] = [];
132
+ while (stmt.step()) {
133
+ rows.push(stmt.getAsObject());
134
+ }
135
+ return rows;
136
+ } finally {
137
+ stmt.free();
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Execute and return the first row as an object, or undefined.
143
+ */
144
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
145
+ get(...params: any[]): unknown {
146
+ const stmt = this.db.prepare(this.sql);
147
+ try {
148
+ if (params.length > 0) {
149
+ stmt.bind(params as BindParams);
150
+ }
151
+ if (stmt.step()) {
152
+ return stmt.getAsObject();
153
+ }
154
+ return undefined;
155
+ } finally {
156
+ stmt.free();
157
+ }
158
+ }
159
+
160
+ /**
161
+ * Execute without returning rows (INSERT/UPDATE/DELETE).
162
+ * Automatically persists changes to disk.
163
+ */
164
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
165
+ run(...params: any[]): RunResult {
166
+ this.db.run(this.sql, params as BindParams);
167
+ if (this.parent) {
168
+ this.parent.save();
169
+ }
170
+ return new RunResult(this.db);
171
+ }
172
+ }
173
+
174
+ /**
175
+ * Result of a run operation.
176
+ */
177
+ export class RunResult {
178
+ private db: SqlJsDatabase;
179
+
180
+ constructor(db: SqlJsDatabase) {
181
+ this.db = db;
182
+ }
183
+
184
+ /** Number of rows modified by the last INSERT/UPDATE/DELETE. */
185
+ get changes(): number {
186
+ return this.db.getRowsModified();
187
+ }
188
+ }
189
+
190
+ /**
191
+ * Initialize the shared SQLite database.
192
+ * Must be called (and awaited) before using any stores.
193
+ */
194
+ export async function initDatabase(options: LocalStoreOptions = {}): Promise<DatabaseWrapper> {
195
+ if (_db) return _db;
196
+
197
+ if (!_SQL) {
198
+ _SQL = await initSqlJs();
199
+ }
200
+
201
+ const rawPath = options.dbPath || "~/.axiom/lattice.db";
202
+ const dbPath = expandHome(rawPath);
203
+
204
+ // Ensure parent directory exists
205
+ const dir = path.dirname(dbPath);
206
+ if (!fs.existsSync(dir)) {
207
+ fs.mkdirSync(dir, { recursive: true });
208
+ }
209
+
210
+ _db = new DatabaseWrapper(_SQL, dbPath);
211
+ _db.exec("PRAGMA journal_mode = WAL;");
212
+ _db.exec("PRAGMA foreign_keys = ON;");
213
+
214
+ return _db;
215
+ }
216
+
217
+ /**
218
+ * Get the shared database instance.
219
+ * Must be called after `await initDatabase()`.
220
+ */
221
+ export function getDatabase(): DatabaseWrapper {
222
+ if (!_db) {
223
+ throw new Error("Database not initialized. Call await initDatabase() first.");
224
+ }
225
+ return _db;
226
+ }
227
+
228
+ /**
229
+ * Persist and close the database connection.
230
+ */
231
+ export function closeDatabase(): void {
232
+ if (_db) {
233
+ _db.save();
234
+ _db.close();
235
+ _db = null;
236
+ }
237
+ }
238
+
239
+ /**
240
+ * Execute a CREATE TABLE IF NOT EXISTS statement.
241
+ * Helper for idempotent schema initialization.
242
+ */
243
+ export function ensureTable(db: DatabaseWrapper, ddl: string): void {
244
+ db.exec(ddl);
245
+ }
246
+
247
+ /**
248
+ * ISO timestamp helper — returns current time as ISO string.
249
+ */
250
+ export function nowISO(): string {
251
+ return new Date().toISOString();
252
+ }
253
+
254
+ /**
255
+ * Parse an ISO timestamp string back to a Date object.
256
+ */
257
+ export function parseISO(iso: string): Date {
258
+ return new Date(iso);
259
+ }
package/src/index.ts ADDED
@@ -0,0 +1,44 @@
1
+ /**
2
+ * @axiom-lattice/local-stores
3
+ *
4
+ * Local SQLite-based store implementations for the Axiom Lattice framework.
5
+ * All stores share a single SQLite database file for zero-config persistence.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * import { createLocalStoreConfig, closeDatabase } from "@axiom-lattice/local-stores";
10
+ * import { configureStores } from "@axiom-lattice/core";
11
+ *
12
+ * const stores = await createLocalStoreConfig({ dbPath: "./data/lattice.db" });
13
+ * const dispose = await configureStores(stores);
14
+ *
15
+ * // On shutdown:
16
+ * await dispose();
17
+ * closeDatabase();
18
+ * ```
19
+ */
20
+
21
+ export { initDatabase, getDatabase, closeDatabase, ensureTable, nowISO, parseISO, DatabaseWrapper, StatementWrapper, RunResult } from "./database";
22
+ export type { LocalStoreOptions } from "./database";
23
+
24
+ export { createLocalStoreConfig } from "./createLocalStoreConfig";
25
+ export type { LocalStoreConfigOptions } from "./createLocalStoreConfig";
26
+
27
+ export { LocalThreadStore } from "./stores/LocalThreadStore";
28
+ export { LocalAssistantStore } from "./stores/LocalAssistantStore";
29
+ export { LocalWorkspaceStore } from "./stores/LocalWorkspaceStore";
30
+ export { LocalProjectStore } from "./stores/LocalProjectStore";
31
+ export { LocalUserStore } from "./stores/LocalUserStore";
32
+ export { LocalTenantStore } from "./stores/LocalTenantStore";
33
+ export { LocalUserTenantLinkStore } from "./stores/LocalUserTenantLinkStore";
34
+ export { LocalDatabaseConfigStore } from "./stores/LocalDatabaseConfigStore";
35
+ export { LocalMetricsServerConfigStore } from "./stores/LocalMetricsServerConfigStore";
36
+ export { LocalMcpServerConfigStore } from "./stores/LocalMcpServerConfigStore";
37
+ export { LocalWorkflowTrackingStore } from "./stores/LocalWorkflowTrackingStore";
38
+ export { LocalEvalStore } from "./stores/LocalEvalStore";
39
+ export { LocalChannelBindingStore } from "./stores/LocalChannelBindingStore";
40
+ export { LocalChannelInstallationStore } from "./stores/LocalChannelInstallationStore";
41
+ export { LocalA2AApiKeyStore } from "./stores/LocalA2AApiKeyStore";
42
+ export { LocalThreadMessageQueueStore } from "./stores/LocalThreadMessageQueueStore";
43
+ export { LocalSkillStore } from "./stores/LocalSkillStore";
44
+ export { LocalScheduleStorage } from "./stores/LocalScheduleStorage";
@@ -0,0 +1,177 @@
1
+ /**
2
+ * Local SQLite implementation of A2AApiKeyStore.
3
+ */
4
+
5
+ import { DatabaseWrapper } from "../database";
6
+ import type {
7
+ A2AApiKeyStore,
8
+ A2AApiKeyRecord,
9
+ CreateA2AApiKeyInput,
10
+ A2AApiKeyEntry,
11
+ } from "@axiom-lattice/protocols";
12
+ import { ensureTable, nowISO, parseISO } from "../database";
13
+ import { encrypt, decrypt } from "@axiom-lattice/core";
14
+ import { randomUUID } from "crypto";
15
+
16
+ const DDL = `
17
+ CREATE TABLE IF NOT EXISTS lt_a2a_api_keys (
18
+ id TEXT PRIMARY KEY,
19
+ key_value TEXT NOT NULL,
20
+ tenant_id TEXT NOT NULL,
21
+ project_id TEXT,
22
+ workspace_id TEXT,
23
+ label TEXT,
24
+ enabled INTEGER NOT NULL DEFAULT 1,
25
+ created_at TEXT NOT NULL,
26
+ updated_at TEXT NOT NULL
27
+ );
28
+ `;
29
+
30
+ interface KeyRow {
31
+ id: string;
32
+ key_value: string;
33
+ tenant_id: string;
34
+ project_id: string | null;
35
+ workspace_id: string | null;
36
+ label: string | null;
37
+ enabled: number;
38
+ created_at: string;
39
+ updated_at: string;
40
+ }
41
+
42
+ function generateApiKey(): string {
43
+ return `a2a_${randomUUID().replace(/-/g, "")}`;
44
+ }
45
+
46
+ export class LocalA2AApiKeyStore implements A2AApiKeyStore {
47
+ private db: DatabaseWrapper;
48
+
49
+ constructor(db: DatabaseWrapper) {
50
+ this.db = db;
51
+ ensureTable(db, DDL);
52
+ }
53
+
54
+ async findByKey(key: string): Promise<A2AApiKeyRecord | null> {
55
+ const rows = this.db.prepare(
56
+ `SELECT * FROM lt_a2a_api_keys WHERE enabled = 1`,
57
+ ).all() as unknown as KeyRow[];
58
+ for (const row of rows) {
59
+ try {
60
+ if (decrypt(row.key_value) === key) return mapRowToRecord(row);
61
+ } catch { /* skip unreadable */ }
62
+ }
63
+ return null;
64
+ }
65
+
66
+ async list(params: { tenantId?: string; limit?: number; offset?: number }): Promise<A2AApiKeyRecord[]> {
67
+ const limit = params.limit || 100;
68
+ const offset = params.offset || 0;
69
+ let rows: KeyRow[];
70
+ if (params.tenantId) {
71
+ rows = this.db.prepare(
72
+ `SELECT * FROM lt_a2a_api_keys WHERE tenant_id = ? ORDER BY created_at DESC LIMIT ? OFFSET ?`,
73
+ ).all(params.tenantId, limit, offset) as unknown as KeyRow[];
74
+ } else {
75
+ rows = this.db.prepare(
76
+ `SELECT * FROM lt_a2a_api_keys ORDER BY created_at DESC LIMIT ? OFFSET ?`,
77
+ ).all(limit, offset) as unknown as KeyRow[];
78
+ }
79
+ return rows.map(mapRowToRecord);
80
+ }
81
+
82
+ async create(input: CreateA2AApiKeyInput): Promise<A2AApiKeyRecord> {
83
+ const key = generateApiKey();
84
+ const now = nowISO();
85
+ this.db.prepare(
86
+ `INSERT INTO lt_a2a_api_keys (key_value, tenant_id, project_id, workspace_id, label, created_at, updated_at)
87
+ VALUES (?, ?, ?, ?, ?, ?, ?)`,
88
+ ).run(encrypt(key), input.tenantId, input.projectId || null, input.workspaceId || null, input.label || null, now, now);
89
+
90
+ const lastId = this.db.prepare(`SELECT last_insert_rowid() as id`).get() as { id: number };
91
+
92
+ const record: A2AApiKeyRecord = {
93
+ id: String(lastId.id),
94
+ key,
95
+ tenantId: input.tenantId,
96
+ projectId: input.projectId,
97
+ workspaceId: input.workspaceId,
98
+ label: input.label,
99
+ enabled: true,
100
+ createdAt: parseISO(now),
101
+ updatedAt: parseISO(now),
102
+ };
103
+ return record;
104
+ }
105
+
106
+ async disable(id: string): Promise<A2AApiKeyRecord> {
107
+ const now = nowISO();
108
+ this.db.prepare(
109
+ `UPDATE lt_a2a_api_keys SET enabled = 0, updated_at = ? WHERE id = ?`,
110
+ ).run(now, id);
111
+ const row = this.db.prepare(`SELECT * FROM lt_a2a_api_keys WHERE id = ?`).get(id) as unknown as KeyRow;
112
+ if (!row) throw new Error(`A2A API key not found: ${id}`);
113
+ return mapRowToRecord(row);
114
+ }
115
+
116
+ async enable(id: string): Promise<A2AApiKeyRecord> {
117
+ const now = nowISO();
118
+ this.db.prepare(
119
+ `UPDATE lt_a2a_api_keys SET enabled = 1, updated_at = ? WHERE id = ?`,
120
+ ).run(now, id);
121
+ const row = this.db.prepare(`SELECT * FROM lt_a2a_api_keys WHERE id = ?`).get(id) as unknown as KeyRow;
122
+ if (!row) throw new Error(`A2A API key not found: ${id}`);
123
+ return mapRowToRecord(row);
124
+ }
125
+
126
+ async rotate(id: string): Promise<A2AApiKeyRecord> {
127
+ const key = generateApiKey();
128
+ const now = nowISO();
129
+ this.db.prepare(
130
+ `UPDATE lt_a2a_api_keys SET key_value = ?, updated_at = ? WHERE id = ?`,
131
+ ).run(encrypt(key), now, id);
132
+ const row = this.db.prepare(`SELECT * FROM lt_a2a_api_keys WHERE id = ?`).get(id) as unknown as KeyRow;
133
+ if (!row) throw new Error(`A2A API key not found: ${id}`);
134
+ const record = mapRowToRecord(row);
135
+ record.key = key;
136
+ return record;
137
+ }
138
+
139
+ async delete(id: string): Promise<void> {
140
+ this.db.prepare(`DELETE FROM lt_a2a_api_keys WHERE id = ?`).run(id);
141
+ }
142
+
143
+ async loadIntoMap(): Promise<Map<string, A2AApiKeyEntry>> {
144
+ const rows = this.db.prepare(
145
+ `SELECT * FROM lt_a2a_api_keys WHERE enabled = 1`,
146
+ ).all() as unknown as KeyRow[];
147
+ const map = new Map<string, A2AApiKeyEntry>();
148
+ for (const row of rows) {
149
+ try {
150
+ const key = decrypt(row.key_value);
151
+ map.set(key, {
152
+ key,
153
+ tenantId: row.tenant_id,
154
+ projectId: row.project_id || undefined,
155
+ workspaceId: row.workspace_id || undefined,
156
+ });
157
+ } catch { /* skip */ }
158
+ }
159
+ return map;
160
+ }
161
+ }
162
+
163
+ function mapRowToRecord(row: KeyRow): A2AApiKeyRecord {
164
+ let key = "";
165
+ try { key = decrypt(row.key_value); } catch { key = row.key_value; }
166
+ return {
167
+ id: row.id,
168
+ key,
169
+ tenantId: row.tenant_id,
170
+ projectId: row.project_id || undefined,
171
+ workspaceId: row.workspace_id || undefined,
172
+ label: row.label || undefined,
173
+ enabled: row.enabled === 1,
174
+ createdAt: parseISO(row.created_at),
175
+ updatedAt: parseISO(row.updated_at),
176
+ };
177
+ }
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Local SQLite implementation of AssistantStore.
3
+ */
4
+
5
+ import { DatabaseWrapper } from "../database";
6
+ import type {
7
+ AssistantStore,
8
+ Assistant,
9
+ CreateAssistantRequest,
10
+ } from "@axiom-lattice/protocols";
11
+ import { ensureTable, nowISO, parseISO } from "../database";
12
+
13
+ const DDL = `
14
+ CREATE TABLE IF NOT EXISTS lt_assistants (
15
+ id TEXT NOT NULL,
16
+ tenant_id TEXT NOT NULL,
17
+ name TEXT NOT NULL,
18
+ description TEXT,
19
+ graph_definition TEXT NOT NULL,
20
+ created_at TEXT NOT NULL,
21
+ updated_at TEXT NOT NULL,
22
+ PRIMARY KEY (tenant_id, id)
23
+ );
24
+ `;
25
+
26
+ interface AssistantRow {
27
+ id: string;
28
+ tenant_id: string;
29
+ name: string;
30
+ description: string | null;
31
+ graph_definition: string;
32
+ created_at: string;
33
+ updated_at: string;
34
+ }
35
+
36
+ export class LocalAssistantStore implements AssistantStore {
37
+ private db: DatabaseWrapper;
38
+
39
+ constructor(db: DatabaseWrapper) {
40
+ this.db = db;
41
+ ensureTable(db, DDL);
42
+ }
43
+
44
+ async getAllAssistants(tenantId: string): Promise<Assistant[]> {
45
+ const rows = this.db.prepare(
46
+ `SELECT * FROM lt_assistants WHERE tenant_id = ? ORDER BY created_at DESC`,
47
+ ).all(tenantId) as unknown as AssistantRow[];
48
+ return rows.map(mapRowToAssistant);
49
+ }
50
+
51
+ async getAssistantById(tenantId: string, id: string): Promise<Assistant | null> {
52
+ const row = this.db.prepare(
53
+ `SELECT * FROM lt_assistants WHERE tenant_id = ? AND id = ?`,
54
+ ).get(tenantId, id) as unknown as AssistantRow | undefined;
55
+ return row ? mapRowToAssistant(row) : null;
56
+ }
57
+
58
+ async createAssistant(
59
+ tenantId: string,
60
+ id: string,
61
+ data: CreateAssistantRequest,
62
+ ): Promise<Assistant> {
63
+ const now = nowISO();
64
+
65
+ this.db.prepare(
66
+ `INSERT INTO lt_assistants (id, tenant_id, name, description, graph_definition, created_at, updated_at)
67
+ VALUES (?, ?, ?, ?, ?, ?, ?)
68
+ ON CONFLICT(tenant_id, id) DO UPDATE SET
69
+ name = excluded.name,
70
+ description = excluded.description,
71
+ graph_definition = excluded.graph_definition,
72
+ updated_at = excluded.updated_at`,
73
+ ).run(id, tenantId, data.name, data.description || null, JSON.stringify(data.graphDefinition), now, now);
74
+
75
+ return {
76
+ id,
77
+ tenantId,
78
+ name: data.name,
79
+ description: data.description,
80
+ graphDefinition: data.graphDefinition,
81
+ createdAt: parseISO(now),
82
+ updatedAt: parseISO(now),
83
+ };
84
+ }
85
+
86
+ async updateAssistant(
87
+ tenantId: string,
88
+ id: string,
89
+ updates: Partial<CreateAssistantRequest>,
90
+ ): Promise<Assistant | null> {
91
+ const existing = await this.getAssistantById(tenantId, id);
92
+ if (!existing) return null;
93
+
94
+ const setClauses: string[] = [];
95
+ const values: unknown[] = [];
96
+
97
+ if (updates.name !== undefined) {
98
+ setClauses.push("name = ?");
99
+ values.push(updates.name);
100
+ }
101
+ if (updates.description !== undefined) {
102
+ setClauses.push("description = ?");
103
+ values.push(updates.description || null);
104
+ }
105
+ if (updates.graphDefinition !== undefined) {
106
+ setClauses.push("graph_definition = ?");
107
+ values.push(JSON.stringify(updates.graphDefinition));
108
+ }
109
+
110
+ if (setClauses.length === 0) return existing;
111
+
112
+ const now = nowISO();
113
+ setClauses.push("updated_at = ?");
114
+ values.push(now);
115
+ values.push(tenantId, id);
116
+
117
+ this.db.prepare(
118
+ `UPDATE lt_assistants SET ${setClauses.join(", ")} WHERE tenant_id = ? AND id = ?`,
119
+ ).run(...values);
120
+
121
+ return this.getAssistantById(tenantId, id);
122
+ }
123
+
124
+ async deleteAssistant(tenantId: string, id: string): Promise<boolean> {
125
+ const result = this.db.prepare(
126
+ `DELETE FROM lt_assistants WHERE tenant_id = ? AND id = ?`,
127
+ ).run(tenantId, id);
128
+ return result.changes > 0;
129
+ }
130
+
131
+ async hasAssistant(tenantId: string, id: string): Promise<boolean> {
132
+ const row = this.db.prepare(
133
+ `SELECT 1 FROM lt_assistants WHERE tenant_id = ? AND id = ? LIMIT 1`,
134
+ ).get(tenantId, id);
135
+ return row !== undefined;
136
+ }
137
+ }
138
+
139
+ function mapRowToAssistant(row: AssistantRow): Assistant {
140
+ return {
141
+ id: row.id,
142
+ tenantId: row.tenant_id,
143
+ name: row.name,
144
+ description: row.description || undefined,
145
+ graphDefinition: JSON.parse(row.graph_definition),
146
+ createdAt: parseISO(row.created_at),
147
+ updatedAt: parseISO(row.updated_at),
148
+ };
149
+ }