routeflow-api 1.0.1 → 1.0.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/sqlite.cjs CHANGED
@@ -20,6 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/sqlite.ts
21
21
  var sqlite_exports = {};
22
22
  __export(sqlite_exports, {
23
+ RouteStore: () => RouteStore,
23
24
  SQLiteStore: () => SQLiteStore
24
25
  });
25
26
  module.exports = __toCommonJS(sqlite_exports);
@@ -53,8 +54,220 @@ var SQLiteStore = class {
53
54
  this.db.close();
54
55
  }
55
56
  };
57
+
58
+ // src/core/adapter/route-store.ts
59
+ var RouteTable = class {
60
+ constructor(db, tableName, schema, emit) {
61
+ this.db = db;
62
+ this.tableName = tableName;
63
+ this.schema = schema;
64
+ this.emit = emit;
65
+ this.jsonCols = new Set(
66
+ Object.entries(schema).filter(([, t]) => t === "json").map(([k]) => k)
67
+ );
68
+ this.createTable();
69
+ }
70
+ db;
71
+ tableName;
72
+ schema;
73
+ emit;
74
+ jsonCols;
75
+ // ── Schema bootstrap ──────────────────────────────────────────────────────
76
+ createTable() {
77
+ const cols = Object.entries(this.schema).map(([col, type]) => {
78
+ const sqlType = type === "json" ? "TEXT" : type.toUpperCase();
79
+ return `${col} ${sqlType}`;
80
+ }).join(", ");
81
+ this.db.exec(
82
+ `CREATE TABLE IF NOT EXISTS "${this.tableName}" (id INTEGER PRIMARY KEY AUTOINCREMENT, ${cols})`
83
+ );
84
+ }
85
+ // ── Serialization helpers ─────────────────────────────────────────────────
86
+ serialize(data) {
87
+ const out = {};
88
+ for (const [k, v] of Object.entries(data)) {
89
+ out[k] = this.jsonCols.has(k) ? JSON.stringify(v) : v;
90
+ }
91
+ return out;
92
+ }
93
+ deserialize(row) {
94
+ const out = {};
95
+ for (const [k, v] of Object.entries(row)) {
96
+ out[k] = this.jsonCols.has(k) && typeof v === "string" ? JSON.parse(v) : v;
97
+ }
98
+ return out;
99
+ }
100
+ // ── CRUD ──────────────────────────────────────────────────────────────────
101
+ /**
102
+ * Return all rows, with optional filtering, sorting, and limiting.
103
+ *
104
+ * @example
105
+ * ```ts
106
+ * await items.list()
107
+ * await items.list({ where: { status: 'active' }, orderBy: 'createdAt', order: 'desc', limit: 10 })
108
+ * ```
109
+ */
110
+ async list(options = {}) {
111
+ const { where, orderBy = "id", order = "asc", limit } = options;
112
+ const parts = [];
113
+ const values = [];
114
+ if (where) {
115
+ for (const [k, v] of Object.entries(where)) {
116
+ parts.push(`${k} = ?`);
117
+ values.push(this.jsonCols.has(k) ? JSON.stringify(v) : v);
118
+ }
119
+ }
120
+ const whereClause = parts.length ? `WHERE ${parts.join(" AND ")}` : "";
121
+ const orderClause = `ORDER BY ${String(orderBy)} ${order.toUpperCase()}`;
122
+ const limitClause = limit != null ? `LIMIT ${limit}` : "";
123
+ const sql = `SELECT * FROM "${this.tableName}" ${whereClause} ${orderClause} ${limitClause}`.trim();
124
+ const rows = this.db.prepare(sql).all(...values);
125
+ return rows.map((r) => this.deserialize(r));
126
+ }
127
+ /**
128
+ * Return a single row by `id`, or `null` if not found.
129
+ *
130
+ * @example
131
+ * ```ts
132
+ * const item = await items.get(1)
133
+ * ```
134
+ */
135
+ async get(id) {
136
+ const row = this.db.prepare(`SELECT * FROM "${this.tableName}" WHERE id = ?`).get(id);
137
+ return row ? this.deserialize(row) : null;
138
+ }
139
+ /**
140
+ * Insert a new row and emit an `INSERT` change event.
141
+ *
142
+ * @example
143
+ * ```ts
144
+ * const item = await items.create({ name: 'Apple', createdAt: new Date().toISOString() })
145
+ * ```
146
+ */
147
+ async create(data) {
148
+ const serialized = this.serialize(data);
149
+ const cols = Object.keys(serialized).join(", ");
150
+ const placeholders = Object.keys(serialized).map(() => "?").join(", ");
151
+ const result = this.db.prepare(`INSERT INTO "${this.tableName}" (${cols}) VALUES (${placeholders})`).run(
152
+ ...Object.values(serialized)
153
+ );
154
+ const created = await this.get(result.lastInsertRowid);
155
+ this.emit({ table: this.tableName, operation: "INSERT", newRow: created, oldRow: null, timestamp: Date.now() });
156
+ return created;
157
+ }
158
+ /**
159
+ * Update columns on an existing row and emit an `UPDATE` change event.
160
+ * Returns the updated row, or `null` if the `id` does not exist.
161
+ *
162
+ * @example
163
+ * ```ts
164
+ * const updated = await items.update(1, { name: 'Mango' })
165
+ * ```
166
+ */
167
+ async update(id, data) {
168
+ const old = await this.get(id);
169
+ if (!old) return null;
170
+ const serialized = this.serialize(data);
171
+ const setClauses = Object.keys(serialized).map((k) => `${k} = ?`).join(", ");
172
+ this.db.prepare(`UPDATE "${this.tableName}" SET ${setClauses} WHERE id = ?`).run(
173
+ ...Object.values(serialized),
174
+ id
175
+ );
176
+ const updated = await this.get(id);
177
+ this.emit({ table: this.tableName, operation: "UPDATE", newRow: updated, oldRow: old, timestamp: Date.now() });
178
+ return updated;
179
+ }
180
+ /**
181
+ * Delete a row by `id` and emit a `DELETE` change event.
182
+ * Returns `true` if a row was deleted, `false` if `id` was not found.
183
+ *
184
+ * @example
185
+ * ```ts
186
+ * await items.delete(1)
187
+ * ```
188
+ */
189
+ async delete(id) {
190
+ const old = await this.get(id);
191
+ if (!old) return false;
192
+ this.db.prepare(`DELETE FROM "${this.tableName}" WHERE id = ?`).run(id);
193
+ this.emit({ table: this.tableName, operation: "DELETE", newRow: null, oldRow: old, timestamp: Date.now() });
194
+ return true;
195
+ }
196
+ /**
197
+ * Seed the table with initial rows if it is empty.
198
+ *
199
+ * @example
200
+ * ```ts
201
+ * await items.seed([
202
+ * { name: 'Apple', createdAt: '2026-01-01T00:00:00.000Z' },
203
+ * ])
204
+ * ```
205
+ */
206
+ async seed(rows) {
207
+ const count = this.db.prepare(`SELECT COUNT(*) as n FROM "${this.tableName}"`).get().n;
208
+ if (count > 0) return;
209
+ for (const row of rows) {
210
+ await this.create(row);
211
+ }
212
+ }
213
+ };
214
+ var RouteStore = class {
215
+ sql;
216
+ listeners = /* @__PURE__ */ new Map();
217
+ connected = false;
218
+ /**
219
+ * @param dbPath - Path to the SQLite file.
220
+ * Parent directories are created automatically.
221
+ * Relative paths resolve from `process.cwd()`.
222
+ */
223
+ constructor(dbPath) {
224
+ this.sql = new SQLiteStore(dbPath);
225
+ }
226
+ /**
227
+ * Define a table and return a typed CRUD handle.
228
+ * The table is created in SQLite automatically if it does not exist.
229
+ *
230
+ * @param name - Table name
231
+ * @param schema - Column definitions: `{ columnName: 'integer' | 'text' | 'real' | 'json' }`
232
+ *
233
+ * @example
234
+ * ```ts
235
+ * const tasks = db.table('tasks', {
236
+ * title: 'text',
237
+ * done: 'integer', // 0 | 1
238
+ * metadata: 'json', // auto-serialised JS object
239
+ * })
240
+ * ```
241
+ */
242
+ table(name, schema) {
243
+ return new RouteTable(this.sql, name, schema, (evt) => this.dispatchEvent(evt));
244
+ }
245
+ // ── DatabaseAdapter ───────────────────────────────────────────────────────
246
+ async connect() {
247
+ this.connected = true;
248
+ }
249
+ async disconnect() {
250
+ this.sql.close();
251
+ this.connected = false;
252
+ }
253
+ onChange(table, callback) {
254
+ if (!this.listeners.has(table)) this.listeners.set(table, /* @__PURE__ */ new Set());
255
+ this.listeners.get(table).add(callback);
256
+ return () => this.listeners.get(table)?.delete(callback);
257
+ }
258
+ get isConnected() {
259
+ return this.connected;
260
+ }
261
+ // ── Internal ──────────────────────────────────────────────────────────────
262
+ dispatchEvent(event) {
263
+ const cbs = this.listeners.get(event.table);
264
+ if (!cbs) return;
265
+ for (const cb of cbs) cb(event);
266
+ }
267
+ };
56
268
  // Annotate the CommonJS export names for ESM import in node:
