pomegranate-db 0.1.0
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/LICENSE +21 -0
- package/NOTICE.md +38 -0
- package/PomegranateDB.podspec +67 -0
- package/README.md +122 -0
- package/dist/adapters/expo-sqlite/ExpoSQLiteDriver.d.ts +34 -0
- package/dist/adapters/expo-sqlite/ExpoSQLiteDriver.js +155 -0
- package/dist/adapters/expo-sqlite/index.d.ts +2 -0
- package/dist/adapters/expo-sqlite/index.js +6 -0
- package/dist/adapters/index.d.ts +7 -0
- package/dist/adapters/index.js +13 -0
- package/dist/adapters/loki/LokiAdapter.d.ts +100 -0
- package/dist/adapters/loki/LokiAdapter.js +144 -0
- package/dist/adapters/loki/index.d.ts +6 -0
- package/dist/adapters/loki/index.js +12 -0
- package/dist/adapters/loki/worker/LokiDispatcher.d.ts +21 -0
- package/dist/adapters/loki/worker/LokiDispatcher.js +63 -0
- package/dist/adapters/loki/worker/LokiExecutor.d.ts +96 -0
- package/dist/adapters/loki/worker/LokiExecutor.js +462 -0
- package/dist/adapters/loki/worker/SynchronousWorker.d.ts +22 -0
- package/dist/adapters/loki/worker/SynchronousWorker.js +76 -0
- package/dist/adapters/loki/worker/loki.worker.d.ts +14 -0
- package/dist/adapters/loki/worker/loki.worker.js +112 -0
- package/dist/adapters/loki/worker/types.d.ts +44 -0
- package/dist/adapters/loki/worker/types.js +11 -0
- package/dist/adapters/native-sqlite/NativeSQLiteDriver.d.ts +55 -0
- package/dist/adapters/native-sqlite/NativeSQLiteDriver.js +145 -0
- package/dist/adapters/native-sqlite/index.d.ts +2 -0
- package/dist/adapters/native-sqlite/index.js +6 -0
- package/dist/adapters/op-sqlite/OpSQLiteDriver.d.ts +49 -0
- package/dist/adapters/op-sqlite/OpSQLiteDriver.js +140 -0
- package/dist/adapters/op-sqlite/index.d.ts +2 -0
- package/dist/adapters/op-sqlite/index.js +6 -0
- package/dist/adapters/sqlite/SQLiteAdapter.d.ts +70 -0
- package/dist/adapters/sqlite/SQLiteAdapter.js +264 -0
- package/dist/adapters/sqlite/index.d.ts +2 -0
- package/dist/adapters/sqlite/index.js +6 -0
- package/dist/adapters/sqlite/sql.d.ts +35 -0
- package/dist/adapters/sqlite/sql.js +258 -0
- package/dist/adapters/types.d.ts +93 -0
- package/dist/adapters/types.js +9 -0
- package/dist/collection/Collection.d.ts +103 -0
- package/dist/collection/Collection.js +245 -0
- package/dist/collection/index.d.ts +2 -0
- package/dist/collection/index.js +6 -0
- package/dist/database/Database.d.ts +128 -0
- package/dist/database/Database.js +245 -0
- package/dist/database/index.d.ts +2 -0
- package/dist/database/index.js +6 -0
- package/dist/encryption/index.d.ts +62 -0
- package/dist/encryption/index.js +276 -0
- package/dist/encryption/nodeCrypto.d.ts +18 -0
- package/dist/encryption/nodeCrypto.js +25 -0
- package/dist/encryption/nodeCrypto.native.d.ts +13 -0
- package/dist/encryption/nodeCrypto.native.js +26 -0
- package/dist/expo.d.ts +12 -0
- package/dist/expo.js +32 -0
- package/dist/hooks/index.d.ts +115 -0
- package/dist/hooks/index.js +285 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.js +57 -0
- package/dist/model/Model.d.ts +92 -0
- package/dist/model/Model.js +251 -0
- package/dist/model/index.d.ts +2 -0
- package/dist/model/index.js +7 -0
- package/dist/observable/Subject.d.ts +60 -0
- package/dist/observable/Subject.js +132 -0
- package/dist/observable/index.d.ts +2 -0
- package/dist/observable/index.js +10 -0
- package/dist/query/QueryBuilder.d.ts +51 -0
- package/dist/query/QueryBuilder.js +165 -0
- package/dist/query/index.d.ts +2 -0
- package/dist/query/index.js +7 -0
- package/dist/query/types.d.ts +60 -0
- package/dist/query/types.js +9 -0
- package/dist/schema/builder.d.ts +68 -0
- package/dist/schema/builder.js +168 -0
- package/dist/schema/index.d.ts +2 -0
- package/dist/schema/index.js +7 -0
- package/dist/schema/types.d.ts +108 -0
- package/dist/schema/types.js +9 -0
- package/dist/sync/index.d.ts +2 -0
- package/dist/sync/index.js +6 -0
- package/dist/sync/sync.d.ts +15 -0
- package/dist/sync/sync.js +182 -0
- package/dist/sync/types.d.ts +41 -0
- package/dist/sync/types.js +6 -0
- package/dist/utils/index.d.ts +45 -0
- package/dist/utils/index.js +99 -0
- package/expo-plugin/index.d.ts +68 -0
- package/expo-plugin/index.js +83 -0
- package/native/android-jsi/build.gradle +45 -0
- package/native/android-jsi/src/main/AndroidManifest.xml +2 -0
- package/native/android-jsi/src/main/cpp/CMakeLists.txt +73 -0
- package/native/android-jsi/src/main/cpp/DatabasePlatformAndroid.cpp +107 -0
- package/native/android-jsi/src/main/cpp/DatabasePlatformAndroid.h +16 -0
- package/native/android-jsi/src/main/cpp/JSIInstaller.cpp +27 -0
- package/native/android-jsi/src/main/java/com/pomegranate/jsi/JSIInstaller.kt +43 -0
- package/native/android-jsi/src/main/java/com/pomegranate/jsi/PomegranateJSIModule.kt +39 -0
- package/native/android-jsi/src/main/java/com/pomegranate/jsi/PomegranateJSIPackage.kt +17 -0
- package/native/ios/DatabasePlatformIOS.mm +83 -0
- package/native/ios/PomegranateJSI.h +15 -0
- package/native/ios/PomegranateJSI.mm +59 -0
- package/native/shared/Database.cpp +283 -0
- package/native/shared/Database.h +84 -0
- package/native/shared/Sqlite.cpp +61 -0
- package/native/shared/Sqlite.h +67 -0
- package/native/shared/sqlite3/sqlite3.c +260493 -0
- package/native/shared/sqlite3/sqlite3.h +13583 -0
- package/package.json +127 -0
- package/react-native.config.js +28 -0
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* SQLite Adapter.
|
|
4
|
+
*
|
|
5
|
+
* Implements the StorageAdapter interface using a SQL driver.
|
|
6
|
+
* The actual SQLite driver is injected — this adapter generates SQL
|
|
7
|
+
* and delegates execution, enabling different drivers for
|
|
8
|
+
* React Native (react-native-sqlite-storage) and web (sql.js).
|
|
9
|
+
*
|
|
10
|
+
* The driver interface is intentionally minimal so it can wrap
|
|
11
|
+
* any SQLite library.
|
|
12
|
+
*/
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.SQLiteAdapter = void 0;
|
|
15
|
+
const sql_1 = require("./sql");
|
|
16
|
+
// ─── SQLite Adapter ───────────────────────────────────────────────────────
|
|
17
|
+
class SQLiteAdapter {
|
|
18
|
+
_driver;
|
|
19
|
+
_databaseName;
|
|
20
|
+
_encryption;
|
|
21
|
+
_initialized = false;
|
|
22
|
+
constructor(config) {
|
|
23
|
+
this._databaseName = config.databaseName;
|
|
24
|
+
this._encryption = config.encryption;
|
|
25
|
+
if (config.driver) {
|
|
26
|
+
this._driver = config.driver;
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
// Use a no-op driver that throws — user must provide one
|
|
30
|
+
this._driver = createStubDriver();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
// ─── Initialize ──────────────────────────────────────────────────────
|
|
34
|
+
async initialize(schema) {
|
|
35
|
+
if (this._initialized)
|
|
36
|
+
return;
|
|
37
|
+
await this._driver.open(this._databaseName);
|
|
38
|
+
// Create metadata table
|
|
39
|
+
await this._driver.execute(`CREATE TABLE IF NOT EXISTS "__pomegranate_metadata" (
|
|
40
|
+
"key" TEXT PRIMARY KEY NOT NULL,
|
|
41
|
+
"value" TEXT
|
|
42
|
+
)`);
|
|
43
|
+
// Check existing version
|
|
44
|
+
const rows = await this._driver.query('SELECT "value" FROM "__pomegranate_metadata" WHERE "key" = \'schema_version\'');
|
|
45
|
+
const existingVersion = rows.length > 0 ? Number.parseInt(rows[0].value, 10) : 0;
|
|
46
|
+
if (existingVersion === 0) {
|
|
47
|
+
// Fresh install — create all tables
|
|
48
|
+
for (const table of schema.tables) {
|
|
49
|
+
const sql = (0, sql_1.createTableSQL)(table);
|
|
50
|
+
// Split multi-statement SQL
|
|
51
|
+
for (const stmt of sql.split(';\n')) {
|
|
52
|
+
const trimmed = stmt.trim().replace(/;$/, '');
|
|
53
|
+
if (trimmed) {
|
|
54
|
+
await this._driver.execute(trimmed);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// Store version
|
|
59
|
+
await this._driver.execute('INSERT OR REPLACE INTO "__pomegranate_metadata" ("key", "value") VALUES (\'schema_version\', ?)', [String(schema.version)]);
|
|
60
|
+
}
|
|
61
|
+
this._initialized = true;
|
|
62
|
+
}
|
|
63
|
+
// ─── Query ──────────────────────────────────────────────────────────
|
|
64
|
+
async find(query) {
|
|
65
|
+
const { sql, bindings } = (0, sql_1.selectSQL)(query);
|
|
66
|
+
const rows = await this._driver.query(sql, bindings);
|
|
67
|
+
return rows;
|
|
68
|
+
}
|
|
69
|
+
async count(query) {
|
|
70
|
+
const { sql, bindings } = (0, sql_1.countSQL)(query);
|
|
71
|
+
const rows = await this._driver.query(sql, bindings);
|
|
72
|
+
return rows[0]?.count ?? 0;
|
|
73
|
+
}
|
|
74
|
+
async findById(table, id) {
|
|
75
|
+
const rows = await this._driver.query(`SELECT * FROM "${table}" WHERE "id" = ?`, [id]);
|
|
76
|
+
return rows[0] ?? null;
|
|
77
|
+
}
|
|
78
|
+
// ─── Insert / Update / Delete ────────────────────────────────────────
|
|
79
|
+
async insert(table, raw) {
|
|
80
|
+
const { sql, bindings } = (0, sql_1.insertSQL)(table, raw);
|
|
81
|
+
await this._driver.execute(sql, bindings);
|
|
82
|
+
}
|
|
83
|
+
async update(table, raw) {
|
|
84
|
+
const { sql, bindings } = (0, sql_1.updateSQL)(table, raw);
|
|
85
|
+
await this._driver.execute(sql, bindings);
|
|
86
|
+
}
|
|
87
|
+
async markAsDeleted(table, id) {
|
|
88
|
+
await this._driver.execute(`UPDATE "${table}" SET "_status" = 'deleted' WHERE "id" = ?`, [id]);
|
|
89
|
+
}
|
|
90
|
+
async destroyPermanently(table, id) {
|
|
91
|
+
const { sql, bindings } = (0, sql_1.deleteSQL)(table, id);
|
|
92
|
+
await this._driver.execute(sql, bindings);
|
|
93
|
+
}
|
|
94
|
+
// ─── Batch ──────────────────────────────────────────────────────────
|
|
95
|
+
async batch(operations) {
|
|
96
|
+
await this._driver.executeInTransaction(async () => {
|
|
97
|
+
for (const op of operations) {
|
|
98
|
+
switch (op.type) {
|
|
99
|
+
case 'create': {
|
|
100
|
+
const { sql, bindings } = (0, sql_1.insertSQL)(op.table, op.rawRecord);
|
|
101
|
+
await this._driver.execute(sql, bindings);
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
case 'update': {
|
|
105
|
+
const { sql, bindings } = (0, sql_1.updateSQL)(op.table, op.rawRecord);
|
|
106
|
+
await this._driver.execute(sql, bindings);
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
case 'delete':
|
|
110
|
+
await this._driver.execute(`UPDATE "${op.table}" SET "_status" = 'deleted' WHERE "id" = ?`, [op.id]);
|
|
111
|
+
break;
|
|
112
|
+
case 'destroyPermanently': {
|
|
113
|
+
const { sql, bindings } = (0, sql_1.deleteSQL)(op.table, op.id);
|
|
114
|
+
await this._driver.execute(sql, bindings);
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
// ─── Search ──────────────────────────────────────────────────────────
|
|
122
|
+
async search(descriptor) {
|
|
123
|
+
const { sql, countSql, bindings, countBindings } = (0, sql_1.searchSQL)(descriptor);
|
|
124
|
+
const [rows, countRows] = await Promise.all([
|
|
125
|
+
this._driver.query(sql, bindings),
|
|
126
|
+
this._driver.query(countSql, countBindings),
|
|
127
|
+
]);
|
|
128
|
+
return {
|
|
129
|
+
records: rows,
|
|
130
|
+
total: countRows[0]?.count ?? 0,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
// ─── Sync helpers ──────────────────────────────────────────────────
|
|
134
|
+
async getLocalChanges(tables) {
|
|
135
|
+
const result = {};
|
|
136
|
+
for (const table of tables) {
|
|
137
|
+
const created = await this._driver.query(`SELECT * FROM "${table}" WHERE "_status" = 'created'`);
|
|
138
|
+
const updated = await this._driver.query(`SELECT * FROM "${table}" WHERE "_status" = 'updated'`);
|
|
139
|
+
const deletedRows = await this._driver.query(`SELECT "id" FROM "${table}" WHERE "_status" = 'deleted'`);
|
|
140
|
+
result[table] = {
|
|
141
|
+
created: created,
|
|
142
|
+
updated: updated,
|
|
143
|
+
deleted: deletedRows.map((r) => r.id),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
return result;
|
|
147
|
+
}
|
|
148
|
+
async applyRemoteChanges(changes) {
|
|
149
|
+
await this._driver.executeInTransaction(async () => {
|
|
150
|
+
for (const [table, tableChanges] of Object.entries(changes)) {
|
|
151
|
+
// Apply created records
|
|
152
|
+
for (const raw of tableChanges.created) {
|
|
153
|
+
const record = { ...raw, _status: 'synced', _changed: '' };
|
|
154
|
+
// Use INSERT OR REPLACE in case the record already exists locally
|
|
155
|
+
const { sql, bindings } = (0, sql_1.insertSQL)(table, record);
|
|
156
|
+
const replaceSql = sql.replace('INSERT INTO', 'INSERT OR REPLACE INTO');
|
|
157
|
+
await this._driver.execute(replaceSql, bindings);
|
|
158
|
+
}
|
|
159
|
+
// Apply updated records
|
|
160
|
+
for (const raw of tableChanges.updated) {
|
|
161
|
+
const record = { ...raw, _status: 'synced', _changed: '' };
|
|
162
|
+
// Check if record exists
|
|
163
|
+
const existing = await this._driver.query(`SELECT "_status" FROM "${table}" WHERE "id" = ?`, [raw.id]);
|
|
164
|
+
if (existing.length > 0) {
|
|
165
|
+
const { sql, bindings } = (0, sql_1.updateSQL)(table, record);
|
|
166
|
+
await this._driver.execute(sql, bindings);
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
const { sql, bindings } = (0, sql_1.insertSQL)(table, record);
|
|
170
|
+
await this._driver.execute(sql, bindings);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
// Apply deletions
|
|
174
|
+
for (const id of tableChanges.deleted) {
|
|
175
|
+
await this._driver.execute(`DELETE FROM "${table}" WHERE "id" = ?`, [id]);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
async markAsSynced(table, ids) {
|
|
181
|
+
if (ids.length === 0)
|
|
182
|
+
return;
|
|
183
|
+
const placeholders = ids.map(() => '?').join(', ');
|
|
184
|
+
await this._driver.execute(`UPDATE "${table}" SET "_status" = 'synced', "_changed" = '' WHERE "id" IN (${placeholders})`, ids);
|
|
185
|
+
}
|
|
186
|
+
// ─── Schema version ──────────────────────────────────────────────────
|
|
187
|
+
async getSchemaVersion() {
|
|
188
|
+
try {
|
|
189
|
+
const rows = await this._driver.query('SELECT "value" FROM "__pomegranate_metadata" WHERE "key" = \'schema_version\'');
|
|
190
|
+
return rows.length > 0 ? Number.parseInt(rows[0].value, 10) : 0;
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
return 0;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
// ─── Migration ──────────────────────────────────────────────────────
|
|
197
|
+
async migrate(migrations) {
|
|
198
|
+
const currentVersion = await this.getSchemaVersion();
|
|
199
|
+
const applicable = migrations
|
|
200
|
+
.filter((m) => m.fromVersion >= currentVersion)
|
|
201
|
+
.toSorted((a, b) => a.fromVersion - b.fromVersion);
|
|
202
|
+
await this._driver.executeInTransaction(async () => {
|
|
203
|
+
for (const migration of applicable) {
|
|
204
|
+
for (const step of migration.steps) {
|
|
205
|
+
switch (step.type) {
|
|
206
|
+
case 'createTable': {
|
|
207
|
+
const sql = (0, sql_1.createTableSQL)(step.schema);
|
|
208
|
+
for (const stmt of sql.split(';\n')) {
|
|
209
|
+
const trimmed = stmt.trim().replace(/;$/, '');
|
|
210
|
+
if (trimmed)
|
|
211
|
+
await this._driver.execute(trimmed);
|
|
212
|
+
}
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
case 'addColumn':
|
|
216
|
+
await this._driver.execute(`ALTER TABLE "${step.table}" ADD COLUMN "${step.column}" ${step.columnType}${step.isOptional ? '' : ' NOT NULL DEFAULT ""'}`);
|
|
217
|
+
break;
|
|
218
|
+
case 'destroyTable':
|
|
219
|
+
await this._driver.execute(`DROP TABLE IF EXISTS "${step.table}"`);
|
|
220
|
+
break;
|
|
221
|
+
case 'sql':
|
|
222
|
+
await this._driver.execute(step.query);
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
await this._driver.execute('INSERT OR REPLACE INTO "__pomegranate_metadata" ("key", "value") VALUES (\'schema_version\', ?)', [String(migration.toVersion)]);
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
// ─── Reset ──────────────────────────────────────────────────────────
|
|
231
|
+
async reset() {
|
|
232
|
+
// Get all user tables
|
|
233
|
+
const tables = await this._driver.query('SELECT name FROM sqlite_master WHERE type=\'table\' AND name NOT LIKE \'sqlite_%\'');
|
|
234
|
+
await this._driver.executeInTransaction(async () => {
|
|
235
|
+
for (const t of tables) {
|
|
236
|
+
await this._driver.execute(`DROP TABLE IF EXISTS "${t.name}"`);
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
this._initialized = false;
|
|
240
|
+
}
|
|
241
|
+
// ─── Close ──────────────────────────────────────────────────────────
|
|
242
|
+
async close() {
|
|
243
|
+
await this._driver.close();
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
exports.SQLiteAdapter = SQLiteAdapter;
|
|
247
|
+
// ─── Stub Driver ──────────────────────────────────────────────────────────
|
|
248
|
+
function notConfigured() {
|
|
249
|
+
throw new Error('No SQLite driver configured. Provide a driver in SQLiteAdapterConfig.driver, ' +
|
|
250
|
+
'or use LokiAdapter for in-memory/web use.');
|
|
251
|
+
}
|
|
252
|
+
function createStubDriver() {
|
|
253
|
+
return {
|
|
254
|
+
open: async () => notConfigured(),
|
|
255
|
+
execute: async () => notConfigured(),
|
|
256
|
+
query: async () => {
|
|
257
|
+
notConfigured();
|
|
258
|
+
return [];
|
|
259
|
+
},
|
|
260
|
+
executeInTransaction: async () => notConfigured(),
|
|
261
|
+
close: async () => { },
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
//# sourceMappingURL=SQLiteAdapter.js.map
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SQLiteAdapter = void 0;
|
|
4
|
+
var SQLiteAdapter_1 = require("./SQLiteAdapter");
|
|
5
|
+
Object.defineProperty(exports, "SQLiteAdapter", { enumerable: true, get: function () { return SQLiteAdapter_1.SQLiteAdapter; } });
|
|
6
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQL generation utilities.
|
|
3
|
+
*
|
|
4
|
+
* Translates QueryDescriptor / SearchDescriptor into SQL strings + bindings.
|
|
5
|
+
* Used by the SQLite adapter.
|
|
6
|
+
*/
|
|
7
|
+
import type { QueryDescriptor, SearchDescriptor } from '../../query/types';
|
|
8
|
+
import type { TableSchema } from '../../schema/types';
|
|
9
|
+
export declare function createTableSQL(table: TableSchema): string;
|
|
10
|
+
export declare function selectSQL(descriptor: QueryDescriptor): {
|
|
11
|
+
sql: string;
|
|
12
|
+
bindings: unknown[];
|
|
13
|
+
};
|
|
14
|
+
export declare function countSQL(descriptor: QueryDescriptor): {
|
|
15
|
+
sql: string;
|
|
16
|
+
bindings: unknown[];
|
|
17
|
+
};
|
|
18
|
+
export declare function searchSQL(descriptor: SearchDescriptor): {
|
|
19
|
+
sql: string;
|
|
20
|
+
countSql: string;
|
|
21
|
+
bindings: unknown[];
|
|
22
|
+
countBindings: unknown[];
|
|
23
|
+
};
|
|
24
|
+
export declare function insertSQL(table: string, raw: Record<string, unknown>): {
|
|
25
|
+
sql: string;
|
|
26
|
+
bindings: unknown[];
|
|
27
|
+
};
|
|
28
|
+
export declare function updateSQL(table: string, raw: Record<string, unknown>): {
|
|
29
|
+
sql: string;
|
|
30
|
+
bindings: unknown[];
|
|
31
|
+
};
|
|
32
|
+
export declare function deleteSQL(table: string, id: string): {
|
|
33
|
+
sql: string;
|
|
34
|
+
bindings: unknown[];
|
|
35
|
+
};
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* SQL generation utilities.
|
|
4
|
+
*
|
|
5
|
+
* Translates QueryDescriptor / SearchDescriptor into SQL strings + bindings.
|
|
6
|
+
* Used by the SQLite adapter.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.createTableSQL = createTableSQL;
|
|
10
|
+
exports.selectSQL = selectSQL;
|
|
11
|
+
exports.countSQL = countSQL;
|
|
12
|
+
exports.searchSQL = searchSQL;
|
|
13
|
+
exports.insertSQL = insertSQL;
|
|
14
|
+
exports.updateSQL = updateSQL;
|
|
15
|
+
exports.deleteSQL = deleteSQL;
|
|
16
|
+
const utils_1 = require("../../utils");
|
|
17
|
+
// ─── CREATE TABLE ──────────────────────────────────────────────────────────
|
|
18
|
+
function createTableSQL(table) {
|
|
19
|
+
const tableName = (0, utils_1.sanitizeTableName)(table.name);
|
|
20
|
+
const columnDefs = [
|
|
21
|
+
'"id" TEXT PRIMARY KEY NOT NULL',
|
|
22
|
+
'"_status" TEXT NOT NULL DEFAULT \'created\'',
|
|
23
|
+
'"_changed" TEXT NOT NULL DEFAULT \'\'',
|
|
24
|
+
...table.columns.map((col) => {
|
|
25
|
+
const name = (0, utils_1.sanitizeColumnName)(col.name);
|
|
26
|
+
let sqlType;
|
|
27
|
+
switch (col.type) {
|
|
28
|
+
case 'text':
|
|
29
|
+
sqlType = 'TEXT';
|
|
30
|
+
break;
|
|
31
|
+
case 'number':
|
|
32
|
+
sqlType = 'REAL';
|
|
33
|
+
break;
|
|
34
|
+
case 'boolean':
|
|
35
|
+
sqlType = 'INTEGER';
|
|
36
|
+
break;
|
|
37
|
+
case 'date':
|
|
38
|
+
sqlType = 'REAL';
|
|
39
|
+
break;
|
|
40
|
+
default:
|
|
41
|
+
sqlType = 'TEXT';
|
|
42
|
+
}
|
|
43
|
+
const nullable = col.isOptional ? '' : ' NOT NULL';
|
|
44
|
+
const defaultVal = col.isOptional ? ' DEFAULT NULL' : getDefaultClause(col.type);
|
|
45
|
+
return `"${name}" ${sqlType}${nullable}${defaultVal}`;
|
|
46
|
+
}),
|
|
47
|
+
];
|
|
48
|
+
const createSQL = `CREATE TABLE IF NOT EXISTS "${tableName}" (${columnDefs.join(', ')})`;
|
|
49
|
+
// Build index statements
|
|
50
|
+
const indexes = table.columns
|
|
51
|
+
.filter((col) => col.isIndexed)
|
|
52
|
+
.map((col) => {
|
|
53
|
+
const colName = (0, utils_1.sanitizeColumnName)(col.name);
|
|
54
|
+
return `CREATE INDEX IF NOT EXISTS "${tableName}_${colName}" ON "${tableName}" ("${colName}")`;
|
|
55
|
+
});
|
|
56
|
+
// Always index _status
|
|
57
|
+
indexes.unshift(`CREATE INDEX IF NOT EXISTS "${tableName}__status" ON "${tableName}" ("_status")`);
|
|
58
|
+
return [createSQL, ...indexes].join(';\n') + ';';
|
|
59
|
+
}
|
|
60
|
+
function getDefaultClause(type) {
|
|
61
|
+
switch (type) {
|
|
62
|
+
case 'text':
|
|
63
|
+
return " DEFAULT ''";
|
|
64
|
+
case 'number':
|
|
65
|
+
return ' DEFAULT 0';
|
|
66
|
+
case 'boolean':
|
|
67
|
+
return ' DEFAULT 0';
|
|
68
|
+
case 'date':
|
|
69
|
+
return ' DEFAULT NULL';
|
|
70
|
+
default:
|
|
71
|
+
return '';
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// ─── SELECT query ──────────────────────────────────────────────────────────
|
|
75
|
+
function selectSQL(descriptor) {
|
|
76
|
+
const bindings = [];
|
|
77
|
+
const table = (0, utils_1.sanitizeTableName)(descriptor.table);
|
|
78
|
+
let sql = `SELECT * FROM "${table}"`;
|
|
79
|
+
// JOINs
|
|
80
|
+
for (const join of descriptor.joins) {
|
|
81
|
+
const joinTable = (0, utils_1.sanitizeTableName)(join.table);
|
|
82
|
+
const leftCol = (0, utils_1.sanitizeColumnName)(join.leftColumn);
|
|
83
|
+
const rightCol = (0, utils_1.sanitizeColumnName)(join.rightColumn);
|
|
84
|
+
sql += ` JOIN "${joinTable}" ON "${table}"."${leftCol}" = "${joinTable}"."${rightCol}"`;
|
|
85
|
+
}
|
|
86
|
+
// WHERE
|
|
87
|
+
if (descriptor.conditions.length > 0) {
|
|
88
|
+
const whereClause = conditionsToSQL(descriptor.conditions, bindings);
|
|
89
|
+
sql += ` WHERE ${whereClause}`;
|
|
90
|
+
}
|
|
91
|
+
// ORDER BY
|
|
92
|
+
if (descriptor.orderBy.length > 0) {
|
|
93
|
+
const orderClauses = descriptor.orderBy.map((ob) => {
|
|
94
|
+
const col = (0, utils_1.sanitizeColumnName)(ob.column);
|
|
95
|
+
return `"${col}" ${ob.order === 'desc' ? 'DESC' : 'ASC'}`;
|
|
96
|
+
});
|
|
97
|
+
sql += ` ORDER BY ${orderClauses.join(', ')}`;
|
|
98
|
+
}
|
|
99
|
+
// LIMIT / OFFSET
|
|
100
|
+
if (descriptor.limit !== undefined) {
|
|
101
|
+
sql += ' LIMIT ?';
|
|
102
|
+
bindings.push(descriptor.limit);
|
|
103
|
+
}
|
|
104
|
+
if (descriptor.offset !== undefined) {
|
|
105
|
+
sql += ' OFFSET ?';
|
|
106
|
+
bindings.push(descriptor.offset);
|
|
107
|
+
}
|
|
108
|
+
return { sql, bindings };
|
|
109
|
+
}
|
|
110
|
+
// ─── COUNT query ───────────────────────────────────────────────────────────
|
|
111
|
+
function countSQL(descriptor) {
|
|
112
|
+
const bindings = [];
|
|
113
|
+
const table = (0, utils_1.sanitizeTableName)(descriptor.table);
|
|
114
|
+
let sql = `SELECT COUNT(*) as count FROM "${table}"`;
|
|
115
|
+
if (descriptor.conditions.length > 0) {
|
|
116
|
+
const whereClause = conditionsToSQL(descriptor.conditions, bindings);
|
|
117
|
+
sql += ` WHERE ${whereClause}`;
|
|
118
|
+
}
|
|
119
|
+
return { sql, bindings };
|
|
120
|
+
}
|
|
121
|
+
// ─── SEARCH query (LIKE-based) ─────────────────────────────────────────────
|
|
122
|
+
function searchSQL(descriptor) {
|
|
123
|
+
const bindings = [];
|
|
124
|
+
const countBindings = [];
|
|
125
|
+
const table = (0, utils_1.sanitizeTableName)(descriptor.table);
|
|
126
|
+
const pattern = `%${escapeLike(descriptor.term)}%`;
|
|
127
|
+
// Build search conditions
|
|
128
|
+
const searchConditions = descriptor.fields.map((field) => {
|
|
129
|
+
const col = (0, utils_1.sanitizeColumnName)(field);
|
|
130
|
+
bindings.push(pattern);
|
|
131
|
+
countBindings.push(pattern);
|
|
132
|
+
return `"${col}" LIKE ?`;
|
|
133
|
+
});
|
|
134
|
+
const searchWhere = `(${searchConditions.join(' OR ')})`;
|
|
135
|
+
// Additional conditions
|
|
136
|
+
let extraWhere = '';
|
|
137
|
+
if (descriptor.conditions.length > 0) {
|
|
138
|
+
const extraBindings = [];
|
|
139
|
+
extraWhere = ` AND ${conditionsToSQL(descriptor.conditions, extraBindings)}`;
|
|
140
|
+
bindings.push(...extraBindings);
|
|
141
|
+
countBindings.push(...extraBindings);
|
|
142
|
+
}
|
|
143
|
+
// Count query
|
|
144
|
+
const countSql = `SELECT COUNT(*) as count FROM "${table}" WHERE ${searchWhere}${extraWhere}`;
|
|
145
|
+
// Result query
|
|
146
|
+
let sql = `SELECT * FROM "${table}" WHERE ${searchWhere}${extraWhere}`;
|
|
147
|
+
if (descriptor.orderBy.length > 0) {
|
|
148
|
+
const orderClauses = descriptor.orderBy.map((ob) => {
|
|
149
|
+
const col = (0, utils_1.sanitizeColumnName)(ob.column);
|
|
150
|
+
return `"${col}" ${ob.order === 'desc' ? 'DESC' : 'ASC'}`;
|
|
151
|
+
});
|
|
152
|
+
sql += ` ORDER BY ${orderClauses.join(', ')}`;
|
|
153
|
+
}
|
|
154
|
+
sql += ' LIMIT ? OFFSET ?';
|
|
155
|
+
bindings.push(descriptor.limit, descriptor.offset);
|
|
156
|
+
return { sql, countSql, bindings, countBindings };
|
|
157
|
+
}
|
|
158
|
+
// ─── INSERT ────────────────────────────────────────────────────────────────
|
|
159
|
+
function insertSQL(table, raw) {
|
|
160
|
+
const tableName = (0, utils_1.sanitizeTableName)(table);
|
|
161
|
+
const keys = Object.keys(raw);
|
|
162
|
+
const columns = keys.map((k) => `"${(0, utils_1.sanitizeColumnName)(k)}"`).join(', ');
|
|
163
|
+
const placeholders = keys.map(() => '?').join(', ');
|
|
164
|
+
const bindings = keys.map((k) => raw[k]);
|
|
165
|
+
return {
|
|
166
|
+
sql: `INSERT INTO "${tableName}" (${columns}) VALUES (${placeholders})`,
|
|
167
|
+
bindings,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
// ─── UPDATE ────────────────────────────────────────────────────────────────
|
|
171
|
+
function updateSQL(table, raw) {
|
|
172
|
+
const tableName = (0, utils_1.sanitizeTableName)(table);
|
|
173
|
+
const keys = Object.keys(raw).filter((k) => k !== 'id');
|
|
174
|
+
const setClauses = keys.map((k) => `"${(0, utils_1.sanitizeColumnName)(k)}" = ?`).join(', ');
|
|
175
|
+
const bindings = [...keys.map((k) => raw[k]), raw.id];
|
|
176
|
+
return {
|
|
177
|
+
sql: `UPDATE "${tableName}" SET ${setClauses} WHERE "id" = ?`,
|
|
178
|
+
bindings,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
// ─── DELETE ────────────────────────────────────────────────────────────────
|
|
182
|
+
function deleteSQL(table, id) {
|
|
183
|
+
return {
|
|
184
|
+
sql: `DELETE FROM "${(0, utils_1.sanitizeTableName)(table)}" WHERE "id" = ?`,
|
|
185
|
+
bindings: [id],
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
// ─── Condition helpers ─────────────────────────────────────────────────────
|
|
189
|
+
function conditionsToSQL(conditions, bindings) {
|
|
190
|
+
return conditions.map((c) => conditionToSQL(c, bindings)).join(' AND ');
|
|
191
|
+
}
|
|
192
|
+
function conditionToSQL(condition, bindings) {
|
|
193
|
+
switch (condition.type) {
|
|
194
|
+
case 'where':
|
|
195
|
+
return whereToSQL(condition, bindings);
|
|
196
|
+
case 'and':
|
|
197
|
+
return `(${condition.conditions.map((c) => conditionToSQL(c, bindings)).join(' AND ')})`;
|
|
198
|
+
case 'or':
|
|
199
|
+
return `(${condition.conditions.map((c) => conditionToSQL(c, bindings)).join(' OR ')})`;
|
|
200
|
+
case 'not':
|
|
201
|
+
return `NOT (${conditionToSQL(condition.condition, bindings)})`;
|
|
202
|
+
default:
|
|
203
|
+
throw new Error(`Unknown condition type: ${condition.type}`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
function whereToSQL(clause, bindings) {
|
|
207
|
+
const col = `"${(0, utils_1.sanitizeColumnName)(clause.column)}"`;
|
|
208
|
+
switch (clause.operator) {
|
|
209
|
+
case 'eq':
|
|
210
|
+
bindings.push(clause.value);
|
|
211
|
+
return `${col} = ?`;
|
|
212
|
+
case 'neq':
|
|
213
|
+
bindings.push(clause.value);
|
|
214
|
+
return `${col} != ?`;
|
|
215
|
+
case 'gt':
|
|
216
|
+
bindings.push(clause.value);
|
|
217
|
+
return `${col} > ?`;
|
|
218
|
+
case 'gte':
|
|
219
|
+
bindings.push(clause.value);
|
|
220
|
+
return `${col} >= ?`;
|
|
221
|
+
case 'lt':
|
|
222
|
+
bindings.push(clause.value);
|
|
223
|
+
return `${col} < ?`;
|
|
224
|
+
case 'lte':
|
|
225
|
+
bindings.push(clause.value);
|
|
226
|
+
return `${col} <= ?`;
|
|
227
|
+
case 'in':
|
|
228
|
+
const arr = clause.value;
|
|
229
|
+
const placeholders = arr.map(() => '?').join(', ');
|
|
230
|
+
bindings.push(...arr);
|
|
231
|
+
return `${col} IN (${placeholders})`;
|
|
232
|
+
case 'notIn':
|
|
233
|
+
const arr2 = clause.value;
|
|
234
|
+
const placeholders2 = arr2.map(() => '?').join(', ');
|
|
235
|
+
bindings.push(...arr2);
|
|
236
|
+
return `${col} NOT IN (${placeholders2})`;
|
|
237
|
+
case 'like':
|
|
238
|
+
bindings.push(clause.value);
|
|
239
|
+
return `${col} LIKE ?`;
|
|
240
|
+
case 'notLike':
|
|
241
|
+
bindings.push(clause.value);
|
|
242
|
+
return `${col} NOT LIKE ?`;
|
|
243
|
+
case 'between':
|
|
244
|
+
const [low, high] = clause.value;
|
|
245
|
+
bindings.push(low, high);
|
|
246
|
+
return `${col} BETWEEN ? AND ?`;
|
|
247
|
+
case 'isNull':
|
|
248
|
+
return `${col} IS NULL`;
|
|
249
|
+
case 'isNotNull':
|
|
250
|
+
return `${col} IS NOT NULL`;
|
|
251
|
+
default:
|
|
252
|
+
throw new Error(`Unknown operator: ${clause.operator}`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
function escapeLike(str) {
|
|
256
|
+
return str.replaceAll('%', String.raw `\%`).replaceAll('_', String.raw `\_`);
|
|
257
|
+
}
|
|
258
|
+
//# sourceMappingURL=sql.js.map
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Storage adapter interface.
|
|
3
|
+
*
|
|
4
|
+
* All database operations go through this interface, enabling
|
|
5
|
+
* pluggable backends (SQLite, LokiJS, etc.).
|
|
6
|
+
*/
|
|
7
|
+
import type { QueryDescriptor, SearchDescriptor, BatchOperation } from '../query/types';
|
|
8
|
+
import type { DatabaseSchema, RawRecord, TableSchema } from '../schema/types';
|
|
9
|
+
export interface AdapterConfig {
|
|
10
|
+
readonly databaseName: string;
|
|
11
|
+
/** Optional schema version override; normally derived from DatabaseSchema. */
|
|
12
|
+
readonly schemaVersion?: number;
|
|
13
|
+
}
|
|
14
|
+
export interface EncryptionConfig {
|
|
15
|
+
readonly enabled: boolean;
|
|
16
|
+
readonly keyProvider: () => Promise<Uint8Array>;
|
|
17
|
+
}
|
|
18
|
+
export interface Migration {
|
|
19
|
+
readonly fromVersion: number;
|
|
20
|
+
readonly toVersion: number;
|
|
21
|
+
readonly steps: MigrationStep[];
|
|
22
|
+
}
|
|
23
|
+
export type MigrationStep = {
|
|
24
|
+
type: 'createTable';
|
|
25
|
+
schema: TableSchema;
|
|
26
|
+
} | {
|
|
27
|
+
type: 'addColumn';
|
|
28
|
+
table: string;
|
|
29
|
+
column: string;
|
|
30
|
+
columnType: string;
|
|
31
|
+
isOptional?: boolean;
|
|
32
|
+
} | {
|
|
33
|
+
type: 'destroyTable';
|
|
34
|
+
table: string;
|
|
35
|
+
} | {
|
|
36
|
+
type: 'sql';
|
|
37
|
+
query: string;
|
|
38
|
+
};
|
|
39
|
+
export interface StorageAdapter {
|
|
40
|
+
/** Initialize the adapter. Creates tables if needed. */
|
|
41
|
+
initialize(schema: DatabaseSchema): Promise<void>;
|
|
42
|
+
/** Find records matching a query descriptor. */
|
|
43
|
+
find(query: QueryDescriptor): Promise<RawRecord[]>;
|
|
44
|
+
/** Count records matching a query descriptor. */
|
|
45
|
+
count(query: QueryDescriptor): Promise<number>;
|
|
46
|
+
/** Find a single record by ID. */
|
|
47
|
+
findById(table: string, id: string): Promise<RawRecord | null>;
|
|
48
|
+
/** Insert a new raw record. */
|
|
49
|
+
insert(table: string, raw: RawRecord): Promise<void>;
|
|
50
|
+
/** Update an existing raw record. */
|
|
51
|
+
update(table: string, raw: RawRecord): Promise<void>;
|
|
52
|
+
/** Mark a record as deleted (_status = 'deleted'). */
|
|
53
|
+
markAsDeleted(table: string, id: string): Promise<void>;
|
|
54
|
+
/** Permanently remove a record from the database. */
|
|
55
|
+
destroyPermanently(table: string, id: string): Promise<void>;
|
|
56
|
+
/** Execute a batch of operations atomically. */
|
|
57
|
+
batch(operations: BatchOperation[]): Promise<void>;
|
|
58
|
+
/** Full-text search. */
|
|
59
|
+
search(descriptor: SearchDescriptor): Promise<{
|
|
60
|
+
records: RawRecord[];
|
|
61
|
+
total: number;
|
|
62
|
+
}>;
|
|
63
|
+
/** Return all records with _status != 'synced' */
|
|
64
|
+
getLocalChanges(tables: string[]): Promise<Record<string, {
|
|
65
|
+
created: RawRecord[];
|
|
66
|
+
updated: RawRecord[];
|
|
67
|
+
deleted: string[];
|
|
68
|
+
}>>;
|
|
69
|
+
/** Apply synced changes from remote (in a transaction). */
|
|
70
|
+
applyRemoteChanges(changes: Record<string, {
|
|
71
|
+
created: RawRecord[];
|
|
72
|
+
updated: RawRecord[];
|
|
73
|
+
deleted: string[];
|
|
74
|
+
}>): Promise<void>;
|
|
75
|
+
/** Mark synced records as _status = 'synced'. */
|
|
76
|
+
markAsSynced(table: string, ids: string[]): Promise<void>;
|
|
77
|
+
/** Get the database schema version currently stored. */
|
|
78
|
+
getSchemaVersion(): Promise<number>;
|
|
79
|
+
/** Run migrations. */
|
|
80
|
+
migrate(migrations: Migration[]): Promise<void>;
|
|
81
|
+
/** Completely reset the database. */
|
|
82
|
+
reset(): Promise<void>;
|
|
83
|
+
/** Close the database connection. */
|
|
84
|
+
close(): Promise<void>;
|
|
85
|
+
}
|
|
86
|
+
export type AdapterEvent = {
|
|
87
|
+
type: 'initialized';
|
|
88
|
+
} | {
|
|
89
|
+
type: 'batch_completed';
|
|
90
|
+
operations: BatchOperation[];
|
|
91
|
+
} | {
|
|
92
|
+
type: 'reset';
|
|
93
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Storage adapter interface.
|
|
4
|
+
*
|
|
5
|
+
* All database operations go through this interface, enabling
|
|
6
|
+
* pluggable backends (SQLite, LokiJS, etc.).
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
//# sourceMappingURL=types.js.map
|