Comparing version
@@ -19,2 +19,2 @@ import { SuperMap } from "../factory/factory.js"; | ||
}; | ||
export declare function createArray<T, MapType extends SuperMap<T>>(map: MapType, defaultValue: T): SuperArray<T>; | ||
export declare function createArray<T, MapType extends SuperMap<T, []>>(map: MapType, defaultValue: T): SuperArray<T>; |
import { createArray } from "../array/array.js"; | ||
import * as BetterSqlite3Map from "../map/better-sqlite3.js"; | ||
import { DEFAULT_SERIALIZER } from "./factory.js"; | ||
import { DEFAULT_SERIALIZER, } from "./factory.js"; | ||
/** | ||
@@ -17,5 +17,13 @@ * Creates a StorageFactory that uses BetterSqlite3 as the underlying storage. | ||
const getMap = (name, options) => { | ||
var _a, _b; | ||
const format = (_a = options === null || options === void 0 ? void 0 : options.format) !== null && _a !== void 0 ? _a : "text"; | ||
const serializer = (_b = options === null || options === void 0 ? void 0 : options.serializer) !== null && _b !== void 0 ? _b : DEFAULT_SERIALIZER[format]; | ||
var _a; | ||
let format; | ||
let serializer; | ||
if (options === null || options === void 0 ? void 0 : options.format) { | ||
format = options.format === "text" ? "text" : "binary"; | ||
serializer = (_a = options === null || options === void 0 ? void 0 : options.serializer) !== null && _a !== void 0 ? _a : DEFAULT_SERIALIZER[format]; | ||
} | ||
else { | ||
format = "text"; | ||
serializer = DEFAULT_SERIALIZER[format]; | ||
} | ||
getOrCreateTable(name, format); | ||
@@ -22,0 +30,0 @@ return BetterSqlite3Map.create(db, name, serializer); |
@@ -6,6 +6,60 @@ import { SuperArray } from "../array/array.js"; | ||
}; | ||
export type SuperMap<T> = Map<string, T> & { | ||
export type SuperMap<T, Relations extends string[]> = Omit<Map<string, T>, "set"> & { | ||
name: string; | ||
toString: () => string; | ||
/** | ||
* @param key | ||
* @param value | ||
* @param relations We can associate one or more related ids for this row. For | ||
* example, | ||
* | ||
* ```ts | ||
* posts.set("post1", { content: "post1" }, { user_id: ["user1"] }); | ||
* ``` | ||
* @returns | ||
*/ | ||
set: (key: string, value: T, relations?: Record<Relations[number], string[]>) => SuperMap<T, Relations>; | ||
/** | ||
* @param relations We can get all rows that match any of the related ids. For | ||
* example, | ||
* | ||
* ```ts | ||
* posts.getRelated({ user_id: ["user1"] }); | ||
* ``` | ||
* @returns An array of {key, value, relations} objects. | ||
*/ | ||
getRelated: (relations?: Record<Relations[number], string[]>) => { | ||
key: string; | ||
value: T; | ||
relations: Record<Relations[number], string[]>; | ||
}[]; | ||
/** | ||
* @param relations We can delete all rows that match any of the related ids. | ||
* For example, | ||
* | ||
* ```ts | ||
* posts.deleteRelated({ user_id: ["user1"] }); | ||
* ``` | ||
*/ | ||
deleteRelated: (relations: Record<Relations[number], string[]>) => void; | ||
/** | ||
* @param value | ||
* @param relations This is convenient method for adding new rows (with uuid | ||
* as key) and associating them with the related ids. For example, | ||
* | ||
* ```ts | ||
* posts.addRelated({ content: "post3" }, { user_id: ["user1"] }); | ||
* ``` | ||
*/ | ||
addRelated: (value: T, relations: Record<Relations[number], string[]>) => void; | ||
}; | ||
export type GetMapOptions = { | ||
format?: undefined; | ||
} | { | ||
format: "text"; | ||
serializer?: Serializer<string>; | ||
} | { | ||
format: "binary"; | ||
serializer?: Serializer<Uint8Array>; | ||
}; | ||
/** A factory for creating SuperMaps and SuperArrays. */ | ||
@@ -25,9 +79,3 @@ export type StorageFactory = { | ||
*/ | ||
getMap: <T = any>(name: string, options?: { | ||
format: "text"; | ||
serializer?: Serializer<string>; | ||
} | { | ||
format: "binary"; | ||
serializer?: Serializer<Uint8Array>; | ||
}) => SuperMap<T>; | ||
getMap: <T = any, Relations extends string[] = []>(name: string, options?: GetMapOptions) => SuperMap<T, Relations>; | ||
/** | ||
@@ -34,0 +82,0 @@ * @param name Name of the storage (this represents the table name in the |
import BetterSqlite3 from "better-sqlite3"; | ||
import { Serializer, SuperMap } from "../factory/factory.js"; | ||
export declare function create<T>(database: BetterSqlite3.Database, name: string, serializer: Serializer<any>): SuperMap<T>; | ||
export declare function create<T, Relations extends string[]>(database: BetterSqlite3.Database, name: string, serializer: Serializer<any>): SuperMap<T, Relations>; |
@@ -0,2 +1,14 @@ | ||
import crypto from "crypto"; | ||
export function create(database, name, serializer) { | ||
const res = database.pragma(`table_info(${name})`); | ||
const existingRelations = new Set(res.map((x) => x.name).filter((x) => x !== "key" && x !== "value")); | ||
function addRelationsIfNotExists(relations) { | ||
for (const relation of relations) { | ||
if (existingRelations.has(relation)) { | ||
return; | ||
} | ||
database.prepare(`ALTER TABLE ${name} ADD COLUMN ${relation} TEXT`).run(); | ||
existingRelations.add(relation); | ||
} | ||
} | ||
return { | ||
@@ -13,6 +25,15 @@ name, | ||
}, | ||
set(key, value) { | ||
set(key, value, relations) { | ||
if (!relations) { | ||
database | ||
.prepare(`REPLACE INTO ${name} (key, value) VALUES (?, ?)`) | ||
.run(key, serializer.encode(value)); | ||
return this; | ||
} | ||
addRelationsIfNotExists(Object.keys(relations)); | ||
database | ||
.prepare(`REPLACE INTO ${name} (key, value) VALUES (?, ?)`) | ||
.run(key, serializer.encode(value)); | ||
.prepare(`REPLACE INTO ${name} (key, value, ${Object.keys(relations).join(", ")}) VALUES (?, ?, ${Object.values(relations) | ||
.map(() => "?") | ||
.join(", ")})`) | ||
.run(key, serializer.encode(value), ...Object.values(relations).map((x) => (Array.isArray(x) ? x : [x]).join(","))); | ||
return this; | ||
@@ -79,3 +100,44 @@ }, | ||
}, | ||
getRelated(relations) { | ||
var _a; | ||
const rows = database.prepare(`SELECT * FROM ${name}`).all(); | ||
const res = []; | ||
for (const row of rows) { | ||
const value = serializer.decode(row.value); | ||
const relationValues = {}; | ||
for (const key of existingRelations) { | ||
const columnValues = row[key]; | ||
if (!columnValues) | ||
continue; | ||
const values = new Set(columnValues.split(",")); | ||
if (relations) { | ||
const queryValues = (_a = relations[key]) !== null && _a !== void 0 ? _a : []; | ||
if (queryValues.length === 0) | ||
continue; | ||
let foundOne = false; | ||
for (const queryValue of queryValues) { | ||
if (values.has(queryValue)) { | ||
foundOne = true; | ||
break; | ||
} | ||
} | ||
if (!foundOne) { | ||
continue; | ||
} | ||
} | ||
relationValues[key] = [...values]; | ||
} | ||
if (relations && Object.values(relationValues).length === 0) | ||
continue; | ||
res.push({ key: row.key, value, relations: relationValues }); | ||
} | ||
return res; | ||
}, | ||
deleteRelated(relations) { | ||
this.getRelated(relations).forEach((x) => this.delete(x.key)); | ||
}, | ||
addRelated(value, relations) { | ||
this.set(crypto.randomUUID(), value, relations); | ||
}, | ||
}; | ||
} |
{ | ||
"name": "sqlite-map", | ||
"version": "0.1.0", | ||
"description": "Simple javascript persistent map and array datastructures based on sqlite", | ||
"version": "0.1.2", | ||
"description": "Simple javascript persistent map and array datastructures based on better-sqlite3", | ||
"license": "ISC", | ||
@@ -6,0 +6,0 @@ "author": "Amit", |
@@ -112,1 +112,40 @@ # sqlite-map | ||
``` | ||
## Related data | ||
The library also provides simple ways to work with related data (however these require full table scan to find/delete related data). We can pass a third parameter to the set function to associate related ids. The `set()` call will create the corresponding columns in sqlite table to hold these related values (internally stored as comma separated values). | ||
Later we can get all rows that have any of the related id's by using `getRelated()`. `getRelated/deleteRelated` are only convenience features and have performance issue because they perform a complete table scan to filter rows. | ||
```ts | ||
const database = new Database(":memory:"); | ||
const factory = createBetterSqlite3Factory({ | ||
database, | ||
}); | ||
type User = { | ||
id: string; | ||
}; | ||
type Post = { | ||
content: string; | ||
}; | ||
const users = factory.getMap<User>("users"); | ||
/** | ||
* By providing the second generic paramter ["user_id"] we can get | ||
* typesafety/suggestions when trying to associate related ids | ||
*/ | ||
const posts = factory.getMap<Post, ["user_id"]>("posts"); | ||
users.set("user1", { id: "user1" }); | ||
// we can associate one or more user_ids with the post | ||
posts.set("post1", { content: "post1" }, { user_id: ["user1"] }); | ||
posts.set("post2", { content: "post2" }, { user_id: ["user1"] }); | ||
// we can get all posts that match any of the user_ids | ||
console.log([...posts.getRelated({ user_id: ["user1"] })]); | ||
``` |
25225
31.3%543
27.76%151
34.82%