local-lite 0.0.19 → 0.0.20

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.
Files changed (63) hide show
  1. package/README.md +5 -1
  2. package/dist/actions/store.js +1 -1
  3. package/dist/core/client.d.ts +15 -0
  4. package/dist/core/client.js +46 -2
  5. package/dist/index.d.ts +5 -0
  6. package/dist/index.js +5 -0
  7. package/dist/keys/generator.d.ts +38 -0
  8. package/dist/keys/generator.js +136 -0
  9. package/dist/keys/index.d.ts +5 -0
  10. package/dist/keys/index.js +21 -0
  11. package/dist/keys/manager.d.ts +59 -0
  12. package/dist/keys/manager.js +127 -0
  13. package/dist/keys/signed-token.d.ts +18 -0
  14. package/dist/keys/signed-token.js +171 -0
  15. package/dist/keys/store.d.ts +23 -0
  16. package/dist/keys/store.js +109 -0
  17. package/dist/keys/types.d.ts +95 -0
  18. package/dist/keys/types.js +2 -0
  19. package/dist/kv/index.d.ts +3 -0
  20. package/dist/kv/index.js +19 -0
  21. package/dist/kv/manager.d.ts +52 -0
  22. package/dist/kv/manager.js +164 -0
  23. package/dist/kv/store.d.ts +13 -0
  24. package/dist/kv/store.js +85 -0
  25. package/dist/kv/types.d.ts +27 -0
  26. package/dist/kv/types.js +2 -0
  27. package/dist/react/index.d.ts +2 -0
  28. package/dist/react/index.js +2 -0
  29. package/dist/react/kv.d.ts +9 -0
  30. package/dist/react/kv.js +53 -0
  31. package/dist/react/search.d.ts +19 -0
  32. package/dist/react/search.js +49 -0
  33. package/dist/reactive/event-emitter.d.ts +1 -0
  34. package/dist/reactive/event-emitter.js +9 -6
  35. package/dist/scheduler/cron.d.ts +6 -0
  36. package/dist/scheduler/cron.js +62 -0
  37. package/dist/scheduler/index.d.ts +4 -0
  38. package/dist/scheduler/index.js +20 -0
  39. package/dist/scheduler/manager.d.ts +59 -0
  40. package/dist/scheduler/manager.js +186 -0
  41. package/dist/scheduler/store.d.ts +20 -0
  42. package/dist/scheduler/store.js +134 -0
  43. package/dist/scheduler/types.d.ts +44 -0
  44. package/dist/scheduler/types.js +2 -0
  45. package/dist/search/index.d.ts +3 -0
  46. package/dist/search/index.js +19 -0
  47. package/dist/search/manager.d.ts +42 -0
  48. package/dist/search/manager.js +123 -0
  49. package/dist/search/store.d.ts +33 -0
  50. package/dist/search/store.js +137 -0
  51. package/dist/search/types.d.ts +49 -0
  52. package/dist/search/types.js +2 -0
  53. package/dist/storage/driver.d.ts +25 -0
  54. package/dist/storage/driver.js +92 -0
  55. package/dist/storage/index.d.ts +4 -0
  56. package/dist/storage/index.js +20 -0
  57. package/dist/storage/manager.d.ts +48 -0
  58. package/dist/storage/manager.js +149 -0
  59. package/dist/storage/store.d.ts +14 -0
  60. package/dist/storage/store.js +116 -0
  61. package/dist/storage/types.d.ts +44 -0
  62. package/dist/storage/types.js +2 -0
  63. package/package.json +26 -1
