@offlinejs/storage-sqlite 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.
@@ -0,0 +1,43 @@
1
+ import { IndexableStorageAdapter, EntityRecord, QueryOptions, TransactionStore, StorageMigration, IndexDefinition } from '@offlinejs/types';
2
+
3
+ interface SQLiteDriver {
4
+ execute(sql: string, params?: unknown[]): Promise<void>;
5
+ query<TValue = unknown>(sql: string, params?: unknown[]): Promise<TValue[]>;
6
+ transaction?<TValue>(run: () => Promise<TValue>): Promise<TValue>;
7
+ }
8
+ interface SQLiteStorageOptions {
9
+ driver: SQLiteDriver;
10
+ tableName?: string;
11
+ }
12
+ declare class SQLiteStorageAdapter implements IndexableStorageAdapter {
13
+ readonly name = "sqlite";
14
+ readonly contractVersion = 1;
15
+ readonly capabilities: {
16
+ readonly indexes: true;
17
+ readonly migrations: true;
18
+ readonly persistence: "durable";
19
+ readonly transactions: "best-effort";
20
+ };
21
+ private readonly driver;
22
+ private readonly tableName;
23
+ private initialized;
24
+ constructor(options: SQLiteStorageOptions);
25
+ initialize(): Promise<void>;
26
+ get<TRecord extends EntityRecord>(collection: string, id: string): Promise<TRecord | null>;
27
+ set<TRecord extends EntityRecord>(collection: string, value: TRecord): Promise<void>;
28
+ delete(collection: string, id: string): Promise<void>;
29
+ find<TRecord extends EntityRecord>(collection: string, query?: QueryOptions<TRecord>): Promise<TRecord[]>;
30
+ clear(collection?: string): Promise<void>;
31
+ transaction<TValue>(_scope: string[], run: (store: TransactionStore) => Promise<TValue>): Promise<TValue>;
32
+ migrate(migrations: StorageMigration[]): Promise<void>;
33
+ createIndex<TRecord extends EntityRecord>(definition: IndexDefinition<TRecord>): Promise<void>;
34
+ dropIndex(collection: string, name: string): Promise<void>;
35
+ listIndexes(collection?: string): Promise<IndexDefinition[]>;
36
+ private findViaIndex;
37
+ private assertUniqueIndexes;
38
+ private writeIndexEntries;
39
+ private removeIndexEntries;
40
+ }
41
+ declare const createSQLiteStorage: (options: SQLiteStorageOptions) => SQLiteStorageAdapter;
42
+
43
+ export { type SQLiteDriver, SQLiteStorageAdapter, type SQLiteStorageOptions, createSQLiteStorage };
package/dist/index.js ADDED
@@ -0,0 +1,212 @@
1
+ import { STORAGE_ADAPTER_CONTRACT_VERSION } from '@offlinejs/types';
2
+ import { applyQuery, findMatchingIndex, getEqualityFilterLookups, serializeCompoundIndexValue, readIndexFields } from '@offlinejs/utils';
3
+
4
+ // src/index.ts
5
+ var SQLiteStorageAdapter = class {
6
+ name = "sqlite";
7
+ contractVersion = STORAGE_ADAPTER_CONTRACT_VERSION;
8
+ capabilities = {
9
+ indexes: true,
10
+ migrations: true,
11
+ persistence: "durable",
12
+ transactions: "best-effort"
13
+ };
14
+ driver;
15
+ tableName;
16
+ initialized = false;
17
+ constructor(options) {
18
+ this.driver = options.driver;
19
+ this.tableName = options.tableName ?? "offlinejs_records";
20
+ }
21
+ async initialize() {
22
+ if (this.initialized) {
23
+ return;
24
+ }
25
+ await this.driver.execute(
26
+ `CREATE TABLE IF NOT EXISTS ${this.tableName} (collection TEXT NOT NULL, id TEXT NOT NULL, value TEXT NOT NULL, PRIMARY KEY (collection, id))`
27
+ );
28
+ await this.driver.execute(
29
+ `CREATE TABLE IF NOT EXISTS ${this.tableName}_indexes (collection TEXT NOT NULL, name TEXT NOT NULL, value TEXT NOT NULL, PRIMARY KEY (collection, name))`
30
+ );
31
+ await this.driver.execute(
32
+ `CREATE TABLE IF NOT EXISTS ${this.tableName}_index_entries (collection TEXT NOT NULL, index_name TEXT NOT NULL, value_key TEXT NOT NULL, record_id TEXT NOT NULL, PRIMARY KEY (collection, index_name, value_key, record_id))`
33
+ );
34
+ await this.driver.execute(
35
+ `CREATE INDEX IF NOT EXISTS ${this.tableName}_index_entries_lookup ON ${this.tableName}_index_entries (collection, index_name, value_key)`
36
+ );
37
+ this.initialized = true;
38
+ }
39
+ async get(collection, id) {
40
+ await this.initialize();
41
+ const rows = await this.driver.query(
42
+ `SELECT value FROM ${this.tableName} WHERE collection = ? AND id = ? LIMIT 1`,
43
+ [collection, id]
44
+ );
45
+ return rows[0] ? JSON.parse(rows[0].value) : null;
46
+ }
47
+ async set(collection, value) {
48
+ await this.initialize();
49
+ const previous = await this.get(collection, value.id);
50
+ if (previous) {
51
+ await this.removeIndexEntries(collection, previous);
52
+ }
53
+ await this.assertUniqueIndexes(collection, value, previous?.id);
54
+ await this.driver.execute(
55
+ `INSERT OR REPLACE INTO ${this.tableName} (collection, id, value) VALUES (?, ?, ?)`,
56
+ [collection, value.id, JSON.stringify(value)]
57
+ );
58
+ await this.writeIndexEntries(collection, value);
59
+ }
60
+ async delete(collection, id) {
61
+ await this.initialize();
62
+ const previous = await this.get(collection, id);
63
+ if (previous) {
64
+ await this.removeIndexEntries(collection, previous);
65
+ }
66
+ await this.driver.execute(`DELETE FROM ${this.tableName} WHERE collection = ? AND id = ?`, [
67
+ collection,
68
+ id
69
+ ]);
70
+ }
71
+ async find(collection, query) {
72
+ await this.initialize();
73
+ const indexed = await this.findViaIndex(collection, query);
74
+ const records = indexed ?? (await this.driver.query(
75
+ `SELECT value FROM ${this.tableName} WHERE collection = ?`,
76
+ [collection]
77
+ )).map((row) => JSON.parse(row.value));
78
+ return applyQuery(records, query);
79
+ }
80
+ async clear(collection) {
81
+ await this.initialize();
82
+ if (collection) {
83
+ await this.driver.execute(`DELETE FROM ${this.tableName} WHERE collection = ?`, [collection]);
84
+ await this.driver.execute(`DELETE FROM ${this.tableName}_indexes WHERE collection = ?`, [
85
+ collection
86
+ ]);
87
+ await this.driver.execute(
88
+ `DELETE FROM ${this.tableName}_index_entries WHERE collection = ?`,
89
+ [collection]
90
+ );
91
+ return;
92
+ }
93
+ await this.driver.execute(`DELETE FROM ${this.tableName}`);
94
+ await this.driver.execute(`DELETE FROM ${this.tableName}_indexes`);
95
+ await this.driver.execute(`DELETE FROM ${this.tableName}_index_entries`);
96
+ }
97
+ async transaction(_scope, run) {
98
+ if (this.driver.transaction) {
99
+ return this.driver.transaction(() => run(this));
100
+ }
101
+ return run(this);
102
+ }
103
+ async migrate(migrations) {
104
+ await this.initialize();
105
+ const applied = new Set(
106
+ (await this.find("__migrations")).map((record) => record.id)
107
+ );
108
+ for (const migration of migrations) {
109
+ if (applied.has(migration.name)) {
110
+ continue;
111
+ }
112
+ await migration.up(this);
113
+ await this.set("__migrations", { id: migration.name, appliedAt: Date.now() });
114
+ }
115
+ }
116
+ async createIndex(definition) {
117
+ await this.initialize();
118
+ await this.driver.execute(
119
+ `INSERT OR REPLACE INTO ${this.tableName}_indexes (collection, name, value) VALUES (?, ?, ?)`,
120
+ [definition.collection, definition.name, JSON.stringify(definition)]
121
+ );
122
+ const rows = await this.driver.query(
123
+ `SELECT value FROM ${this.tableName} WHERE collection = ?`,
124
+ [definition.collection]
125
+ );
126
+ for (const row of rows) {
127
+ const record = JSON.parse(row.value);
128
+ await this.assertUniqueIndexes(definition.collection, record);
129
+ await this.writeIndexEntries(definition.collection, record, [definition]);
130
+ }
131
+ }
132
+ async dropIndex(collection, name) {
133
+ await this.initialize();
134
+ await this.driver.execute(
135
+ `DELETE FROM ${this.tableName}_indexes WHERE collection = ? AND name = ?`,
136
+ [collection, name]
137
+ );
138
+ await this.driver.execute(
139
+ `DELETE FROM ${this.tableName}_index_entries WHERE collection = ? AND index_name = ?`,
140
+ [collection, name]
141
+ );
142
+ }
143
+ async listIndexes(collection) {
144
+ await this.initialize();
145
+ const rows = collection ? await this.driver.query(
146
+ `SELECT value FROM ${this.tableName}_indexes WHERE collection = ?`,
147
+ [collection]
148
+ ) : await this.driver.query(`SELECT value FROM ${this.tableName}_indexes`);
149
+ return rows.map((row) => JSON.parse(row.value));
150
+ }
151
+ async findViaIndex(collection, query) {
152
+ const match = findMatchingIndex(
153
+ await this.listIndexes(collection),
154
+ getEqualityFilterLookups(query?.filters)
155
+ );
156
+ if (!match) {
157
+ return null;
158
+ }
159
+ const valueKey = serializeCompoundIndexValue(match.values);
160
+ const rows = await this.driver.query(
161
+ `SELECT record_id FROM ${this.tableName}_index_entries WHERE collection = ? AND index_name = ? AND value_key = ?`,
162
+ [collection, match.index.name, valueKey]
163
+ );
164
+ const records = [];
165
+ for (const row of rows) {
166
+ const record = await this.get(collection, row.record_id);
167
+ if (record) {
168
+ records.push(record);
169
+ }
170
+ }
171
+ return records;
172
+ }
173
+ async assertUniqueIndexes(collection, record, ignoreId) {
174
+ for (const definition of await this.listIndexes(collection)) {
175
+ if (!definition.unique) {
176
+ continue;
177
+ }
178
+ const valueKey = serializeCompoundIndexValue(readIndexFields(record, definition.fields));
179
+ const rows = await this.driver.query(
180
+ `SELECT record_id FROM ${this.tableName}_index_entries WHERE collection = ? AND index_name = ? AND value_key = ?`,
181
+ [collection, definition.name, valueKey]
182
+ );
183
+ if (rows.some((row) => row.record_id !== record.id && row.record_id !== ignoreId)) {
184
+ throw new Error(`Unique index "${definition.name}" violated for ${collection}`);
185
+ }
186
+ }
187
+ }
188
+ async writeIndexEntries(collection, record, definitions) {
189
+ const indexes = definitions ?? await this.listIndexes(collection);
190
+ for (const definition of indexes) {
191
+ const valueKey = serializeCompoundIndexValue(readIndexFields(record, definition.fields));
192
+ await this.driver.execute(
193
+ `INSERT OR REPLACE INTO ${this.tableName}_index_entries (collection, index_name, value_key, record_id) VALUES (?, ?, ?, ?)`,
194
+ [collection, definition.name, valueKey, record.id]
195
+ );
196
+ }
197
+ }
198
+ async removeIndexEntries(collection, record) {
199
+ for (const definition of await this.listIndexes(collection)) {
200
+ const valueKey = serializeCompoundIndexValue(readIndexFields(record, definition.fields));
201
+ await this.driver.execute(
202
+ `DELETE FROM ${this.tableName}_index_entries WHERE collection = ? AND index_name = ? AND value_key = ? AND record_id = ?`,
203
+ [collection, definition.name, valueKey, record.id]
204
+ );
205
+ }
206
+ }
207
+ };
208
+ var createSQLiteStorage = (options) => new SQLiteStorageAdapter(options);
209
+
210
+ export { SQLiteStorageAdapter, createSQLiteStorage };
211
+ //# sourceMappingURL=index.js.map
212
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import {\n STORAGE_ADAPTER_CONTRACT_VERSION,\n type EntityRecord,\n type IndexDefinition,\n type IndexableStorageAdapter,\n type QueryOptions,\n type StorageMigration,\n type TransactionStore\n} from \"@offlinejs/types\";\nimport {\n applyQuery,\n findMatchingIndex,\n getEqualityFilterLookups,\n readIndexFields,\n serializeCompoundIndexValue\n} from \"@offlinejs/utils\";\n\nexport interface SQLiteDriver {\n execute(sql: string, params?: unknown[]): Promise<void>;\n query<TValue = unknown>(sql: string, params?: unknown[]): Promise<TValue[]>;\n transaction?<TValue>(run: () => Promise<TValue>): Promise<TValue>;\n}\n\nexport interface SQLiteStorageOptions {\n driver: SQLiteDriver;\n tableName?: string;\n}\n\ninterface SQLiteRecordRow {\n collection: string;\n id: string;\n value: string;\n}\n\nexport class SQLiteStorageAdapter implements IndexableStorageAdapter {\n readonly name = \"sqlite\";\n readonly contractVersion = STORAGE_ADAPTER_CONTRACT_VERSION;\n readonly capabilities = {\n indexes: true,\n migrations: true,\n persistence: \"durable\",\n transactions: \"best-effort\"\n } as const;\n\n private readonly driver: SQLiteDriver;\n private readonly tableName: string;\n private initialized = false;\n\n constructor(options: SQLiteStorageOptions) {\n this.driver = options.driver;\n this.tableName = options.tableName ?? \"offlinejs_records\";\n }\n\n async initialize(): Promise<void> {\n if (this.initialized) {\n return;\n }\n\n await this.driver.execute(\n `CREATE TABLE IF NOT EXISTS ${this.tableName} (collection TEXT NOT NULL, id TEXT NOT NULL, value TEXT NOT NULL, PRIMARY KEY (collection, id))`\n );\n await this.driver.execute(\n `CREATE TABLE IF NOT EXISTS ${this.tableName}_indexes (collection TEXT NOT NULL, name TEXT NOT NULL, value TEXT NOT NULL, PRIMARY KEY (collection, name))`\n );\n await this.driver.execute(\n `CREATE TABLE IF NOT EXISTS ${this.tableName}_index_entries (collection TEXT NOT NULL, index_name TEXT NOT NULL, value_key TEXT NOT NULL, record_id TEXT NOT NULL, PRIMARY KEY (collection, index_name, value_key, record_id))`\n );\n await this.driver.execute(\n `CREATE INDEX IF NOT EXISTS ${this.tableName}_index_entries_lookup ON ${this.tableName}_index_entries (collection, index_name, value_key)`\n );\n this.initialized = true;\n }\n\n async get<TRecord extends EntityRecord>(collection: string, id: string): Promise<TRecord | null> {\n await this.initialize();\n const rows = await this.driver.query<SQLiteRecordRow>(\n `SELECT value FROM ${this.tableName} WHERE collection = ? AND id = ? LIMIT 1`,\n [collection, id]\n );\n\n return rows[0] ? (JSON.parse(rows[0].value) as TRecord) : null;\n }\n\n async set<TRecord extends EntityRecord>(collection: string, value: TRecord): Promise<void> {\n await this.initialize();\n const previous = await this.get(collection, value.id);\n if (previous) {\n await this.removeIndexEntries(collection, previous);\n }\n\n await this.assertUniqueIndexes(collection, value, previous?.id);\n await this.driver.execute(\n `INSERT OR REPLACE INTO ${this.tableName} (collection, id, value) VALUES (?, ?, ?)`,\n [collection, value.id, JSON.stringify(value)]\n );\n await this.writeIndexEntries(collection, value);\n }\n\n async delete(collection: string, id: string): Promise<void> {\n await this.initialize();\n const previous = await this.get(collection, id);\n if (previous) {\n await this.removeIndexEntries(collection, previous);\n }\n await this.driver.execute(`DELETE FROM ${this.tableName} WHERE collection = ? AND id = ?`, [\n collection,\n id\n ]);\n }\n\n async find<TRecord extends EntityRecord>(\n collection: string,\n query?: QueryOptions<TRecord>\n ): Promise<TRecord[]> {\n await this.initialize();\n const indexed = await this.findViaIndex<TRecord>(collection, query);\n const records =\n indexed ??\n (\n await this.driver.query<SQLiteRecordRow>(\n `SELECT value FROM ${this.tableName} WHERE collection = ?`,\n [collection]\n )\n ).map((row) => JSON.parse(row.value) as TRecord);\n\n return applyQuery(records, query);\n }\n\n async clear(collection?: string): Promise<void> {\n await this.initialize();\n\n if (collection) {\n await this.driver.execute(`DELETE FROM ${this.tableName} WHERE collection = ?`, [collection]);\n await this.driver.execute(`DELETE FROM ${this.tableName}_indexes WHERE collection = ?`, [\n collection\n ]);\n await this.driver.execute(\n `DELETE FROM ${this.tableName}_index_entries WHERE collection = ?`,\n [collection]\n );\n return;\n }\n\n await this.driver.execute(`DELETE FROM ${this.tableName}`);\n await this.driver.execute(`DELETE FROM ${this.tableName}_indexes`);\n await this.driver.execute(`DELETE FROM ${this.tableName}_index_entries`);\n }\n\n async transaction<TValue>(\n _scope: string[],\n run: (store: TransactionStore) => Promise<TValue>\n ): Promise<TValue> {\n if (this.driver.transaction) {\n return this.driver.transaction(() => run(this));\n }\n\n return run(this);\n }\n\n async migrate(migrations: StorageMigration[]): Promise<void> {\n await this.initialize();\n const applied = new Set(\n (await this.find<EntityRecord>(\"__migrations\")).map((record) => record.id)\n );\n\n for (const migration of migrations) {\n if (applied.has(migration.name)) {\n continue;\n }\n\n await migration.up(this);\n await this.set(\"__migrations\", { id: migration.name, appliedAt: Date.now() });\n }\n }\n\n async createIndex<TRecord extends EntityRecord>(\n definition: IndexDefinition<TRecord>\n ): Promise<void> {\n await this.initialize();\n await this.driver.execute(\n `INSERT OR REPLACE INTO ${this.tableName}_indexes (collection, name, value) VALUES (?, ?, ?)`,\n [definition.collection, definition.name, JSON.stringify(definition)]\n );\n\n const rows = await this.driver.query<SQLiteRecordRow>(\n `SELECT value FROM ${this.tableName} WHERE collection = ?`,\n [definition.collection]\n );\n\n for (const row of rows) {\n const record = JSON.parse(row.value) as EntityRecord;\n await this.assertUniqueIndexes(definition.collection, record);\n await this.writeIndexEntries(definition.collection, record, [definition as IndexDefinition]);\n }\n }\n\n async dropIndex(collection: string, name: string): Promise<void> {\n await this.initialize();\n await this.driver.execute(\n `DELETE FROM ${this.tableName}_indexes WHERE collection = ? AND name = ?`,\n [collection, name]\n );\n await this.driver.execute(\n `DELETE FROM ${this.tableName}_index_entries WHERE collection = ? AND index_name = ?`,\n [collection, name]\n );\n }\n\n async listIndexes(collection?: string): Promise<IndexDefinition[]> {\n await this.initialize();\n const rows = collection\n ? await this.driver.query<{ value: string }>(\n `SELECT value FROM ${this.tableName}_indexes WHERE collection = ?`,\n [collection]\n )\n : await this.driver.query<{ value: string }>(`SELECT value FROM ${this.tableName}_indexes`);\n\n return rows.map((row) => JSON.parse(row.value) as IndexDefinition);\n }\n\n private async findViaIndex<TRecord extends EntityRecord>(\n collection: string,\n query?: QueryOptions<TRecord>\n ): Promise<TRecord[] | null> {\n const match = findMatchingIndex(\n await this.listIndexes(collection),\n getEqualityFilterLookups(query?.filters)\n );\n\n if (!match) {\n return null;\n }\n\n const valueKey = serializeCompoundIndexValue(match.values);\n const rows = await this.driver.query<{ record_id: string }>(\n `SELECT record_id FROM ${this.tableName}_index_entries WHERE collection = ? AND index_name = ? AND value_key = ?`,\n [collection, match.index.name, valueKey]\n );\n const records: TRecord[] = [];\n\n for (const row of rows) {\n const record = await this.get<TRecord>(collection, row.record_id);\n if (record) {\n records.push(record);\n }\n }\n\n return records;\n }\n\n private async assertUniqueIndexes(\n collection: string,\n record: EntityRecord,\n ignoreId?: string\n ): Promise<void> {\n for (const definition of await this.listIndexes(collection)) {\n if (!definition.unique) {\n continue;\n }\n\n const valueKey = serializeCompoundIndexValue(readIndexFields(record, definition.fields));\n const rows = await this.driver.query<{ record_id: string }>(\n `SELECT record_id FROM ${this.tableName}_index_entries WHERE collection = ? AND index_name = ? AND value_key = ?`,\n [collection, definition.name, valueKey]\n );\n\n if (rows.some((row) => row.record_id !== record.id && row.record_id !== ignoreId)) {\n throw new Error(`Unique index \"${definition.name}\" violated for ${collection}`);\n }\n }\n }\n\n private async writeIndexEntries(\n collection: string,\n record: EntityRecord,\n definitions?: IndexDefinition[]\n ): Promise<void> {\n const indexes = definitions ?? (await this.listIndexes(collection));\n\n for (const definition of indexes) {\n const valueKey = serializeCompoundIndexValue(readIndexFields(record, definition.fields));\n await this.driver.execute(\n `INSERT OR REPLACE INTO ${this.tableName}_index_entries (collection, index_name, value_key, record_id) VALUES (?, ?, ?, ?)`,\n [collection, definition.name, valueKey, record.id]\n );\n }\n }\n\n private async removeIndexEntries(collection: string, record: EntityRecord): Promise<void> {\n for (const definition of await this.listIndexes(collection)) {\n const valueKey = serializeCompoundIndexValue(readIndexFields(record, definition.fields));\n await this.driver.execute(\n `DELETE FROM ${this.tableName}_index_entries WHERE collection = ? AND index_name = ? AND value_key = ? AND record_id = ?`,\n [collection, definition.name, valueKey, record.id]\n );\n }\n }\n}\n\nexport const createSQLiteStorage = (options: SQLiteStorageOptions): SQLiteStorageAdapter =>\n new SQLiteStorageAdapter(options);\n"]}
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@offlinejs/storage-sqlite",
3
+ "version": "0.1.0",
4
+ "description": "SQLite storage adapter for OfflineJS using a pluggable async driver.",
5
+ "type": "module",
6
+ "sideEffects": false,
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "import": "./dist/index.js"
11
+ }
12
+ },
13
+ "files": [
14
+ "dist"
15
+ ],
16
+ "dependencies": {
17
+ "@offlinejs/types": "0.1.0",
18
+ "@offlinejs/utils": "0.1.0"
19
+ },
20
+ "devDependencies": {
21
+ "tsup": "latest",
22
+ "typescript": "latest"
23
+ },
24
+ "publishConfig": {
25
+ "access": "public",
26
+ "registry": "https://registry.npmjs.org/"
27
+ },
28
+ "scripts": {
29
+ "build": "tsup",
30
+ "typecheck": "tsc --noEmit"
31
+ }
32
+ }