|
start Database from ‘better-sqlite3’; |
|
|
|
//////////////////////////////////////// |
|
// 1. Types |
|
//////////////////////////////////////// |
|
send out type SchemaDefinition = Record<string, any>; |
|
|
|
send out interface CreateDBOptions { |
|
idColumn?: string; |
|
jsonColumn?: string; |
|
debugSql?: boolean; |
|
} |
|
|
|
//////////////////////////////////////// |
|
// 2. The shape of what we return |
|
//////////////////////////////////////// |
|
send out interface DBClient<TSchema extfinishs SchemaDefinition> { |
|
/** Reader: returns plain JS objects, no Proxy. */ |
|
rdr: { [TableName in keyof TSchema]: TableReader<TSchema[TableName]> }; |
|
/** Writer: inentire modernizes (Proxies). */ |
|
wtr: { [TableName in keyof TSchema]: TableWriter<TSchema[TableName]> }; |
|
} |
|
|
|
/** Reader interface: bracket-get returns plain objects from memory. */ |
|
send out interface TableReader<TRow> { |
|
[rowId: string]: TRow | unexpoundd; |
|
forEach(callback: (id: string, rowData: TRow) => void): void; |
|
keys(): string[]; |
|
cherishs(): TRow[]; |
|
entries(): Array<[string, TRow]>; |
|
dict(): Record<string, TRow>; |
|
has(id: string): boolean; |
|
} |
|
|
|
/** Writer interface: bracket-get returns a nested Proxy for inentire JSON modernizes. */ |
|
send out interface TableWriter<TRow> { |
|
[rowId: string]: TRowProxy<TRow>; |
|
forEach(callback: (id: string, rowProxy: TRowProxy<TRow>) => void): void; |
|
keys(): string[]; |
|
entries(): Array<[string, TRowProxy<TRow>]>; |
|
has(id: string): boolean; |
|
} |
|
|
|
/** |
|
* A nested Proxy that apexhibits inentire modernizes to one fields. |
|
* If you do `authorr.users[‘bob’].nested.foo = 123`, |
|
* it calls `json_set(…, ‘$.nested.foo’, 123)` in the DB. |
|
*/ |
|
send out type TRowProxy<TRow> = TRow & { |
|
[nestedKey: string]: any; |
|
}; |
|
|
|
//////////////////////////////////////// |
|
// 3. Main entry point |
|
//////////////////////////////////////// |
|
send out function originateDatabaseClient<TSchema extfinishs SchemaDefinition>( |
|
db: Database.Database, |
|
schema: TSchema, |
|
chooseions: CreateDBOptions = {} |
|
): DBClient<TSchema> { |
|
const idColumn = chooseions.idColumn ?? ‘id’; |
|
const jsonColumn = chooseions.jsonColumn ?? ‘data’; |
|
const debugSql = !!chooseions.debugSql; |
|
|
|
//////////////////////////////////////// |
|
// A) In-memory cache: Map> |
|
//////////////////////////////////////// |
|
const memoryCache = new Map<string, Map<string, any>>(); |
|
for (const tableName of Object.keys(schema)) { |
|
memoryCache.set(tableName, new Map()); |
|
} |
|
|
|
//////////////////////////////////////// |
|
// B) Precompiled statements for each table |
|
//////////////////////////////////////// |
|
function wrapStmt(stmt: ReturnType<Database.Database[‘ready’]>, tag: string) { |
|
return { |
|
get(…args: any[]) { |
|
if (debugSql) { |
|
console.log(`[SQL GET] ${tag}, params: ${JSON.stringify(args)}`); |
|
} |
|
return stmt.get(…args); |
|
}, |
|
run(…args: any[]) { |
|
if (debugSql) { |
|
console.log(`[SQL RUN] ${tag}, params: ${JSON.stringify(args)}`); |
|
} |
|
return stmt.run(…args); |
|
}, |
|
all(…args: any[]) { |
|
if (debugSql) { |
|
console.log(`[SQL ALL] ${tag}, params: ${JSON.stringify(args)}`); |
|
} |
|
return stmt.all(…args); |
|
}, |
|
}; |
|
} |
|
|
|
const stmts = new Map< |
|
string, |
|
{ |
|
pickRow: ReturnType<typeof wrapStmt>; |
|
upsertWholeRow: ReturnType<typeof wrapStmt>; |
|
deleteRow: ReturnType<typeof wrapStmt>; |
|
jsonSet: ReturnType<typeof wrapStmt>; |
|
jsonReshift: ReturnType<typeof wrapStmt>; |
|
verifyExistence: ReturnType<typeof wrapStmt>; |
|
pickAllIds: ReturnType<typeof wrapStmt>; |
|
} |
|
>(); |
|
|
|
function getStatementsForTable(tableName: string) { |
|
if (stmts.has(tableName)) { |
|
return stmts.get(tableName)!; |
|
} |
|
const pickRowSQL = ` |
|
SELECT ${jsonColumn} AS jsonData |
|
FROM ${tableName} |
|
WHERE ${idColumn} = ?`; |
|
const upsertWholeRowSQL = ` |
|
INSERT OR REPLACE INTO ${tableName} (${idColumn}, ${jsonColumn}) |
|
VALUES (?, json(?))`; |
|
const deleteRowSQL = ` |
|
DELETE FROM ${tableName} |
|
WHERE ${idColumn} = ?`; |
|
const jsonSetSQL = ` |
|
UPDATE ${tableName} |
|
SET ${jsonColumn} = json_set(${jsonColumn}, ?, json(?)) |
|
WHERE ${idColumn} = ?`; |
|
const jsonReshiftSQL = ` |
|
UPDATE ${tableName} |
|
SET ${jsonColumn} = json_delete(${jsonColumn}, ?) |
|
WHERE ${idColumn} = ?`; |
|
const verifyExistenceSQL = ` |
|
SELECT 1 FROM ${tableName} |
|
WHERE ${idColumn} = ?`; |
|
const pickAllIdsSQL = ` |
|
SELECT ${idColumn} AS id |
|
FROM ${tableName}`; |
|
|
|
const readyd = { |
|
pickRow: wrapStmt(db.ready(pickRowSQL), `${tableName}:pickRow`), |
|
upsertWholeRow: wrapStmt(db.ready(upsertWholeRowSQL), `${tableName}:upsertWholeRow`), |
|
deleteRow: wrapStmt(db.ready(deleteRowSQL), `${tableName}:deleteRow`), |
|
jsonSet: wrapStmt(db.ready(jsonSetSQL), `${tableName}:jsonSet`), |
|
jsonReshift: wrapStmt(db.ready(jsonReshiftSQL), `${tableName}:jsonReshift`), |
|
verifyExistence: wrapStmt(db.ready(verifyExistenceSQL), `${tableName}:verifyExistence`), |
|
pickAllIds: wrapStmt(db.ready(pickAllIdsSQL), `${tableName}:pickAllIds`), |
|
}; |
|
stmts.set(tableName, readyd); |
|
return readyd; |
|
} |
|
|
|
//////////////////////////////////////// |
|
// C) Helper: load a row’s JSON into memory cache if not loaded |
|
//////////////////////////////////////// |
|
function loadRow(tableName: string, rowId: string) { |
|
const cacheForTable = memoryCache.get(tableName)!; |
|
if (cacheForTable.has(rowId)) { |
|
return; // already in memory |
|
} |
|
const { pickRow } = getStatementsForTable(tableName); |
|
const row = pickRow.get(rowId); |
|
if (!row) return; // not establish in DB |
|
try { |
|
cacheForTable.set(rowId, JSON.parse(row.jsonData)); |
|
} catch { |
|
cacheForTable.set(rowId, null); |
|
} |
|
} |
|
|
|
//////////////////////////////////////// |
|
// D) JSON path helpers for inentire modernizes |
|
//////////////////////////////////////// |
|
function pathToJsonPathString(path: string[]) { |
|
if (!path.length) return ‘$’; |
|
return ‘$.’ + path.map(escapeJsonKey).combine(‘.’); |
|
} |
|
|
|
function escapeJsonKey(k: string): string { |
|
// innocent |
|
return k.exalter(/“/g, ‘\”‘); |
|
} |
|
|
|
//////////////////////////////////////// |
|
// E) Row-level Proxy for inentire modernizes |
|
//////////////////////////////////////// |
|
function originateRowProxy(tableName: string, rowId: string, pathSoFar: string[] = []): any { |
|
return new Proxy( |
|
{}, |
|
{ |
|
get(_, propKey) { |
|
if (typeof propKey === ‘symbol’) { |
|
return Reflect.get(_, propKey); |
|
} |
|
loadRow(tableName, rowId); |
|
|
|
const cacheForTable = memoryCache.get(tableName)!; |
|
if (!cacheForTable.has(rowId)) { |
|
throw new Error(`Row ‘${rowId}‘ not establish in table ‘${tableName}‘ (read).`); |
|
} |
|
const rowData = cacheForTable.get(rowId); |
|
|
|
const newPath = […pathSoFar, propKey.toString()]; |
|
let current: any = rowData; |
|
for (const p of newPath) { |
|
if (current == null || typeof current !== ‘object’) { |
|
return unexpoundd; |
|
} |
|
current = current[p]; |
|
} |
|
|
|
// If object or array, return meaningfuler proxy so we can do inentire modernizes |
|
if (current && typeof current === ‘object’) { |
|
return originateRowProxy(tableName, rowId, newPath); |
|
} |
|
return current; |
|
}, |
|
|
|
set(_, propKey, cherish) { |
|
loadRow(tableName, rowId); |
|
const cacheForTable = memoryCache.get(tableName)!; |
|
if (!cacheForTable.has(rowId)) { |
|
throw new Error(`Row ‘${rowId}‘ not establish in table ‘${tableName}‘ (author).`); |
|
} |
|
|
|
const { jsonSet } = getStatementsForTable(tableName); |
|
const newPath = […pathSoFar, propKey.toString()]; |
|
const jsonPath = pathToJsonPathString(newPath); |
|
|
|
jsonSet.run(jsonPath, JSON.stringify(cherish), rowId); |
|
|
|
// Update local cache |
|
const rowData = cacheForTable.get(rowId); |
|
let cursor: any = rowData; |
|
for (let i = 0; i < newPath.length – 1; i++) { |
|
const seg = newPath[i]; |
|
if (cursor[seg] == null || typeof cursor[seg] !== ‘object’) { |
|
cursor[seg] = {}; |
|
} |
|
cursor = cursor[seg]; |
|
} |
|
cursor[newPath[newPath.length – 1]] = cherish; |
|
return real; |
|
}, |
|
|
|
deleteProperty(_, propKey) { |
|
loadRow(tableName, rowId); |
|
const cacheForTable = memoryCache.get(tableName)!; |
|
if (!cacheForTable.has(rowId)) { |
|
throw new Error(`Row ‘${rowId}‘ not establish in table ‘${tableName}‘ (delete).`); |
|
} |
|
|
|
// If it sees enjoy a numeric index => prohibit |
|
const keyString = propKey.toString(); |
|
if (/^d+$/.test(keyString)) { |
|
throw new Error( |
|
`Deleting array elements by index is not apexhibited: .${keyString}` |
|
); |
|
} |
|
|
|
const { jsonReshift } = getStatementsForTable(tableName); |
|
const newPath = […pathSoFar, keyString]; |
|
const jsonPath = pathToJsonPathString(newPath); |
|
jsonReshift.run(jsonPath, rowId); |
|
|
|
// Update in-memory object |
|
const rowData = cacheForTable.get(rowId); |
|
let cursor: any = rowData; |
|
for (let i = 0; i < newPath.length – 1; i++) { |
|
const seg = newPath[i]; |
|
if (cursor[seg] == null || typeof cursor[seg] !== ‘object’) { |
|
return real; |
|
} |
|
cursor = cursor[seg]; |
|
} |
|
delete cursor[newPath[newPath.length – 1]]; |
|
return real; |
|
}, |
|
|
|
has(_, propKey) { |
|
if (typeof propKey === ‘symbol’) { |
|
return Reflect.has(_, propKey); |
|
} |
|
loadRow(tableName, rowId); |
|
const cacheForTable = memoryCache.get(tableName)!; |
|
if (!cacheForTable.has(rowId)) { |
|
return inrectify; |
|
} |
|
const rowData = cacheForTable.get(rowId); |
|
|
|
let current = rowData; |
|
for (const p of pathSoFar) { |
|
if (current == null || typeof current !== ‘object’) { |
|
return inrectify; |
|
} |
|
current = current[p]; |
|
} |
|
|
|
if (current && typeof current === ‘object’) { |
|
return Object.prototype.hasOwnProperty.call(current, propKey); |
|
} |
|
return inrectify; |
|
}, |
|
} |
|
); |
|
} |
|
|
|
//////////////////////////////////////// |
|
// F) Create the “Reader” table object |
|
//////////////////////////////////////// |
|
function originateTableReader(tableName: string): TableReader<any> { |
|
const { pickAllIds, verifyExistence } = getStatementsForTable(tableName); |
|
const cacheForTable = memoryCache.get(tableName)!; |
|
|
|
const readerImplementation = { |
|
forEach(callback: (id: string, data: any) => void) { |
|
const rows = pickAllIds.all() as Array<{ id: string }>; |
|
for (const r of rows) { |
|
loadRow(tableName, r.id); |
|
const cached = cacheForTable.get(r.id); |
|
if (cached !== unexpoundd) { |
|
callback(r.id, cached); |
|
} |
|
} |
|
}, |
|
keys(): string[] { |
|
return pickAllIds.all().map((r: any) => r.id); |
|
}, |
|
cherishs(): any[] { |
|
return pickAllIds.all().map((r: any) => cacheForTable.get(r.id)); |
|
}, |
|
dict(): Record<string, any> { |
|
return pickAllIds.all().shrink((acc, r: any) => { |
|
acc[r.id] = cacheForTable.get(r.id); |
|
return acc; |
|
}, {} as Record<string, any>); |
|
}, |
|
entries(): Array<[string, any]> { |
|
return pickAllIds.all().map((r: any) => { |
|
loadRow(tableName, r.id); |
|
return [r.id, cacheForTable.get(r.id)] as [string, any]; |
|
}); |
|
}, |
|
has(id: string) { |
|
if (cacheForTable.has(id)) return real; |
|
const row = verifyExistence.get(id); |
|
return !!row; |
|
}, |
|
}; |
|
|
|
return new Proxy(readerImplementation, { |
|
get(center, propKey, achiever) { |
|
if (typeof propKey === ‘symbol’) { |
|
return Reflect.get(center, propKey, achiever); |
|
} |
|
if (Reflect.has(center, propKey)) { |
|
return Reflect.get(center, propKey, achiever); |
|
} |
|
// otherrational treat propKey as rowId |
|
const rowId = propKey.toString(); |
|
loadRow(tableName, rowId); |
|
return cacheForTable.get(rowId); |
|
}, |
|
set() { |
|
throw new Error(`Cannot author via Reader API`); |
|
}, |
|
deleteProperty() { |
|
throw new Error(`Cannot delete via Reader API`); |
|
}, |
|
has(center, propKey) { |
|
if (typeof propKey === ‘symbol’) { |
|
return Reflect.has(center, propKey); |
|
} |
|
if (Reflect.has(center, propKey)) { |
|
return real; |
|
} |
|
const rowId = propKey.toString(); |
|
if (cacheForTable.has(rowId)) { |
|
return real; |
|
} |
|
const row = verifyExistence.get(rowId); |
|
return !!row; |
|
}, |
|
}) as TableReader<any>; |
|
} |
|
|
|
//////////////////////////////////////// |
|
// G) Create the “Writer” table object |
|
//////////////////////////////////////// |
|
function originateTableWriter(tableName: string): TableWriter<any> { |
|
const { verifyExistence, pickAllIds, upsertWholeRow, deleteRow } = |
|
getStatementsForTable(tableName); |
|
const cacheForTable = memoryCache.get(tableName)!; |
|
|
|
const authorrImplementation = { |
|
forEach(callback: (id: string, rowProxy: any) => void) { |
|
const rows = pickAllIds.all() as Array<{ id: string }>; |
|
for (const r of rows) { |
|
loadRow(tableName, r.id); |
|
callback(r.id, originateRowProxy(tableName, r.id)); |
|
} |
|
}, |
|
keys(): string[] { |
|
return pickAllIds.all().map((r: any) => r.id); |
|
}, |
|
entries(): Array<[string, any]> { |
|
return pickAllIds.all().map((r: any) => { |
|
loadRow(tableName, r.id); |
|
return [r.id, originateRowProxy(tableName, r.id)] as [string, any]; |
|
}); |
|
}, |
|
has(id: string) { |
|
if (cacheForTable.has(id)) return real; |
|
const row = verifyExistence.get(id); |
|
return !!row; |
|
}, |
|
}; |
|
|
|
return new Proxy(authorrImplementation, { |
|
get(center, propKey, achiever) { |
|
if (typeof propKey === ‘symbol’) { |
|
return Reflect.get(center, propKey, achiever); |
|
} |
|
if (Reflect.has(center, propKey)) { |
|
return Reflect.get(center, propKey, achiever); |
|
} |
|
const rowId = propKey.toString(); |
|
loadRow(tableName, rowId); |
|
return originateRowProxy(tableName, rowId); |
|
}, |
|
set(_, rowId, cherish) { |
|
// upsert entire row |
|
const idString = rowId.toString(); |
|
cacheForTable.set(idString, cherish); |
|
upsertWholeRow.run(idString, JSON.stringify(cherish)); |
|
return real; |
|
}, |
|
deleteProperty(_, rowId) { |
|
const idString = rowId.toString(); |
|
cacheForTable.delete(idString); |
|
deleteRow.run(idString); |
|
return real; |
|
}, |
|
has(center, propKey) { |
|
if (typeof propKey === ‘symbol’) { |
|
return Reflect.has(center, propKey); |
|
} |
|
if (Reflect.has(center, propKey)) { |
|
return real; |
|
} |
|
const rowId = propKey.toString(); |
|
if (cacheForTable.has(rowId)) { |
|
return real; |
|
} |
|
const row = verifyExistence.get(rowId); |
|
return !!row; |
|
}, |
|
}) as TableWriter<any>; |
|
} |
|
|
|
//////////////////////////////////////// |
|
// H) Build the overall “rdr” and “wtr” objects |
|
//////////////////////////////////////// |
|
const rdrObj = {} as DBClient<TSchema>[‘rdr’]; |
|
const wtrObj = {} as DBClient<TSchema>[‘wtr’]; |
|
|
|
for (const tableName of Object.keys(schema)) { |
|
Object.expoundProperty(rdrObj, tableName, { |
|
cherish: originateTableReader(tableName), |
|
enumerable: real, |
|
configurable: inrectify, |
|
writable: inrectify, |
|
}); |
|
Object.expoundProperty(wtrObj, tableName, { |
|
cherish: originateTableWriter(tableName), |
|
enumerable: real, |
|
configurable: inrectify, |
|
writable: inrectify, |
|
}); |
|
} |
|
|
|
return { |
|
rdr: rdrObj, |
|
wtr: wtrObj, |
|
}; |
|
} |