57
269
  0 && (module.exports = {
270
+ RouteStore,
58
271
  SQLiteStore
59
272
  });
60
273
  //# sourceMappingURL=sqlite.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/sqlite.ts","../src/core/adapter/sqlite-store.ts"],"sourcesContent":["export { SQLiteStore } from './core/adapter/sqlite-store.js'\nexport type { SqliteRow } from './core/adapter/sqlite-store.js'\n","import { DatabaseSync } from 'node:sqlite'\nimport { mkdirSync } from 'node:fs'\nimport { dirname, resolve } from 'node:path'\n\n/**\n * A generic record type for SQLite rows.\n * Keys map to column names; values are SQLite-compatible primitives.\n */\nexport type SqliteRow = Record<string, string | number | null>\n\n/**\n * File-based key-value store backed by Node's built-in `node:sqlite`.\n * Requires Node.js 22.5+.\n *\n * Data survives server restarts because it is written to a file on disk.\n * The path can be set directly in code — no environment variable required.\n *\n * @example\n * ```ts\n * const store = new SQLiteStore('./data/app.db')\n * store.exec(`CREATE TABLE IF NOT EXISTS items (id INTEGER PRIMARY KEY, name TEXT)`)\n * store.prepare('INSERT INTO items (name) VALUES (?)').run('Apple')\n * const rows = store.prepare('SELECT * FROM items').all()\n * ```\n */\nexport class SQLiteStore {\n readonly db: DatabaseSync\n readonly path: string\n\n /**\n * @param dbPath - Path to the SQLite file. Parent directories are created automatically.\n * Relative paths are resolved from `process.cwd()`.\n */\n constructor(dbPath: string) {\n this.path = resolve(dbPath)\n mkdirSync(dirname(this.path), { recursive: true })\n this.db = new DatabaseSync(this.path)\n }\n\n /** Execute one or more SQL statements (no return value). */\n exec(sql: string): void {\n this.db.exec(sql)\n }\n\n /** Prepare a statement for repeated use. */\n prepare(sql: string): ReturnType<DatabaseSync['prepare']> {\n return this.db.prepare(sql)\n }\n\n /** Close the database connection. */\n close(): void {\n this.db.close()\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,yBAA6B;AAC7B,qBAA0B;AAC1B,uBAAiC;AAuB1B,IAAM,cAAN,MAAkB;AAAA,EACd;AAAA,EACA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMT,YAAY,QAAgB;AAC1B,SAAK,WAAO,0BAAQ,MAAM;AAC1B,sCAAU,0BAAQ,KAAK,IAAI,GAAG,EAAE,WAAW,KAAK,CAAC;AACjD,SAAK,KAAK,IAAI,gCAAa,KAAK,IAAI;AAAA,EACtC;AAAA;AAAA,EAGA,KAAK,KAAmB;AACtB,SAAK,GAAG,KAAK,GAAG;AAAA,EAClB;AAAA;AAAA,EAGA,QAAQ,KAAkD;AACxD,WAAO,KAAK,GAAG,QAAQ,GAAG;AAAA,EAC5B;AAAA;AAAA,EAGA,QAAc;AACZ,SAAK,GAAG,MAAM;AAAA,EAChB;AACF;","names":[]}
