local-lite 0.0.19 → 0.0.21
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/README.md +5 -1
- package/dist/actions/store.js +1 -1
- package/dist/auth/index.d.ts +1 -0
- package/dist/auth/index.js +1 -0
- package/dist/auth/session.d.ts +107 -0
- package/dist/auth/session.js +259 -0
- package/dist/core/client.d.ts +18 -0
- package/dist/core/client.js +55 -2
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/keys/generator.d.ts +38 -0
- package/dist/keys/generator.js +136 -0
- package/dist/keys/index.d.ts +5 -0
- package/dist/keys/index.js +21 -0
- package/dist/keys/manager.d.ts +59 -0
- package/dist/keys/manager.js +127 -0
- package/dist/keys/signed-token.d.ts +18 -0
- package/dist/keys/signed-token.js +171 -0
- package/dist/keys/store.d.ts +23 -0
- package/dist/keys/store.js +109 -0
- package/dist/keys/types.d.ts +95 -0
- package/dist/keys/types.js +2 -0
- package/dist/kv/index.d.ts +3 -0
- package/dist/kv/index.js +19 -0
- package/dist/kv/manager.d.ts +52 -0
- package/dist/kv/manager.js +164 -0
- package/dist/kv/store.d.ts +13 -0
- package/dist/kv/store.js +85 -0
- package/dist/kv/types.d.ts +27 -0
- package/dist/kv/types.js +2 -0
- package/dist/react/index.d.ts +3 -0
- package/dist/react/index.js +3 -0
- package/dist/react/kv.d.ts +9 -0
- package/dist/react/kv.js +53 -0
- package/dist/react/search.d.ts +19 -0
- package/dist/react/search.js +49 -0
- package/dist/react/session.d.ts +26 -0
- package/dist/react/session.js +118 -0
- package/dist/reactive/event-emitter.d.ts +1 -0
- package/dist/reactive/event-emitter.js +9 -6
- package/dist/scheduler/cron.d.ts +6 -0
- package/dist/scheduler/cron.js +62 -0
- package/dist/scheduler/index.d.ts +4 -0
- package/dist/scheduler/index.js +20 -0
- package/dist/scheduler/manager.d.ts +59 -0
- package/dist/scheduler/manager.js +186 -0
- package/dist/scheduler/store.d.ts +20 -0
- package/dist/scheduler/store.js +134 -0
- package/dist/scheduler/types.d.ts +44 -0
- package/dist/scheduler/types.js +2 -0
- package/dist/search/index.d.ts +3 -0
- package/dist/search/index.js +19 -0
- package/dist/search/manager.d.ts +42 -0
- package/dist/search/manager.js +123 -0
- package/dist/search/store.d.ts +33 -0
- package/dist/search/store.js +137 -0
- package/dist/search/types.d.ts +49 -0
- package/dist/search/types.js +2 -0
- package/dist/storage/driver.d.ts +25 -0
- package/dist/storage/driver.js +92 -0
- package/dist/storage/index.d.ts +4 -0
- package/dist/storage/index.js +20 -0
- package/dist/storage/manager.d.ts +48 -0
- package/dist/storage/manager.js +149 -0
- package/dist/storage/store.d.ts +14 -0
- package/dist/storage/store.js +116 -0
- package/dist/storage/types.d.ts +44 -0
- package/dist/storage/types.js +2 -0
- package/package.json +26 -1
|
@@ -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,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,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
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.NodeFileDriver = exports.MemoryFileDriver = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Universal in-memory driver (ideal for tests, memory caches, and browser storage fallback).
|
|
6
|
+
*/
|
|
7
|
+
class MemoryFileDriver {
|
|
8
|
+
files = new Map();
|
|
9
|
+
async write(key, data) {
|
|
10
|
+
this.files.set(key, new Uint8Array(data));
|
|
11
|
+
return `memory://${key}`;
|
|
12
|
+
}
|
|
13
|
+
async read(key) {
|
|
14
|
+
const file = this.files.get(key);
|
|
15
|
+
return file ? new Uint8Array(file) : null;
|
|
16
|
+
}
|
|
17
|
+
async delete(key) {
|
|
18
|
+
this.files.delete(key);
|
|
19
|
+
}
|
|
20
|
+
async exists(key) {
|
|
21
|
+
return this.files.has(key);
|
|
22
|
+
}
|
|
23
|
+
clear() {
|
|
24
|
+
this.files.clear();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
exports.MemoryFileDriver = MemoryFileDriver;
|
|
28
|
+
/**
|
|
29
|
+
* Node.js filesystem driver for writing blobs to disk.
|
|
30
|
+
*/
|
|
31
|
+
class NodeFileDriver {
|
|
32
|
+
baseDir;
|
|
33
|
+
constructor(baseDir = './.local-lite-blobs') {
|
|
34
|
+
this.baseDir = baseDir;
|
|
35
|
+
}
|
|
36
|
+
async getFs() {
|
|
37
|
+
try {
|
|
38
|
+
const fs = await import('node:fs/promises');
|
|
39
|
+
const path = await import('node:path');
|
|
40
|
+
return { fs, path };
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
throw new Error('NodeFileDriver is only supported in Node.js environments.');
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
async getResolvedPath(key) {
|
|
47
|
+
const { path } = await this.getFs();
|
|
48
|
+
// sanitize key to prevent directory traversal
|
|
49
|
+
const safeKey = key.replace(/\.\./g, '_');
|
|
50
|
+
return path.resolve(this.baseDir, safeKey);
|
|
51
|
+
}
|
|
52
|
+
async write(key, data) {
|
|
53
|
+
const { fs, path } = await this.getFs();
|
|
54
|
+
const filePath = await this.getResolvedPath(key);
|
|
55
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
56
|
+
await fs.writeFile(filePath, data);
|
|
57
|
+
return filePath;
|
|
58
|
+
}
|
|
59
|
+
async read(key) {
|
|
60
|
+
const { fs } = await this.getFs();
|
|
61
|
+
const filePath = await this.getResolvedPath(key);
|
|
62
|
+
try {
|
|
63
|
+
const buffer = await fs.readFile(filePath);
|
|
64
|
+
return new Uint8Array(buffer);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
async delete(key) {
|
|
71
|
+
const { fs } = await this.getFs();
|
|
72
|
+
const filePath = await this.getResolvedPath(key);
|
|
73
|
+
try {
|
|
74
|
+
await fs.unlink(filePath);
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
// file might already be removed
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
async exists(key) {
|
|
81
|
+
const { fs } = await this.getFs();
|
|
82
|
+
const filePath = await this.getResolvedPath(key);
|
|
83
|
+
try {
|
|
84
|
+
await fs.access(filePath);
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
exports.NodeFileDriver = NodeFileDriver;
|
|
@@ -0,0 +1,20 @@
|
|
|
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("./driver.js"), exports);
|
|
19
|
+
__exportStar(require("./store.js"), exports);
|
|
20
|
+
__exportStar(require("./manager.js"), exports);
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { DBExecutable } from '../core/schema-manager.js';
|
|
2
|
+
import { BlobRecord, BlobStorageManagerOptions, BlobUploadStatus, FileDriver, RemoteStorageUploader, SaveBlobOptions } from './types.js';
|
|
3
|
+
export declare class BlobStorageManager {
|
|
4
|
+
private db;
|
|
5
|
+
private driver;
|
|
6
|
+
private uploader?;
|
|
7
|
+
private network?;
|
|
8
|
+
private tableName;
|
|
9
|
+
constructor(db: DBExecutable, options?: BlobStorageManagerOptions);
|
|
10
|
+
initialize(): Promise<void>;
|
|
11
|
+
setDriver(driver: FileDriver): void;
|
|
12
|
+
setUploader(uploader: RemoteStorageUploader): void;
|
|
13
|
+
/**
|
|
14
|
+
* Saves a binary blob or file locally. Records metadata in SQLite and writes binary to driver.
|
|
15
|
+
* If online and uploader configured, can immediately trigger upload.
|
|
16
|
+
*/
|
|
17
|
+
save(key: string, data: Uint8Array | string, options?: SaveBlobOptions): Promise<BlobRecord>;
|
|
18
|
+
/**
|
|
19
|
+
* Retrieves blob record and raw binary data.
|
|
20
|
+
*/
|
|
21
|
+
get(key: string): Promise<{
|
|
22
|
+
record: BlobRecord;
|
|
23
|
+
data: Uint8Array;
|
|
24
|
+
} | null>;
|
|
25
|
+
/**
|
|
26
|
+
* Reads raw binary data for key.
|
|
27
|
+
*/
|
|
28
|
+
getData(key: string): Promise<Uint8Array | null>;
|
|
29
|
+
/**
|
|
30
|
+
* Retrieves blob metadata record.
|
|
31
|
+
*/
|
|
32
|
+
getRecord(key: string): Promise<BlobRecord | null>;
|
|
33
|
+
/**
|
|
34
|
+
* Lists blob metadata records, optionally filtered by upload status.
|
|
35
|
+
*/
|
|
36
|
+
list(status?: BlobUploadStatus): Promise<BlobRecord[]>;
|
|
37
|
+
/**
|
|
38
|
+
* Deletes the blob binary and metadata record.
|
|
39
|
+
*/
|
|
40
|
+
delete(key: string): Promise<void>;
|
|
41
|
+
/**
|
|
42
|
+
* Flushes and syncs pending or failed uploads to the remote storage uploader.
|
|
43
|
+
*/
|
|
44
|
+
syncPending(): Promise<{
|
|
45
|
+
uploaded: number;
|
|
46
|
+
failed: number;
|
|
47
|
+
}>;
|
|
48
|
+
}
|