@@ -0,0 +1,134 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.JobStore = exports.DEFAULT_JOB_RUNS_TABLE = exports.DEFAULT_JOBS_TABLE = void 0;
4
+ exports.mapJobRow = mapJobRow;
5
+ exports.mapJobRunRow = mapJobRunRow;
6
+ exports.DEFAULT_JOBS_TABLE = '_local_scheduled_jobs';
7
+ exports.DEFAULT_JOB_RUNS_TABLE = '_local_job_runs';
8
+ function mapJobRow(r) {
9
+ let payload = undefined;
10
+ if (r.payload) {
11
+ try {
12
+ payload = typeof r.payload === 'string' ? JSON.parse(r.payload) : r.payload;
13
+ }
14
+ catch {
15
+ payload = r.payload;
16
+ }
17
+ }
18
+ return {
19
+ id: r.id,
20
+ name: r.name,
21
+ schedule: r.schedule,
22
+ intervalMs: Number(r.interval_ms || 0),
23
+ lastRunAt: r.last_run_at != null ? Number(r.last_run_at) : null,
24
+ nextRunAt: Number(r.next_run_at || 0),
25
+ enabled: r.enabled === 1 || r.enabled === '1' || r.enabled === true,
26
+ payload,
27
+ createdAt: Number(r.created_at || 0),
28
+ };
29
+ }
30
+ function mapJobRunRow(r) {
31
+ return {
32
+ id: r.id,
33
+ jobId: r.job_id,
34
+ status: r.status,
35
+ startedAt: Number(r.started_at || 0),
36
+ finishedAt: r.finished_at != null ? Number(r.finished_at) : null,
37
+ durationMs: r.duration_ms != null ? Number(r.duration_ms) : null,
38
+ error: r.error || null,
39
+ };
40
+ }
41
+ class JobStore {
42
+ static async ensureSchema(db, jobsTable = exports.DEFAULT_JOBS_TABLE, runsTable = exports.DEFAULT_JOB_RUNS_TABLE) {
43
+ await db.execute(`CREATE TABLE IF NOT EXISTS ${jobsTable} (
44
+ id TEXT PRIMARY KEY,
45
+ name TEXT NOT NULL,
46
+ schedule TEXT NOT NULL,
47
+ interval_ms INTEGER NOT NULL,
48
+ last_run_at INTEGER,
49
+ next_run_at INTEGER NOT NULL,
50
+ enabled INTEGER NOT NULL DEFAULT 1,
51
+ payload TEXT,
52
+ created_at INTEGER NOT NULL
53
+ );`);
54
+ await db.execute(`CREATE INDEX IF NOT EXISTS idx_${jobsTable}_next ON ${jobsTable} (next_run_at);`);
55
+ await db.execute(`CREATE TABLE IF NOT EXISTS ${runsTable} (
56
+ id TEXT PRIMARY KEY,
57
+ job_id TEXT NOT NULL,
58
+ status TEXT NOT NULL,
59
+ started_at INTEGER NOT NULL,
60
+ finished_at INTEGER,
61
+ duration_ms INTEGER,
62
+ error TEXT
63
+ );`);
64
+ await db.execute(`CREATE INDEX IF NOT EXISTS idx_${runsTable}_job ON ${runsTable} (job_id, started_at);`);
65
+ }
66
+ static async upsertJob(db, jobsTable = exports.DEFAULT_JOBS_TABLE, job) {
67
+ const payloadStr = job.payload !== undefined ? JSON.stringify(job.payload) : null;
68
+ await db.execute(`INSERT INTO ${jobsTable} (id, name, schedule, interval_ms, last_run_at, next_run_at, enabled, payload, created_at)
69
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
70
+ ON CONFLICT(id) DO UPDATE SET
71
+ name = excluded.name,
72
+ schedule = excluded.schedule,
73
+ interval_ms = excluded.interval_ms,
74
+ last_run_at = excluded.last_run_at,
75
+ next_run_at = excluded.next_run_at,
76
+ enabled = excluded.enabled,
77
+ payload = excluded.payload;`, [
78
+ job.id,
79
+ job.name,
80
+ job.schedule,
81
+ job.intervalMs,
82
+ job.lastRunAt ?? null,
83
+ job.nextRunAt,
84
+ job.enabled ? 1 : 0,
85
+ payloadStr,
86
+ job.createdAt,
87
+ ]);
88
+ }
89
+ static async getJob(db, jobsTable = exports.DEFAULT_JOBS_TABLE, id) {
90
+ const rows = await db.execute(`SELECT * FROM ${jobsTable} WHERE id = ? LIMIT 1;`, [id]);
91
+ if (!rows || rows.length === 0)
92
+ return null;
93
+ return mapJobRow(rows[0]);
94
+ }
95
+ static async getAllJobs(db, jobsTable = exports.DEFAULT_JOBS_TABLE) {
96
+ const rows = await db.execute(`SELECT * FROM ${jobsTable} ORDER BY next_run_at ASC;`);
97
+ return (rows || []).map(mapJobRow);
98
+ }
99
+ static async getDueJobs(db, jobsTable = exports.DEFAULT_JOBS_TABLE, now = Date.now()) {
100
+ const rows = await db.execute(`SELECT * FROM ${jobsTable} WHERE enabled = 1 AND next_run_at <= ? ORDER BY next_run_at ASC;`, [now]);
101
+ return (rows || []).map(mapJobRow);
102
+ }
103
+ static async updateJobSchedule(db, jobsTable = exports.DEFAULT_JOBS_TABLE, id, lastRunAt, nextRunAt) {
104
+ await db.execute(`UPDATE ${jobsTable} SET last_run_at = ?, next_run_at = ? WHERE id = ?;`, [lastRunAt, nextRunAt, id]);
105
+ }
106
+ static async setJobEnabled(db, jobsTable = exports.DEFAULT_JOBS_TABLE, id, enabled) {
107
+ await db.execute(`UPDATE ${jobsTable} SET enabled = ? WHERE id = ?;`, [enabled ? 1 : 0, id]);
108
+ }
109
+ static async deleteJob(db, jobsTable = exports.DEFAULT_JOBS_TABLE, id) {
110
+ await db.execute(`DELETE FROM ${jobsTable} WHERE id = ?;`, [id]);
111
+ }
112
+ // --- Run History ---
113
+ static async recordRunStart(db, runsTable = exports.DEFAULT_JOB_RUNS_TABLE, run) {
114
+ await db.execute(`INSERT INTO ${runsTable} (id, job_id, status, started_at, finished_at, duration_ms, error)
115
+ VALUES (?, ?, ?, ?, ?, ?, ?);`, [run.id, run.jobId, run.status, run.startedAt, null, null, null]);
116
+ }
117
+ static async recordRunFinish(db, runsTable = exports.DEFAULT_JOB_RUNS_TABLE, id, status, finishedAt, durationMs, error) {
118
+ await db.execute(`UPDATE ${runsTable} SET status = ?, finished_at = ?, duration_ms = ?, error = ? WHERE id = ?;`, [status, finishedAt, durationMs, error ?? null, id]);
119
+ }
120
+ static async getRuns(db, runsTable = exports.DEFAULT_JOB_RUNS_TABLE, jobId, limit = 50) {
121
+ let rows;
122
+ if (jobId) {
123
+ rows = await db.execute(`SELECT * FROM ${runsTable} WHERE job_id = ? ORDER BY started_at DESC LIMIT ?;`, [jobId, limit]);
124
+ }
125
+ else {
126
+ rows = await db.execute(`SELECT * FROM ${runsTable} ORDER BY started_at DESC LIMIT ?;`, [limit]);
127
+ }
128
+ return (rows || []).map(mapJobRunRow);
129
+ }
130
+ static async pruneRuns(db, runsTable = exports.DEFAULT_JOB_RUNS_TABLE, cutoffTimestamp) {
131
+ await db.execute(`DELETE FROM ${runsTable} WHERE started_at < ?;`, [cutoffTimestamp]);
132
+ }
133
+ }
134
+ exports.JobStore = JobStore;
@@ -0,0 +1,44 @@
1
+ export type ScheduleType = string | {
2
+ intervalMs: number;
3
+ };
4
+ export interface JobDefinition {
5
+ id: string;
6
+ name?: string;
7
+ /**
8
+ * Schedule expression:
9
+ * - Human string (e.g., 'every 10 minutes', 'every 1 hour', 'daily', 'hourly')
10
+ * - Standard 5-field cron (e.g., '* / 15 * * * *', '0 9 * * *')
11
+ * - Object with explicit millisecond interval: { intervalMs: 60000 }
12
+ */
13
+ schedule: ScheduleType;
14
+ /**
15
+ * Function to execute when the job triggers.
16
+ */
17
+ handler?: (job: JobRecord) => Promise<void> | void;
18
+ payload?: any;
19
+ enabled?: boolean;
20
+ }
21
+ export interface JobRecord {
22
+ id: string;
23
+ name: string;
24
+ schedule: string;
25
+ intervalMs: number;
26
+ lastRunAt: number | null;
27
+ nextRunAt: number;
28
+ enabled: boolean;
29
+ payload?: any;
30
+ createdAt: number;
31
+ }
32
+ export interface JobRunRecord {
33
+ id: string;
34
+ jobId: string;
35
+ status: 'SUCCESS' | 'FAILED' | 'RUNNING';
36
+ startedAt: number;
37
+ finishedAt: number | null;
38
+ durationMs: number | null;
39
+ error?: string | null;
40
+ }
41
+ export interface JobSchedulerOptions {
42
+ checkIntervalMs?: number;
43
+ autoStart?: boolean;
44
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,3 @@
1
+ export * from './types.js';
2
+ export * from './store.js';
3
+ export * from './manager.js';
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./types.js"), exports);
18
+ __exportStar(require("./store.js"), exports);
19
+ __exportStar(require("./manager.js"), exports);
@@ -0,0 +1,42 @@
1
+ import { DBExecutable } from '../core/schema-manager.js';
2
+ import { EventEmitter } from '../reactive/event-emitter.js';
3
+ import { SearchIndexConfig, SearchQueryOptions, SearchResult } from './types.js';
4
+ export declare class SearchManager {
5
+ private db;
6
+ private events?;
7
+ private getRowById?;
8
+ private indices;
9
+ private eventListeners;
10
+ constructor(db: DBExecutable, events?: EventEmitter | undefined, getRowById?: ((table: string, id: string) => Promise<any | null>) | undefined);
11
+ /**
12
+ * Registers and creates an FTS5 index for the specified table and columns.
13
+ * If an EventEmitter is available, hooks automatically update the index on mutations.
14
+ */
15
+ createIndex(config: SearchIndexConfig): Promise<void>;
16
+ /**
17
+ * Indexes or updates a single record in the FTS index.
18
+ */
19
+ indexRecord(table: string, rowId: string, values: Record<string, any>): Promise<void>;
20
+ /**
21
+ * Removes a record from the FTS index.
22
+ */
23
+ removeRecord(table: string, rowId: string): Promise<void>;
24
+ /**
25
+ * Rebuilds the search index for a table from an array of existing records.
26
+ */
27
+ rebuildIndex(table: string, records: Array<{
28
+ id: string;
29
+ } & Record<string, any>>): Promise<void>;
30
+ /**
31
+ * Executes a full-text search query, returning ranked matches with optional snippets and hydrated rows.
32
+ */
33
+ query<T = any>(table: string, queryText: string, options?: SearchQueryOptions): Promise<SearchResult<T>[]>;
34
+ /**
35
+ * Drops an index and cleans up auto-sync listeners.
36
+ */
37
+ dropIndex(table: string): Promise<void>;
38
+ /**
39
+ * Returns list of currently indexed table names.
40
+ */
41
+ getIndexedTables(): string[];
42
+ }
@@ -0,0 +1,123 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SearchManager = void 0;
4
+ const store_js_1 = require("./store.js");
5
+ class SearchManager {
6
+ db;
7
+ events;
8
+ getRowById;
9
+ indices = new Map();
10
+ eventListeners = new Map();
11
+ constructor(db, events, getRowById) {
12
+ this.db = db;
13
+ this.events = events;
14
+ this.getRowById = getRowById;
15
+ }
16
+ /**
17
+ * Registers and creates an FTS5 index for the specified table and columns.
18
+ * If an EventEmitter is available, hooks automatically update the index on mutations.
19
+ */
20
+ async createIndex(config) {
21
+ const table = config.table;
22
+ this.indices.set(table, config);
23
+ await store_js_1.SearchStore.ensureFtsTable(this.db, config);
24
+ // Auto-sync via event emitter if configured
25
+ if (this.events && !this.eventListeners.has(table)) {
26
+ const listener = async (e) => {
27
+ try {
28
+ if (e.operation === 'DELETE') {
29
+ await this.removeRecord(table, e.rowId);
30
+ }
31
+ else if (e.payload) {
32
+ await this.indexRecord(table, e.rowId, e.payload);
33
+ }
34
+ }
35
+ catch {
36
+ // Background sync errors shouldn't crash the event bus
37
+ }
38
+ };
39
+ this.events.on(table, listener);
40
+ this.eventListeners.set(table, listener);
41
+ }
42
+ }
43
+ /**
44
+ * Indexes or updates a single record in the FTS index.
45
+ */
46
+ async indexRecord(table, rowId, values) {
47
+ const config = this.indices.get(table);
48
+ const columns = config ? config.columns : Object.keys(values).filter((k) => k !== 'id');
49
+ await store_js_1.SearchStore.indexRow(this.db, table, rowId, columns, values);
50
+ }
51
+ /**
52
+ * Removes a record from the FTS index.
53
+ */
54
+ async removeRecord(table, rowId) {
55
+ await store_js_1.SearchStore.removeRow(this.db, table, rowId);
56
+ }
57
+ /**
58
+ * Rebuilds the search index for a table from an array of existing records.
59
+ */
60
+ async rebuildIndex(table, records) {
61
+ const config = this.indices.get(table);
62
+ if (!config) {
63
+ throw new Error(`Search index not configured for table "${table}". Call createIndex first.`);
64
+ }
65
+ await store_js_1.SearchStore.clearFts(this.db, table);
66
+ for (const record of records) {
67
+ await store_js_1.SearchStore.indexRow(this.db, table, record.id, config.columns, record);
68
+ }
69
+ }
70
+ /**
71
+ * Executes a full-text search query, returning ranked matches with optional snippets and hydrated rows.
72
+ */
73
+ async query(table, queryText, options = {}) {
74
+ const config = this.indices.get(table);
75
+ const columns = config ? config.columns : [];
76
+ const matches = await store_js_1.SearchStore.search(this.db, table, columns, queryText, options);
77
+ const shouldHydrate = options.hydrateRow !== false;
78
+ const results = [];
79
+ for (const match of matches) {
80
+ let data = null;
81
+ if (shouldHydrate) {
82
+ if (this.getRowById) {
83
+ data = await this.getRowById(table, match.rowId);
84
+ }
85
+ else {
86
+ try {
87
+ const rows = await this.db.execute(`SELECT * FROM ${table} WHERE id = ? LIMIT 1`, [match.rowId]);
88
+ data = rows && rows.length > 0 ? rows[0] : null;
89
+ }
90
+ catch {
91
+ data = null;
92
+ }
93
+ }
94
+ }
95
+ results.push({
96
+ rowId: match.rowId,
97
+ rank: match.rank,
98
+ snippet: match.snippet,
99
+ data,
100
+ });
101
+ }
102
+ return results;
103
+ }
104
+ /**
105
+ * Drops an index and cleans up auto-sync listeners.
106
+ */
107
+ async dropIndex(table) {
108
+ this.indices.delete(table);
109
+ const listener = this.eventListeners.get(table);
110
+ if (listener && this.events) {
111
+ this.events.off(table, listener);
112
+ this.eventListeners.delete(table);
113
+ }
114
+ await store_js_1.SearchStore.dropFtsTable(this.db, table);
115
+ }
116
+ /**
117
+ * Returns list of currently indexed table names.
118
+ */
119
+ getIndexedTables() {
120
+ return Array.from(this.indices.keys());
121
+ }
122
+ }
123
+ exports.SearchManager = SearchManager;
@@ -0,0 +1,33 @@
1
+ import { DBExecutable } from '../core/schema-manager.js';
2
+ import { SearchIndexConfig, SearchQueryOptions } from './types.js';
3
+ export declare function getFtsTableName(table: string): string;
4
+ export declare class SearchStore {
5
+ /**
6
+ * Ensures the FTS5 virtual table exists for the given table and columns.
7
+ */
8
+ static ensureFtsTable(db: DBExecutable, config: SearchIndexConfig): Promise<void>;
9
+ /**
10
+ * Drops the FTS5 virtual table.
11
+ */
12
+ static dropFtsTable(db: DBExecutable, table: string): Promise<void>;
13
+ /**
14
+ * Indexes or updates a single row in the FTS table.
15
+ */
16
+ static indexRow(db: DBExecutable, table: string, rowId: string, columns: string[], values: Record<string, any>): Promise<void>;
17
+ /**
18
+ * Removes a single row from the FTS table.
19
+ */
20
+ static removeRow(db: DBExecutable, table: string, rowId: string): Promise<void>;
21
+ /**
22
+ * Clears all rows in the FTS table.
23
+ */
24
+ static clearFts(db: DBExecutable, table: string): Promise<void>;
25
+ /**
26
+ * Executes a full-text search match query against the FTS5 table.
27
+ */
28
+ static search(db: DBExecutable, table: string, columns: string[], queryText: string, options?: SearchQueryOptions): Promise<Array<{
29
+ rowId: string;
30
+ rank: number;
31
+ snippet?: string;
32
+ }>>;
33
+ }
@@ -0,0 +1,137 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SearchStore = void 0;
4
+ exports.getFtsTableName = getFtsTableName;
5
+ function getFtsTableName(table) {
6
+ return `_fts_${table}`;
7
+ }
8
+ class SearchStore {
9
+ /**
10
+ * Ensures the FTS5 virtual table exists for the given table and columns.
11
+ */
12
+ static async ensureFtsTable(db, config) {
13
+ const ftsTable = getFtsTableName(config.table);
14
+ const cols = config.columns.map((c) => c.trim()).filter(Boolean);
15
+ const tokenize = config.tokenize || 'unicode61';
16
+ // row_id is UNINDEXED in FTS5 so it does not pollute search tokens
17
+ const columnDefs = ['row_id UNINDEXED', ...cols].join(', ');
18
+ const sql = `CREATE VIRTUAL TABLE IF NOT EXISTS ${ftsTable} USING fts5(${columnDefs}, tokenize='${tokenize}');`;
19
+ try {
20
+ await db.execute(sql);
21
+ }
22
+ catch (err) {
23
+ // In case tokenize='unicode61' isn't supported by legacy SQLite build, retry without tokenize option
24
+ if (err?.message?.includes('tokenize') || err?.message?.includes('unknown tokenizer')) {
25
+ await db.execute(`CREATE VIRTUAL TABLE IF NOT EXISTS ${ftsTable} USING fts5(${columnDefs});`);
26
+ }
27
+ else {
28
+ throw err;
29
+ }
30
+ }
31
+ }
32
+ /**
33
+ * Drops the FTS5 virtual table.
34
+ */
35
+ static async dropFtsTable(db, table) {
36
+ const ftsTable = getFtsTableName(table);
37
+ await db.execute(`DROP TABLE IF EXISTS ${ftsTable};`);
38
+ }
39
+ /**
40
+ * Indexes or updates a single row in the FTS table.
41
+ */
42
+ static async indexRow(db, table, rowId, columns, values) {
43
+ const ftsTable = getFtsTableName(table);
44
+ // Delete existing entry if any
45
+ await db.execute(`DELETE FROM ${ftsTable} WHERE row_id = ?;`, [rowId]);
46
+ const cols = ['row_id', ...columns];
47
+ const placeholders = cols.map(() => '?').join(', ');
48
+ const params = [
49
+ rowId,
50
+ ...columns.map((c) => {
51
+ const val = values[c];
52
+ if (val === null || val === undefined)
53
+ return '';
54
+ if (typeof val === 'object')
55
+ return JSON.stringify(val);
56
+ return String(val);
57
+ }),
58
+ ];
59
+ await db.execute(`INSERT INTO ${ftsTable} (${cols.join(', ')}) VALUES (${placeholders});`, params);
60
+ }
61
+ /**
62
+ * Removes a single row from the FTS table.
63
+ */
64
+ static async removeRow(db, table, rowId) {
65
+ const ftsTable = getFtsTableName(table);
66
+ await db.execute(`DELETE FROM ${ftsTable} WHERE row_id = ?;`, [rowId]);
67
+ }
68
+ /**
69
+ * Clears all rows in the FTS table.
70
+ */
71
+ static async clearFts(db, table) {
72
+ const ftsTable = getFtsTableName(table);
73
+ await db.execute(`DELETE FROM ${ftsTable};`);
74
+ }
75
+ /**
76
+ * Executes a full-text search match query against the FTS5 table.
77
+ */
78
+ static async search(db, table, columns, queryText, options = {}) {
79
+ const ftsTable = getFtsTableName(table);
80
+ const limit = options.limit ?? 20;
81
+ const offset = options.offset ?? 0;
82
+ const preTag = options.highlightPreTag ?? '<mark>';
83
+ const postTag = options.highlightPostTag ?? '</mark>';
84
+ // Prepare match phrase: escape quotes or sanitize
85
+ // If query has multiple terms, SQLite FTS5 supports prefix matching (e.g. "term*")
86
+ const terms = queryText
87
+ .trim()
88
+ .split(/\s+/)
89
+ .map((t) => t.replace(/["*]/g, ''))
90
+ .filter(Boolean);
91
+ if (terms.length === 0) {
92
+ return [];
93
+ }
94
+ // Search query syntax: each term as prefix search: "term1"* "term2"*
95
+ const matchQuery = terms.map((t) => `"${t}"*`).join(' ');
96
+ // Snippet column index in FTS5: columns are 0-based: col 0 is row_id, col 1 is columns[0], etc.
97
+ // We pick column index 1 (or the first indexed text column) for snippet extraction
98
+ const snippetColIndex = 1;
99
+ let sql = `
100
+ SELECT
101
+ row_id,
102
+ rank,
103
+ snippet(${ftsTable}, ${snippetColIndex}, '${preTag}', '${postTag}', '...', 15) as snippet_text
104
+ FROM ${ftsTable}
105
+ WHERE ${ftsTable} MATCH ?
106
+ ORDER BY rank
107
+ LIMIT ? OFFSET ?;
108
+ `;
109
+ try {
110
+ const rows = await db.execute(sql, [matchQuery, limit, offset]);
111
+ return (rows || []).map((r) => ({
112
+ rowId: String(r.row_id),
113
+ rank: Number(r.rank ?? 0),
114
+ snippet: r.snippet_text ?? undefined,
115
+ }));
116
+ }
117
+ catch (err) {
118
+ // In case snippet() function is unavailable in the environment, fallback to rank-only search
119
+ if (err?.message?.includes('snippet') || err?.message?.includes('no such function')) {
120
+ const fallbackSql = `
121
+ SELECT row_id, rank
122
+ FROM ${ftsTable}
123
+ WHERE ${ftsTable} MATCH ?
124
+ ORDER BY rank
125
+ LIMIT ? OFFSET ?;
126
+ `;
127
+ const rows = await db.execute(fallbackSql, [matchQuery, limit, offset]);
128
+ return (rows || []).map((r) => ({
129
+ rowId: String(r.row_id),
130
+ rank: Number(r.rank ?? 0),
131
+ }));
132
+ }
133
+ throw err;
134
+ }
135
+ }
136
+ }
137
+ exports.SearchStore = SearchStore;
@@ -0,0 +1,49 @@
1
+ export interface SearchIndexConfig {
2
+ /**
3
+ * The name of the table to index (e.g. 'properties', 'tenants').
4
+ */
5
+ table: string;
6
+ /**
7
+ * The text columns to include in the full-text search index.
8
+ */
9
+ columns: string[];
10
+ /**
11
+ * SQLite FTS tokenizer (e.g. 'unicode61', 'porter', 'ascii').
12
+ * Default: 'unicode61'
13
+ */
14
+ tokenize?: string;
15
+ }
16
+ export interface SearchQueryOptions {
17
+ /**
18
+ * Maximum search results to return (default: 20).
19
+ */
20
+ limit?: number;
21
+ /**
22
+ * Pagination offset.
23
+ */
24
+ offset?: number;
25
+ /**
26
+ * Whether to include a highlighted snippet matching the search query.
27
+ * Default: true
28
+ */
29
+ highlight?: boolean;
30
+ /**
31
+ * Tag prefix for highlights (default: '<mark>').
32
+ */
33
+ highlightPreTag?: string;
34
+ /**
35
+ * Tag suffix for highlights (default: '</mark>').
36
+ */
37
+ highlightPostTag?: string;
38
+ /**
39
+ * Whether to hydrate the full row record from the base table.
40
+ * Default: true
41
+ */
42
+ hydrateRow?: boolean;
43
+ }
44
+ export interface SearchResult<T = any> {
45
+ rowId: string;
46
+ rank: number;
47
+ snippet?: string;
48
+ data?: T | null;
49
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,25 @@
1
+ import { FileDriver } from './types.js';
2
+ /**
3
+ * Universal in-memory driver (ideal for tests, memory caches, and browser storage fallback).
4
+ */
5
+ export declare class MemoryFileDriver implements FileDriver {
6
+ private files;
7
+ write(key: string, data: Uint8Array): Promise<string>;
8
+ read(key: string): Promise<Uint8Array | null>;
9
+ delete(key: string): Promise<void>;
10
+ exists(key: string): Promise<boolean>;
11
+ clear(): void;
12
+ }
13
+ /**
14
+ * Node.js filesystem driver for writing blobs to disk.
15
+ */
16
+ export declare class NodeFileDriver implements FileDriver {
17
+ private baseDir;
18
+ constructor(baseDir?: string);
19
+ private getFs;
20
+ private getResolvedPath;
21
+ write(key: string, data: Uint8Array): Promise<string>;
22
+ read(key: string): Promise<Uint8Array | null>;
23
+ delete(key: string): Promise<void>;
24
+ exists(key: string): Promise<boolean>;
25
+ }