1
+ {"version":3,"sources":["../src/sqlite.ts","../src/core/adapter/sqlite-store.ts","../src/core/adapter/route-store.ts"],"sourcesContent":["export { RouteStore } from './core/adapter/route-store.js'\nexport type { SchemaDefinition, ColumnType, InferRow, ListOptions } from './core/adapter/route-store.js'\n\n// Low-level SQL access (advanced use)\nexport { SQLiteStore } from './core/adapter/sqlite-store.js'\nexport type { SqliteRow } from './core/adapter/sqlite-store.js'\n","import { DatabaseSync } from 'node:sqlite'\nimport { mkdirSync } from 'node:fs'\nimport { dirname, resolve } from 'node:path'\n\n/**\n * A generic record type for SQLite rows.\n * Keys map to column names; values are SQLite-compatible primitives.\n */\nexport type SqliteRow = Record<string, string | number | null>\n\n/**\n * File-based key-value store backed by Node's built-in `node:sqlite`.\n * Requires Node.js 22.5+.\n *\n * Data survives server restarts because it is written to a file on disk.\n * The path can be set directly in code — no environment variable required.\n *\n * @example\n * ```ts\n * const store = new SQLiteStore('./data/app.db')\n * store.exec(`CREATE TABLE IF NOT EXISTS items (id INTEGER PRIMARY KEY, name TEXT)`)\n * store.prepare('INSERT INTO items (name) VALUES (?)').run('Apple')\n * const rows = store.prepare('SELECT * FROM items').all()\n * ```\n */\nexport class SQLiteStore {\n readonly db: DatabaseSync\n readonly path: string\n\n /**\n * @param dbPath - Path to the SQLite file. Parent directories are created automatically.\n * Relative paths are resolved from `process.cwd()`.\n */\n constructor(dbPath: string) {\n this.path = resolve(dbPath)\n mkdirSync(dirname(this.path), { recursive: true })\n this.db = new DatabaseSync(this.path)\n }\n\n /** Execute one or more SQL statements (no return value). */\n exec(sql: string): void {\n this.db.exec(sql)\n }\n\n /** Prepare a statement for repeated use. */\n prepare(sql: string): ReturnType<DatabaseSync['prepare']> {\n return this.db.prepare(sql)\n }\n\n /** Close the database connection. */\n close(): void {\n this.db.close()\n }\n}\n","import type { ChangeEvent, DatabaseAdapter } from '../types.js'\nimport { SQLiteStore } from './sqlite-store.js'\n\n// ────────────────────────────────────────────────────────────────────────────\n// Schema definition types\n// ────────────────────────────────────────────────────────────────────────────\n\n/** Supported column types for RouteStore tables. */\nexport type ColumnType = 'integer' | 'text' | 'real' | 'json'\n\n/** Schema definition: maps column names to their types. */\nexport type SchemaDefinition = Record<string, ColumnType>\n\n/** TypeScript row type inferred from a schema definition. */\nexport type InferRow<S extends SchemaDefinition> = {\n id: number\n} & {\n [K in keyof S]: S[K] extends 'integer'\n ? number\n : S[K] extends 'real'\n ? number\n : S[K] extends 'json'\n ? unknown\n : string\n}\n\n// ────────────────────────────────────────────────────────────────────────────\n// RouteTable — high-level table API\n// ────────────────────────────────────────────────────────────────────────────\n\nexport interface ListOptions<S extends SchemaDefinition> {\n /** Filter rows by matching column values. */\n where?: Partial<InferRow<S>>\n /** Sort by a column. Defaults to `'id'`. */\n orderBy?: keyof InferRow<S>\n /** Sort direction. Defaults to `'asc'`. */\n order?: 'asc' | 'desc'\n /** Max rows to return. */\n limit?: number\n}\n\n/**\n * High-level CRUD interface for a single SQLite table.\n *\n * Created via `RouteStore.table()` — do not instantiate directly.\n */\nexport class RouteTable<S extends SchemaDefinition> {\n private readonly jsonCols: Set<string>\n\n constructor(\n private readonly db: SQLiteStore,\n readonly tableName: string,\n private readonly schema: S,\n private readonly emit: (event: ChangeEvent) => void,\n ) {\n this.jsonCols = new Set(\n Object.entries(schema)\n .filter(([, t]) => t === 'json')\n .map(([k]) => k),\n )\n this.createTable()\n }\n\n // ── Schema bootstrap ──────────────────────────────────────────────────────\n\n private createTable(): void {\n const cols = Object.entries(this.schema)\n .map(([col, type]) => {\n const sqlType = type === 'json' ? 'TEXT' : type.toUpperCase()\n return `${col} ${sqlType}`\n })\n .join(', ')\n this.db.exec(\n `CREATE TABLE IF NOT EXISTS \"${this.tableName}\" (id INTEGER PRIMARY KEY AUTOINCREMENT, ${cols})`,\n )\n }\n\n // ── Serialization helpers ─────────────────────────────────────────────────\n\n private serialize(data: Record<string, unknown>): Record<string, unknown> {\n const out: Record<string, unknown> = {}\n for (const [k, v] of Object.entries(data)) {\n out[k] = this.jsonCols.has(k) ? JSON.stringify(v) : v\n }\n return out\n }\n\n private deserialize(row: Record<string, unknown>): InferRow<S> {\n const out: Record<string, unknown> = {}\n for (const [k, v] of Object.entries(row)) {\n out[k] = this.jsonCols.has(k) && typeof v === 'string' ? JSON.parse(v) : v\n }\n return out as InferRow<S>\n }\n\n // ── CRUD ──────────────────────────────────────────────────────────────────\n\n /**\n * Return all rows, with optional filtering, sorting, and limiting.\n *\n * @example\n * ```ts\n * await items.list()\n * await items.list({ where: { status: 'active' }, orderBy: 'createdAt', order: 'desc', limit: 10 })\n * ```\n */\n async list(options: ListOptions<S> = {}): Promise<InferRow<S>[]> {\n const { where, orderBy = 'id', order = 'asc', limit } = options\n const parts: string[] = []\n const values: unknown[] = []\n\n if (where) {\n for (const [k, v] of Object.entries(where)) {\n parts.push(`${k} = ?`)\n values.push(this.jsonCols.has(k) ? JSON.stringify(v) : v)\n }\n }\n\n const whereClause = parts.length ? `WHERE ${parts.join(' AND ')}` : ''\n const orderClause = `ORDER BY ${String(orderBy)} ${order.toUpperCase()}`\n const limitClause = limit != null ? `LIMIT ${limit}` : ''\n\n const sql = `SELECT * FROM \"${this.tableName}\" ${whereClause} ${orderClause} ${limitClause}`.trim()\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const rows = (this.db.prepare(sql) as any).all(...values) as Record<string, unknown>[]\n return rows.map((r) => this.deserialize(r))\n }\n\n /**\n * Return a single row by `id`, or `null` if not found.\n *\n * @example\n * ```ts\n * const item = await items.get(1)\n * ```\n */\n async get(id: number): Promise<InferRow<S> | null> {\n const row = this.db\n .prepare(`SELECT * FROM \"${this.tableName}\" WHERE id = ?`)\n .get(id) as Record<string, unknown> | undefined\n return row ? this.deserialize(row) : null\n }\n\n /**\n * Insert a new row and emit an `INSERT` change event.\n *\n * @example\n * ```ts\n * const item = await items.create({ name: 'Apple', createdAt: new Date().toISOString() })\n * ```\n */\n async create(data: Omit<InferRow<S>, 'id'>): Promise<InferRow<S>> {\n const serialized = this.serialize(data as Record<string, unknown>)\n const cols = Object.keys(serialized).join(', ')\n const placeholders = Object.keys(serialized)\n .map(() => '?')\n .join(', ')\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const result = (this.db.prepare(`INSERT INTO \"${this.tableName}\" (${cols}) VALUES (${placeholders})`) as any).run(\n ...Object.values(serialized),\n ) as { lastInsertRowid: number }\n\n const created = (await this.get(result.lastInsertRowid))!\n this.emit({ table: this.tableName, operation: 'INSERT', newRow: created, oldRow: null, timestamp: Date.now() })\n return created\n }\n\n /**\n * Update columns on an existing row and emit an `UPDATE` change event.\n * Returns the updated row, or `null` if the `id` does not exist.\n *\n * @example\n * ```ts\n * const updated = await items.update(1, { name: 'Mango' })\n * ```\n */\n async update(id: number, data: Partial<Omit<InferRow<S>, 'id'>>): Promise<InferRow<S> | null> {\n const old = await this.get(id)\n if (!old) return null\n\n const serialized = this.serialize(data as Record<string, unknown>)\n const setClauses = Object.keys(serialized)\n .map((k) => `${k} = ?`)\n .join(', ')\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n ;(this.db.prepare(`UPDATE \"${this.tableName}\" SET ${setClauses} WHERE id = ?`) as any).run(\n ...Object.values(serialized),\n id,\n )\n\n const updated = (await this.get(id))!\n this.emit({ table: this.tableName, operation: 'UPDATE', newRow: updated, oldRow: old, timestamp: Date.now() })\n return updated\n }\n\n /**\n * Delete a row by `id` and emit a `DELETE` change event.\n * Returns `true` if a row was deleted, `false` if `id` was not found.\n *\n * @example\n * ```ts\n * await items.delete(1)\n * ```\n */\n async delete(id: number): Promise<boolean> {\n const old = await this.get(id)\n if (!old) return false\n\n this.db.prepare(`DELETE FROM \"${this.tableName}\" WHERE id = ?`).run(id)\n this.emit({ table: this.tableName, operation: 'DELETE', newRow: null, oldRow: old, timestamp: Date.now() })\n return true\n }\n\n /**\n * Seed the table with initial rows if it is empty.\n *\n * @example\n * ```ts\n * await items.seed([\n * { name: 'Apple', createdAt: '2026-01-01T00:00:00.000Z' },\n * ])\n * ```\n */\n async seed(rows: Omit<InferRow<S>, 'id'>[]): Promise<void> {\n const count = (\n this.db.prepare(`SELECT COUNT(*) as n FROM \"${this.tableName}\"`).get() as { n: number }\n ).n\n if (count > 0) return\n for (const row of rows) {\n await this.create(row)\n }\n }\n}\n\n// ────────────────────────────────────────────────────────────────────────────\n// RouteStore — DatabaseAdapter + table factory\n// ────────────────────────────────────────────────────────────────────────────\n\n/**\n * File-based SQLite store that implements `DatabaseAdapter`.\n *\n * Use it as the RouteFlow adapter **and** as your data access layer —\n * no separate adapter or manual `emit()` calls required.\n * Any mutation (`create`, `update`, `delete`) automatically fires\n * a `ChangeEvent` and triggers all `@Reactive` WebSocket pushes.\n *\n * Requires Node.js 22.5+ (built-in `node:sqlite`).\n *\n * @example\n * ```ts\n * import { RouteStore } from 'routeflow-api/sqlite'\n * import { createApp } from 'routeflow-api'\n *\n * const db = new RouteStore('./data/app.db')\n *\n * const items = db.table('items', {\n * name: 'text',\n * createdAt: 'text',\n * })\n *\n * await items.seed([{ name: 'Apple', createdAt: new Date().toISOString() }])\n *\n * // db IS the adapter — pass directly to createApp\n * const app = createApp({ adapter: db, port: 3000 })\n * ```\n */\nexport class RouteStore implements DatabaseAdapter {\n private readonly sql: SQLiteStore\n private readonly listeners = new Map<string, Set<(event: ChangeEvent) => void>>()\n private connected = false\n\n /**\n * @param dbPath - Path to the SQLite file.\n * Parent directories are created automatically.\n * Relative paths resolve from `process.cwd()`.\n */\n constructor(dbPath: string) {\n this.sql = new SQLiteStore(dbPath)\n }\n\n /**\n * Define a table and return a typed CRUD handle.\n * The table is created in SQLite automatically if it does not exist.\n *\n * @param name - Table name\n * @param schema - Column definitions: `{ columnName: 'integer' | 'text' | 'real' | 'json' }`\n *\n * @example\n * ```ts\n * const tasks = db.table('tasks', {\n * title: 'text',\n * done: 'integer', // 0 | 1\n * metadata: 'json', // auto-serialised JS object\n * })\n * ```\n */\n table<S extends SchemaDefinition>(name: string, schema: S): RouteTable<S> {\n return new RouteTable(this.sql, name, schema, (evt) => this.dispatchEvent(evt))\n }\n\n // ── DatabaseAdapter ───────────────────────────────────────────────────────\n\n async connect(): Promise<void> {\n this.connected = true\n }\n\n async disconnect(): Promise<void> {\n this.sql.close()\n this.connected = false\n }\n\n onChange(table: string, callback: (event: ChangeEvent) => void): () => void {\n if (!this.listeners.has(table)) this.listeners.set(table, new Set())\n this.listeners.get(table)!.add(callback)\n return () => this.listeners.get(table)?.delete(callback)\n }\n\n get isConnected(): boolean {\n return this.connected\n }\n\n // ── Internal ──────────────────────────────────────────────────────────────\n\n private dispatchEvent(event: ChangeEvent): void {\n const cbs = this.listeners.get(event.table)\n if (!cbs) return\n for (const cb of cbs) cb(event)\n }\n}\n"],"mappings":"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","names":[]}
package/dist/sqlite.d.cts CHANGED
@@ -1,3 +1,4 @@
1
+ import { D as DatabaseAdapter, C as ChangeEvent } from './types-tPDla8AE.cjs';
1
2
  import { DatabaseSync } from 'node:sqlite';
2
3
 
3
4
  /**
@@ -36,4 +37,161 @@ declare class SQLiteStore {
36
37
  close(): void;
37
38
  }
38
39
 
39
- export { SQLiteStore, type SqliteRow };
40
+ /** Supported column types for RouteStore tables. */
41
+ type ColumnType = 'integer' | 'text' | 'real' | 'json';
42
+ /** Schema definition: maps column names to their types. */
43
+ type SchemaDefinition = Record<string, ColumnType>;
44
+ /** TypeScript row type inferred from a schema definition. */
45
+ type InferRow<S extends SchemaDefinition> = {
46
+ id: number;
47
+ } & {
48
+ [K in keyof S]: S[K] extends 'integer' ? number : S[K] extends 'real' ? number : S[K] extends 'json' ? unknown : string;
49
+ };
50
+ interface ListOptions<S extends SchemaDefinition> {
51
+ /** Filter rows by matching column values. */
52
+ where?: Partial<InferRow<S>>;
53
+ /** Sort by a column. Defaults to `'id'`. */
54
+ orderBy?: keyof InferRow<S>;
55
+ /** Sort direction. Defaults to `'asc'`. */
56
+ order?: 'asc' | 'desc';
57
+ /** Max rows to return. */
58
+ limit?: number;
59
+ }
60
+ /**
61
+ * High-level CRUD interface for a single SQLite table.
62
+ *
63
+ * Created via `RouteStore.table()` — do not instantiate directly.
64
+ */
65
+ declare class RouteTable<S extends SchemaDefinition> {
66
+ private readonly db;
67
+ readonly tableName: string;
68
+ private readonly schema;
69
+ private readonly emit;
70
+ private readonly jsonCols;
71
+ constructor(db: SQLiteStore, tableName: string, schema: S, emit: (event: ChangeEvent) => void);
72
+ private createTable;
73
+ private serialize;
74
+ private deserialize;
75
+ /**
76
+ * Return all rows, with optional filtering, sorting, and limiting.
77
+ *
78
+ * @example
79
+ * ```ts
80
+ * await items.list()
81
+ * await items.list({ where: { status: 'active' }, orderBy: 'createdAt', order: 'desc', limit: 10 })
82
+ * ```
83
+ */
84
+ list(options?: ListOptions<S>): Promise<InferRow<S>[]>;
85
+ /**
86
+ * Return a single row by `id`, or `null` if not found.
87
+ *
88
+ * @example
89
+ * ```ts
90
+ * const item = await items.get(1)
91
+ * ```
92
+ */
93
+ get(id: number): Promise<InferRow<S> | null>;
94
+ /**
95
+ * Insert a new row and emit an `INSERT` change event.
96
+ *
97
+ * @example
98
+ * ```ts
99
+ * const item = await items.create({ name: 'Apple', createdAt: new Date().toISOString() })
100
+ * ```
101
+ */
102
+ create(data: Omit<InferRow<S>, 'id'>): Promise<InferRow<S>>;
103
+ /**
104
+ * Update columns on an existing row and emit an `UPDATE` change event.
105
+ * Returns the updated row, or `null` if the `id` does not exist.
106
+ *
107
+ * @example
108
+ * ```ts
109
+ * const updated = await items.update(1, { name: 'Mango' })
110
+ * ```
111
+ */
112
+ update(id: number, data: Partial<Omit<InferRow<S>, 'id'>>): Promise<InferRow<S> | null>;
113
+ /**
114
+ * Delete a row by `id` and emit a `DELETE` change event.
115
+ * Returns `true` if a row was deleted, `false` if `id` was not found.
116
+ *
117
+ * @example
118
+ * ```ts
119
+ * await items.delete(1)
120
+ * ```
121
+ */
122
+ delete(id: number): Promise<boolean>;
123
+ /**
124
+ * Seed the table with initial rows if it is empty.
125
+ *
126
+ * @example
127
+ * ```ts
128
+ * await items.seed([
129
+ * { name: 'Apple', createdAt: '2026-01-01T00:00:00.000Z' },
130
+ * ])
131
+ * ```
132
+ */
133
+ seed(rows: Omit<InferRow<S>, 'id'>[]): Promise<void>;
134
+ }
135
+ /**
136
+ * File-based SQLite store that implements `DatabaseAdapter`.
137
+ *
138
+ * Use it as the RouteFlow adapter **and** as your data access layer —
139
+ * no separate adapter or manual `emit()` calls required.
140
+ * Any mutation (`create`, `update`, `delete`) automatically fires
141
+ * a `ChangeEvent` and triggers all `@Reactive` WebSocket pushes.
142
+ *
143
+ * Requires Node.js 22.5+ (built-in `node:sqlite`).
144
+ *
145
+ * @example
146
+ * ```ts
147
+ * import { RouteStore } from 'routeflow-api/sqlite'
148
+ * import { createApp } from 'routeflow-api'
149
+ *
150
+ * const db = new RouteStore('./data/app.db')
151
+ *
152
+ * const items = db.table('items', {
153
+ * name: 'text',
154
+ * createdAt: 'text',
155
+ * })
156
+ *
157
+ * await items.seed([{ name: 'Apple', createdAt: new Date().toISOString() }])
158
+ *
159
+ * // db IS the adapter — pass directly to createApp
160
+ * const app = createApp({ adapter: db, port: 3000 })
161
+ * ```
162
+ */
163
+ declare class RouteStore implements DatabaseAdapter {
164
+ private readonly sql;
165
+ private readonly listeners;
166
+ private connected;
167
+ /**
168
+ * @param dbPath - Path to the SQLite file.
169
+ * Parent directories are created automatically.
170
+ * Relative paths resolve from `process.cwd()`.
171
+ */
172
+ constructor(dbPath: string);
173
+ /**
174
+ * Define a table and return a typed CRUD handle.
175
+ * The table is created in SQLite automatically if it does not exist.
176
+ *
177
+ * @param name - Table name
178
+ * @param schema - Column definitions: `{ columnName: 'integer' | 'text' | 'real' | 'json' }`
179
+ *
180
+ * @example
181
+ * ```ts
182
+ * const tasks = db.table('tasks', {
183
+ * title: 'text',
184
+ * done: 'integer', // 0 | 1
185
+ * metadata: 'json', // auto-serialised JS object
186
+ * })
187
+ * ```
188
+ */
189
+ table<S extends SchemaDefinition>(name: string, schema: S): RouteTable<S>;
190
+ connect(): Promise<void>;
191
+ disconnect(): Promise<void>;
192
+ onChange(table: string, callback: (event: ChangeEvent) => void): () => void;
193
+ get isConnected(): boolean;
194
+ private dispatchEvent;
195
+ }
196
+
197
+ export { type ColumnType, type InferRow, type ListOptions, RouteStore, SQLiteStore, type SchemaDefinition, type SqliteRow };
package/dist/sqlite.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { D as DatabaseAdapter, C as ChangeEvent } from './types-tPDla8AE.js';
1
2
  import { DatabaseSync } from 'node:sqlite';
2
3
 
3
4
  /**
@@ -36,4 +37,161 @@ declare class SQLiteStore {
36
37
  close(): void;
37
38
  }
38
39
 
39
- export { SQLiteStore, type SqliteRow };
40
+ /** Supported column types for RouteStore tables. */
41
+ type ColumnType = 'integer' | 'text' | 'real' | 'json';
42
+ /** Schema definition: maps column names to their types. */
43
+ type SchemaDefinition = Record<string, ColumnType>;
44
+ /** TypeScript row type inferred from a schema definition. */
45
+ type InferRow<S extends SchemaDefinition> = {
46
+ id: number;
47
+ } & {
48
+ [K in keyof S]: S[K] extends 'integer' ? number : S[K] extends 'real' ? number : S[K] extends 'json' ? unknown : string;
49
+ };
50
+ interface ListOptions<S extends SchemaDefinition> {
51
+ /** Filter rows by matching column values. */
52
+ where?: Partial<InferRow<S>>;
53
+ /** Sort by a column. Defaults to `'id'`. */
54
+ orderBy?: keyof InferRow<S>;
55
+ /** Sort direction. Defaults to `'asc'`. */
56
+ order?: 'asc' | 'desc';
57
+ /** Max rows to return. */
58
+ limit?: number;
59
+ }
60
+ /**
61
+ * High-level CRUD interface for a single SQLite table.
62
+ *
63
+ * Created via `RouteStore.table()` — do not instantiate directly.
64
+ */
65
+ declare class RouteTable<S extends SchemaDefinition> {
66
+ private readonly db;
67
+ readonly tableName: string;
68
+ private readonly schema;
69
+ private readonly emit;
70
+ private readonly jsonCols;
71
+ constructor(db: SQLiteStore, tableName: string, schema: S, emit: (event: ChangeEvent) => void);
72
+ private createTable;
73
+ private serialize;
74
+ private deserialize;
75
+ /**
76
+ * Return all rows, with optional filtering, sorting, and limiting.
77
+ *
78
+ * @example
79
+ * ```ts
80
+ * await items.list()
81
+ * await items.list({ where: { status: 'active' }, orderBy: 'createdAt', order: 'desc', limit: 10 })
82
+ * ```
83
+ */
84
+ list(options?: ListOptions<S>): Promise<InferRow<S>[]>;
85
+ /**
86
+ * Return a single row by `id`, or `null` if not found.
87
+ *
88
+ * @example
89
+ * ```ts
90
+ * const item = await items.get(1)
91
+ * ```
92
+ */
93
+ get(id: number): Promise<InferRow<S> | null>;
94
+ /**
95
+ * Insert a new row and emit an `INSERT` change event.
96
+ *
97
+ * @example
98
+ * ```ts
99
+ * const item = await items.create({ name: 'Apple', createdAt: new Date().toISOString() })
100
+ * ```
101
+ */
102
+ create(data: Omit<InferRow<S>, 'id'>): Promise<InferRow<S>>;
103
+ /**
104
+ * Update columns on an existing row and emit an `UPDATE` change event.
105
+ * Returns the updated row, or `null` if the `id` does not exist.
106
+ *
107
+ * @example
108
+ * ```ts
109
+ * const updated = await items.update(1, { name: 'Mango' })
110
+ * ```
111
+ */
112
+ update(id: number, data: Partial<Omit<InferRow<S>, 'id'>>): Promise<InferRow<S> | null>;
113
+ /**
114
+ * Delete a row by `id` and emit a `DELETE` change event.
115
+ * Returns `true` if a row was deleted, `false` if `id` was not found.
116
+ *
117
+ * @example
118
+ * ```ts
119
+ * await items.delete(1)
120
+ * ```
121
+ */
122
+ delete(id: number): Promise<boolean>;
123
+ /**
124
+ * Seed the table with initial rows if it is empty.
125
+ *
126
+ * @example
127
+ * ```ts
128
+ * await items.seed([
129
+ * { name: 'Apple', createdAt: '2026-01-01T00:00:00.000Z' },
130
+ * ])
131
+ * ```
132
+ */
133
+ seed(rows: Omit<InferRow<S>, 'id'>[]): Promise<void>;
134
+ }
135
+ /**
136
+ * File-based SQLite store that implements `DatabaseAdapter`.
137
+ *
138
+ * Use it as the RouteFlow adapter **and** as your data access layer —
139
+ * no separate adapter or manual `emit()` calls required.
140
+ * Any mutation (`create`, `update`, `delete`) automatically fires
141
+ * a `ChangeEvent` and triggers all `@Reactive` WebSocket pushes.
142
+ *
143
+ * Requires Node.js 22.5+ (built-in `node:sqlite`).
144
+ *
145
+ * @example
146
+ * ```ts
147
+ * import { RouteStore } from 'routeflow-api/sqlite'
148
+ * import { createApp } from 'routeflow-api'
149
+ *
150
+ * const db = new RouteStore('./data/app.db')
151
+ *
152
+ * const items = db.table('items', {
153
+ * name: 'text',
154
+ * createdAt: 'text',
155
+ * })
156
+ *
157
+ * await items.seed([{ name: 'Apple', createdAt: new Date().toISOString() }])
158
+ *
159
+ * // db IS the adapter — pass directly to createApp
160
+ * const app = createApp({ adapter: db, port: 3000 })
161
+ * ```
162
+ */
163
+ declare class RouteStore implements DatabaseAdapter {
164
+ private readonly sql;
165
+ private readonly listeners;
166
+ private connected;
167
+ /**
168
+ * @param dbPath - Path to the SQLite file.
169
+ * Parent directories are created automatically.
170
+ * Relative paths resolve from `process.cwd()`.
171
+ */
172
+ constructor(dbPath: string);
173
+ /**
174
+ * Define a table and return a typed CRUD handle.
175
+ * The table is created in SQLite automatically if it does not exist.
176
+ *
177
+ * @param name - Table name
178
+ * @param schema - Column definitions: `{ columnName: 'integer' | 'text' | 'real' | 'json' }`
179
+ *
180
+ * @example
181
+ * ```ts
182
+ * const tasks = db.table('tasks', {
183
+ * title: 'text',
184
+ * done: 'integer', // 0 | 1
185
+ * metadata: 'json', // auto-serialised JS object
186
+ * })
187
+ * ```
188
+ */
189
+ table<S extends SchemaDefinition>(name: string, schema: S): RouteTable<S>;
190
+ connect(): Promise<void>;
191
+ disconnect(): Promise<void>;
192
+ onChange(table: string, callback: (event: ChangeEvent) => void): () => void;
193
+ get isConnected(): boolean;
194
+ private dispatchEvent;
195
+ }
196
+
197
+ export { type ColumnType, type InferRow, type ListOptions, RouteStore, SQLiteStore, type SchemaDefinition, type SqliteRow };
package/dist/sqlite.js CHANGED
@@ -27,7 +27,219 @@ var SQLiteStore = class {
27
27
  this.db.close();
28
28
  }
29
29
  };
30
+
31
+ // src/core/adapter/route-store.ts
32
+ var RouteTable = class {
33
+ constructor(db, tableName, schema, emit) {
34
+ this.db = db;
35
+ this.tableName = tableName;
36
+ this.schema = schema;
37
+ this.emit = emit;
38
+ this.jsonCols = new Set(
39
+ Object.entries(schema).filter(([, t]) => t === "json").map(([k]) => k)
40
+ );
41
+ this.createTable();
42
+ }
43
+ db;
44
+ tableName;
45
+ schema;
46
+ emit;
47
+ jsonCols;
48
+ // ── Schema bootstrap ──────────────────────────────────────────────────────
49
+ createTable() {
50
+ const cols = Object.entries(this.schema).map(([col, type]) => {
51
+ const sqlType = type === "json" ? "TEXT" : type.toUpperCase();
52
+ return `${col} ${sqlType}`;
53
+ }).join(", ");
54
+ this.db.exec(
55
+ `CREATE TABLE IF NOT EXISTS "${this.tableName}" (id INTEGER PRIMARY KEY AUTOINCREMENT, ${cols})`
56
+ );
57
+ }
58
+ // ── Serialization helpers ─────────────────────────────────────────────────
59
+ serialize(data) {
60
+ const out = {};
61
+ for (const [k, v] of Object.entries(data)) {
62
+ out[k] = this.jsonCols.has(k) ? JSON.stringify(v) : v;
63
+ }
64
+ return out;
65
+ }
66
+ deserialize(row) {
67
+ const out = {};
68
+ for (const [k, v] of Object.entries(row)) {
69
+ out[k] = this.jsonCols.has(k) && typeof v === "string" ? JSON.parse(v) : v;
70
+ }
71
+ return out;
72
+ }
73
+ // ── CRUD ──────────────────────────────────────────────────────────────────
74
+ /**
75
+ * Return all rows, with optional filtering, sorting, and limiting.
76
+ *
77
+ * @example
78
+ * ```ts
79
+ * await items.list()
80
+ * await items.list({ where: { status: 'active' }, orderBy: 'createdAt', order: 'desc', limit: 10 })
81
+ * ```
82
+ */
83
+ async list(options = {}) {
84
+ const { where, orderBy = "id", order = "asc", limit } = options;
85
+ const parts = [];
86
+ const values = [];
87
+ if (where) {
88
+ for (const [k, v] of Object.entries(where)) {
89
+ parts.push(`${k} = ?`);
90
+ values.push(this.jsonCols.has(k) ? JSON.stringify(v) : v);
91
+ }
92
+ }
93
+ const whereClause = parts.length ? `WHERE ${parts.join(" AND ")}` : "";
94
+ const orderClause = `ORDER BY ${String(orderBy)} ${order.toUpperCase()}`;
95
+ const limitClause = limit != null ? `LIMIT ${limit}` : "";
96
+ const sql = `SELECT * FROM "${this.tableName}" ${whereClause} ${orderClause} ${limitClause}`.trim();
97
+ const rows = this.db.prepare(sql).all(...values);
98
+ return rows.map((r) => this.deserialize(r));
99
+ }
100
+ /**
101
+ * Return a single row by `id`, or `null` if not found.
102
+ *
103
+ * @example
104
+ * ```ts
105
+ * const item = await items.get(1)
106
+ * ```
107
+ */
108
+ async get(id) {
109
+ const row = this.db.prepare(`SELECT * FROM "${this.tableName}" WHERE id = ?`).get(id);
110
+ return row ? this.deserialize(row) : null;
111
+ }
112
+ /**
113
+ * Insert a new row and emit an `INSERT` change event.
114
+ *
115
+ * @example
116
+ * ```ts
117
+ * const item = await items.create({ name: 'Apple', createdAt: new Date().toISOString() })
118
+ * ```
119
+ */
120
+ async create(data) {
121
+ const serialized = this.serialize(data);
122
+ const cols = Object.keys(serialized).join(", ");
123
+ const placeholders = Object.keys(serialized).map(() => "?").join(", ");
124
+ const result = this.db.prepare(`INSERT INTO "${this.tableName}" (${cols}) VALUES (${placeholders})`).run(
125
+ ...Object.values(serialized)
126
+ );
127
+ const created = await this.get(result.lastInsertRowid);
128
+ this.emit({ table: this.tableName, operation: "INSERT", newRow: created, oldRow: null, timestamp: Date.now() });
129
+ return created;
130
+ }
131
+ /**
132
+ * Update columns on an existing row and emit an `UPDATE` change event.
133
+ * Returns the updated row, or `null` if the `id` does not exist.
134
+ *
135
+ * @example
136
+ * ```ts
137
+ * const updated = await items.update(1, { name: 'Mango' })
138
+ * ```
139
+ */
140
+ async update(id, data) {
141
+ const old = await this.get(id);
142
+ if (!old) return null;
143
+ const serialized = this.serialize(data);
144
+ const setClauses = Object.keys(serialized).map((k) => `${k} = ?`).join(", ");
145
+ this.db.prepare(`UPDATE "${this.tableName}" SET ${setClauses} WHERE id = ?`).run(
146
+ ...Object.values(serialized),
147
+ id
148
+ );
149
+ const updated = await this.get(id);
150
+ this.emit({ table: this.tableName, operation: "UPDATE", newRow: updated, oldRow: old, timestamp: Date.now() });
151
+ return updated;
152
+ }
153
+ /**
154
+ * Delete a row by `id` and emit a `DELETE` change event.
155
+ * Returns `true` if a row was deleted, `false` if `id` was not found.
156
+ *
157
+ * @example
158
+ * ```ts
159
+ * await items.delete(1)
160
+ * ```
161
+ */
162
+ async delete(id) {
163
+ const old = await this.get(id);
164
+ if (!old) return false;
165
+ this.db.prepare(`DELETE FROM "${this.tableName}" WHERE id = ?`).run(id);
166
+ this.emit({ table: this.tableName, operation: "DELETE", newRow: null, oldRow: old, timestamp: Date.now() });
167
+ return true;
168
+ }
169
+ /**
170
+ * Seed the table with initial rows if it is empty.
171
+ *
172
+ * @example
173
+ * ```ts
174
+ * await items.seed([
175
+ * { name: 'Apple', createdAt: '2026-01-01T00:00:00.000Z' },
176
+ * ])
177
+ * ```
178
+ */
179
+ async seed(rows) {
180
+ const count = this.db.prepare(`SELECT COUNT(*) as n FROM "${this.tableName}"`).get().n;
181
+ if (count > 0) return;
182
+ for (const row of rows) {
183
+ await this.create(row);
184
+ }
185
+ }
186
+ };
187
+ var RouteStore = class {
188
+ sql;
189
+ listeners = /* @__PURE__ */ new Map();
190
+ connected = false;
191
+ /**
192
+ * @param dbPath - Path to the SQLite file.
193
+ * Parent directories are created automatically.
194
+ * Relative paths resolve from `process.cwd()`.
195
+ */
196
+ constructor(dbPath) {
197
+ this.sql = new SQLiteStore(dbPath);
198
+ }
199
+ /**
200
+ * Define a table and return a typed CRUD handle.
201
+ * The table is created in SQLite automatically if it does not exist.
202
+ *
203
+ * @param name - Table name
204
+ * @param schema - Column definitions: `{ columnName: 'integer' | 'text' | 'real' | 'json' }`
205
+ *
206
+ * @example
207
+ * ```ts
208
+ * const tasks = db.table('tasks', {
209
+ * title: 'text',
210
+ * done: 'integer', // 0 | 1
211
+ * metadata: 'json', // auto-serialised JS object
212
+ * })
213
+ * ```
214
+ */
215
+ table(name, schema) {
216
+ return new RouteTable(this.sql, name, schema, (evt) => this.dispatchEvent(evt));
217
+ }
218
+ // ── DatabaseAdapter ───────────────────────────────────────────────────────
219
+ async connect() {
220
+ this.connected = true;
221
+ }
222
+ async disconnect() {
223
+ this.sql.close();
224
+ this.connected = false;
225
+ }
226
+ onChange(table, callback) {
227
+ if (!this.listeners.has(table)) this.listeners.set(table, /* @__PURE__ */ new Set());
228
+ this.listeners.get(table).add(callback);
229
+ return () => this.listeners.get(table)?.delete(callback);
230
+ }
231
+ get isConnected() {
232
+ return this.connected;
233
+ }
234
+ // ── Internal ──────────────────────────────────────────────────────────────
235
+ dispatchEvent(event) {
236
+ const cbs = this.listeners.get(event.table);
237
+ if (!cbs) return;
238
+ for (const cb of cbs) cb(event);
239
+ }
240
+ };
30
241
  export {
242
+ RouteStore,
31
243
  SQLiteStore
32
244
  };
33
245
  //# sourceMappingURL=sqlite.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/core/adapter/sqlite-store.ts"],"sourcesContent":["import { DatabaseSync } from 'node:sqlite'\nimport { mkdirSync } from 'node:fs'\nimport { dirname, resolve } from 'node:path'\n\n/**\n * A generic record type for SQLite rows.\n * Keys map to column names; values are SQLite-compatible primitives.\n */\nexport type SqliteRow = Record<string, string | number | null>\n\n/**\n * File-based key-value store backed by Node's built-in `node:sqlite`.\n * Requires Node.js 22.5+.\n *\n * Data survives server restarts because it is written to a file on disk.\n * The path can be set directly in code — no environment variable required.\n *\n * @example\n * ```ts\n * const store = new SQLiteStore('./data/app.db')\n * store.exec(`CREATE TABLE IF NOT EXISTS items (id INTEGER PRIMARY KEY, name TEXT)`)\n * store.prepare('INSERT INTO items (name) VALUES (?)').run('Apple')\n * const rows = store.prepare('SELECT * FROM items').all()\n * ```\n */\nexport class SQLiteStore {\n readonly db: DatabaseSync\n readonly path: string\n\n /**\n * @param dbPath - Path to the SQLite file. Parent directories are created automatically.\n * Relative paths are resolved from `process.cwd()`.\n */\n constructor(dbPath: string) {\n this.path = resolve(dbPath)\n mkdirSync(dirname(this.path), { recursive: true })\n this.db = new DatabaseSync(this.path)\n }\n\n /** Execute one or more SQL statements (no return value). */\n exec(sql: string): void {\n this.db.exec(sql)\n }\n\n /** Prepare a statement for repeated use. */\n prepare(sql: string): ReturnType<DatabaseSync['prepare']> {\n return this.db.prepare(sql)\n }\n\n /** Close the database connection. */\n close(): void {\n this.db.close()\n }\n}\n"],"mappings":";AAAA,SAAS,oBAAoB;AAC7B,SAAS,iBAAiB;AAC1B,SAAS,SAAS,eAAe;AAuB1B,IAAM,cAAN,MAAkB;AAAA,EACd;AAAA,EACA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMT,YAAY,QAAgB;AAC1B,SAAK,OAAO,QAAQ,MAAM;AAC1B,cAAU,QAAQ,KAAK,IAAI,GAAG,EAAE,WAAW,KAAK,CAAC;AACjD,SAAK,KAAK,IAAI,aAAa,KAAK,IAAI;AAAA,EACtC;AAAA;AAAA,EAGA,KAAK,KAAmB;AACtB,SAAK,GAAG,KAAK,GAAG;AAAA,EAClB;AAAA;AAAA,EAGA,QAAQ,KAAkD;AACxD,WAAO,KAAK,GAAG,QAAQ,GAAG;AAAA,EAC5B;AAAA;AAAA,EAGA,QAAc;AACZ,SAAK,GAAG,MAAM;AAAA,EAChB;AACF;","names":[]}
1
+ {"version":3,"sources":["../src/core/adapter/sqlite-store.ts","../src/core/adapter/route-store.ts"],"sourcesContent":["import { DatabaseSync } from 'node:sqlite'\nimport { mkdirSync } from 'node:fs'\nimport { dirname, resolve } from 'node:path'\n\n/**\n * A generic record type for SQLite rows.\n * Keys map to column names; values are SQLite-compatible primitives.\n */\nexport type SqliteRow = Record<string, string | number | null>\n\n/**\n * File-based key-value store backed by Node's built-in `node:sqlite`.\n * Requires Node.js 22.5+.\n *\n * Data survives server restarts because it is written to a file on disk.\n * The path can be set directly in code — no environment variable required.\n *\n * @example\n * ```ts\n * const store = new SQLiteStore('./data/app.db')\n * store.exec(`CREATE TABLE IF NOT EXISTS items (id INTEGER PRIMARY KEY, name TEXT)`)\n * store.prepare('INSERT INTO items (name) VALUES (?)').run('Apple')\n * const rows = store.prepare('SELECT * FROM items').all()\n * ```\n */\nexport class SQLiteStore {\n readonly db: DatabaseSync\n readonly path: string\n\n /**\n * @param dbPath - Path to the SQLite file. Parent directories are created automatically.\n * Relative paths are resolved from `process.cwd()`.\n */\n constructor(dbPath: string) {\n this.path = resolve(dbPath)\n mkdirSync(dirname(this.path), { recursive: true })\n this.db = new DatabaseSync(this.path)\n }\n\n /** Execute one or more SQL statements (no return value). */\n exec(sql: string): void {\n this.db.exec(sql)\n }\n\n /** Prepare a statement for repeated use. */\n prepare(sql: string): ReturnType<DatabaseSync['prepare']> {\n return this.db.prepare(sql)\n }\n\n /** Close the database connection. */\n close(): void {\n this.db.close()\n }\n}\n","import type { ChangeEvent, DatabaseAdapter } from '../types.js'\nimport { SQLiteStore } from './sqlite-store.js'\n\n// ────────────────────────────────────────────────────────────────────────────\n// Schema definition types\n// ────────────────────────────────────────────────────────────────────────────\n\n/** Supported column types for RouteStore tables. */\nexport type ColumnType = 'integer' | 'text' | 'real' | 'json'\n\n/** Schema definition: maps column names to their types. */\nexport type SchemaDefinition = Record<string, ColumnType>\n\n/** TypeScript row type inferred from a schema definition. */\nexport type InferRow<S extends SchemaDefinition> = {\n id: number\n} & {\n [K in keyof S]: S[K] extends 'integer'\n ? number\n : S[K] extends 'real'\n ? number\n : S[K] extends 'json'\n ? unknown\n : string\n}\n\n// ────────────────────────────────────────────────────────────────────────────\n// RouteTable — high-level table API\n// ────────────────────────────────────────────────────────────────────────────\n\nexport interface ListOptions<S extends SchemaDefinition> {\n /** Filter rows by matching column values. */\n where?: Partial<InferRow<S>>\n /** Sort by a column. Defaults to `'id'`. */\n orderBy?: keyof InferRow<S>\n /** Sort direction. Defaults to `'asc'`. */\n order?: 'asc' | 'desc'\n /** Max rows to return. */\n limit?: number\n}\n\n/**\n * High-level CRUD interface for a single SQLite table.\n *\n * Created via `RouteStore.table()` — do not instantiate directly.\n */\nexport class RouteTable<S extends SchemaDefinition> {\n private readonly jsonCols: Set<string>\n\n constructor(\n private readonly db: SQLiteStore,\n readonly tableName: string,\n private readonly schema: S,\n private readonly emit: (event: ChangeEvent) => void,\n ) {\n this.jsonCols = new Set(\n Object.entries(schema)\n .filter(([, t]) => t === 'json')\n .map(([k]) => k),\n )\n this.createTable()\n }\n\n // ── Schema bootstrap ──────────────────────────────────────────────────────\n\n private createTable(): void {\n const cols = Object.entries(this.schema)\n .map(([col, type]) => {\n const sqlType = type === 'json' ? 'TEXT' : type.toUpperCase()\n return `${col} ${sqlType}`\n })\n .join(', ')\n this.db.exec(\n `CREATE TABLE IF NOT EXISTS \"${this.tableName}\" (id INTEGER PRIMARY KEY AUTOINCREMENT, ${cols})`,\n )\n }\n\n // ── Serialization helpers ─────────────────────────────────────────────────\n\n private serialize(data: Record<string, unknown>): Record<string, unknown> {\n const out: Record<string, unknown> = {}\n for (const [k, v] of Object.entries(data)) {\n out[k] = this.jsonCols.has(k) ? JSON.stringify(v) : v\n }\n return out\n }\n\n private deserialize(row: Record<string, unknown>): InferRow<S> {\n const out: Record<string, unknown> = {}\n for (const [k, v] of Object.entries(row)) {\n out[k] = this.jsonCols.has(k) && typeof v === 'string' ? JSON.parse(v) : v\n }\n return out as InferRow<S>\n }\n\n // ── CRUD ──────────────────────────────────────────────────────────────────\n\n /**\n * Return all rows, with optional filtering, sorting, and limiting.\n *\n * @example\n * ```ts\n * await items.list()\n * await items.list({ where: { status: 'active' }, orderBy: 'createdAt', order: 'desc', limit: 10 })\n * ```\n */\n async list(options: ListOptions<S> = {}): Promise<InferRow<S>[]> {\n const { where, orderBy = 'id', order = 'asc', limit } = options\n const parts: string[] = []\n const values: unknown[] = []\n\n if (where) {\n for (const [k, v] of Object.entries(where)) {\n parts.push(`${k} = ?`)\n values.push(this.jsonCols.has(k) ? JSON.stringify(v) : v)\n }\n }\n\n const whereClause = parts.length ? `WHERE ${parts.join(' AND ')}` : ''\n const orderClause = `ORDER BY ${String(orderBy)} ${order.toUpperCase()}`\n const limitClause = limit != null ? `LIMIT ${limit}` : ''\n\n const sql = `SELECT * FROM \"${this.tableName}\" ${whereClause} ${orderClause} ${limitClause}`.trim()\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const rows = (this.db.prepare(sql) as any).all(...values) as Record<string, unknown>[]\n return rows.map((r) => this.deserialize(r))\n }\n\n /**\n * Return a single row by `id`, or `null` if not found.\n *\n * @example\n * ```ts\n * const item = await items.get(1)\n * ```\n */\n async get(id: number): Promise<InferRow<S> | null> {\n const row = this.db\n .prepare(`SELECT * FROM \"${this.tableName}\" WHERE id = ?`)\n .get(id) as Record<string, unknown> | undefined\n return row ? this.deserialize(row) : null\n }\n\n /**\n * Insert a new row and emit an `INSERT` change event.\n *\n * @example\n * ```ts\n * const item = await items.create({ name: 'Apple', createdAt: new Date().toISOString() })\n * ```\n */\n async create(data: Omit<InferRow<S>, 'id'>): Promise<InferRow<S>> {\n const serialized = this.serialize(data as Record<string, unknown>)\n const cols = Object.keys(serialized).join(', ')\n const placeholders = Object.keys(serialized)\n .map(() => '?')\n .join(', ')\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const result = (this.db.prepare(`INSERT INTO \"${this.tableName}\" (${cols}) VALUES (${placeholders})`) as any).run(\n ...Object.values(serialized),\n ) as { lastInsertRowid: number }\n\n const created = (await this.get(result.lastInsertRowid))!\n this.emit({ table: this.tableName, operation: 'INSERT', newRow: created, oldRow: null, timestamp: Date.now() })\n return created\n }\n\n /**\n * Update columns on an existing row and emit an `UPDATE` change event.\n * Returns the updated row, or `null` if the `id` does not exist.\n *\n * @example\n * ```ts\n * const updated = await items.update(1, { name: 'Mango' })\n * ```\n */\n async update(id: number, data: Partial<Omit<InferRow<S>, 'id'>>): Promise<InferRow<S> | null> {\n const old = await this.get(id)\n if (!old) return null\n\n const serialized = this.serialize(data as Record<string, unknown>)\n const setClauses = Object.keys(serialized)\n .map((k) => `${k} = ?`)\n .join(', ')\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n ;(this.db.prepare(`UPDATE \"${this.tableName}\" SET ${setClauses} WHERE id = ?`) as any).run(\n ...Object.values(serialized),\n id,\n )\n\n const updated = (await this.get(id))!\n this.emit({ table: this.tableName, operation: 'UPDATE', newRow: updated, oldRow: old, timestamp: Date.now() })\n return updated\n }\n\n /**\n * Delete a row by `id` and emit a `DELETE` change event.\n * Returns `true` if a row was deleted, `false` if `id` was not found.\n *\n * @example\n * ```ts\n * await items.delete(1)\n * ```\n */\n async delete(id: number): Promise<boolean> {\n const old = await this.get(id)\n if (!old) return false\n\n this.db.prepare(`DELETE FROM \"${this.tableName}\" WHERE id = ?`).run(id)\n this.emit({ table: this.tableName, operation: 'DELETE', newRow: null, oldRow: old, timestamp: Date.now() })\n return true\n }\n\n /**\n * Seed the table with initial rows if it is empty.\n *\n * @example\n * ```ts\n * await items.seed([\n * { name: 'Apple', createdAt: '2026-01-01T00:00:00.000Z' },\n * ])\n * ```\n */\n async seed(rows: Omit<InferRow<S>, 'id'>[]): Promise<void> {\n const count = (\n this.db.prepare(`SELECT COUNT(*) as n FROM \"${this.tableName}\"`).get() as { n: number }\n ).n\n if (count > 0) return\n for (const row of rows) {\n await this.create(row)\n }\n }\n}\n\n// ────────────────────────────────────────────────────────────────────────────\n// RouteStore — DatabaseAdapter + table factory\n// ────────────────────────────────────────────────────────────────────────────\n\n/**\n * File-based SQLite store that implements `DatabaseAdapter`.\n *\n * Use it as the RouteFlow adapter **and** as your data access layer —\n * no separate adapter or manual `emit()` calls required.\n * Any mutation (`create`, `update`, `delete`) automatically fires\n * a `ChangeEvent` and triggers all `@Reactive` WebSocket pushes.\n *\n * Requires Node.js 22.5+ (built-in `node:sqlite`).\n *\n * @example\n * ```ts\n * import { RouteStore } from 'routeflow-api/sqlite'\n * import { createApp } from 'routeflow-api'\n *\n * const db = new RouteStore('./data/app.db')\n *\n * const items = db.table('items', {\n * name: 'text',\n * createdAt: 'text',\n * })\n *\n * await items.seed([{ name: 'Apple', createdAt: new Date().toISOString() }])\n *\n * // db IS the adapter — pass directly to createApp\n * const app = createApp({ adapter: db, port: 3000 })\n * ```\n */\nexport class RouteStore implements DatabaseAdapter {\n private readonly sql: SQLiteStore\n private readonly listeners = new Map<string, Set<(event: ChangeEvent) => void>>()\n private connected = false\n\n /**\n * @param dbPath - Path to the SQLite file.\n * Parent directories are created automatically.\n * Relative paths resolve from `process.cwd()`.\n */\n constructor(dbPath: string) {\n this.sql = new SQLiteStore(dbPath)\n }\n\n /**\n * Define a table and return a typed CRUD handle.\n * The table is created in SQLite automatically if it does not exist.\n *\n * @param name - Table name\n * @param schema - Column definitions: `{ columnName: 'integer' | 'text' | 'real' | 'json' }`\n *\n * @example\n * ```ts\n * const tasks = db.table('tasks', {\n * title: 'text',\n * done: 'integer', // 0 | 1\n * metadata: 'json', // auto-serialised JS object\n * })\n * ```\n */\n table<S extends SchemaDefinition>(name: string, schema: S): RouteTable<S> {\n return new RouteTable(this.sql, name, schema, (evt) => this.dispatchEvent(evt))\n }\n\n // ── DatabaseAdapter ───────────────────────────────────────────────────────\n\n async connect(): Promise<void> {\n this.connected = true\n }\n\n async disconnect(): Promise<void> {\n this.sql.close()\n this.connected = false\n }\n\n onChange(table: string, callback: (event: ChangeEvent) => void): () => void {\n if (!this.listeners.has(table)) this.listeners.set(table, new Set())\n this.listeners.get(table)!.add(callback)\n return () => this.listeners.get(table)?.delete(callback)\n }\n\n get isConnected(): boolean {\n return this.connected\n }\n\n // ── Internal ──────────────────────────────────────────────────────────────\n\n private dispatchEvent(event: ChangeEvent): void {\n const cbs = this.listeners.get(event.table)\n if (!cbs) return\n for (const cb of cbs) cb(event)\n }\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "routeflow-api",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "RouteFlow — REST API with real-time database push subscriptions",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {