2024-03-16 22:29:24 +08:00
import { applyQueryNoFilterKV } from "../../plug-api/lib/query.ts" ;
2024-07-30 23:33:33 +08:00
import type {
2024-07-30 03:21:16 +08:00
FunctionMap ,
KV ,
KvKey ,
KvQuery ,
QueryExpression ,
} from "../../plug-api/types.ts" ;
2024-03-16 22:29:24 +08:00
import { builtinFunctions } from "../builtin_query_functions.ts" ;
2024-07-30 23:33:33 +08:00
import type { KvPrimitives } from "./kv_primitives.ts" ;
2024-03-16 22:29:24 +08:00
import { evalQueryExpression } from "../../plug-api/lib/query_expression.ts" ;
2023-09-04 03:15:17 +08:00
/ * *
* This is the data store class you ' ll actually want to use , wrapping the primitives
* in a more user - friendly way
* /
export class DataStore {
2023-10-03 20:16:33 +08:00
constructor (
2023-12-07 01:44:48 +08:00
readonly kv : KvPrimitives ,
2024-02-03 02:19:07 +08:00
public functionMap : FunctionMap = builtinFunctions ,
2024-07-30 03:21:16 +08:00
public objectEnrichers : ObjectEnricher [ ] = [ ] ,
2023-10-03 20:16:33 +08:00
) {
}
async get < T = any > ( key : KvKey ) : Promise < T | null > {
return ( await this . batchGet ( [ key ] ) ) [ 0 ] ;
2023-09-04 03:15:17 +08:00
}
2024-07-30 03:21:16 +08:00
async batchGet < T = any > ( keys : KvKey [ ] ) : Promise < ( T | null ) [ ] > {
2024-01-14 01:12:48 +08:00
if ( keys . length === 0 ) {
2024-07-30 03:21:16 +08:00
return [ ] ;
2024-01-14 01:12:48 +08:00
}
2024-07-30 03:21:16 +08:00
const results = await this . kv . batchGet ( keys ) ;
// Enrich the objects based on object enrichers
for ( const entry of results ) {
this . enrichObject ( entry ) ;
}
return results ;
2023-09-04 03:15:17 +08:00
}
2023-10-03 20:16:33 +08:00
set ( key : KvKey , value : any ) : Promise < void > {
return this . batchSet ( [ { key , value } ] ) ;
2023-09-04 03:15:17 +08:00
}
2023-10-03 20:16:33 +08:00
batchSet < T = any > ( entries : KV < T > [ ] ) : Promise < void > {
2024-01-14 01:12:48 +08:00
if ( entries . length === 0 ) {
return Promise . resolve ( ) ;
}
2023-10-03 20:16:33 +08:00
const allKeyStrings = new Set < string > ( ) ;
const uniqueEntries : KV [ ] = [ ] ;
for ( const { key , value } of entries ) {
const keyString = JSON . stringify ( key ) ;
if ( allKeyStrings . has ( keyString ) ) {
console . warn ( ` Duplicate key ${ keyString } in batchSet, skipping ` ) ;
} else {
allKeyStrings . add ( keyString ) ;
2024-07-30 03:21:16 +08:00
this . cleanEnrichedObject ( value ) ;
2023-12-10 20:23:42 +08:00
uniqueEntries . push ( { key , value } ) ;
2023-10-03 20:16:33 +08:00
}
}
return this . kv . batchSet ( uniqueEntries ) ;
2023-09-04 03:15:17 +08:00
}
delete ( key : KvKey ) : Promise < void > {
2023-10-03 20:16:33 +08:00
return this . batchDelete ( [ key ] ) ;
2023-09-04 03:15:17 +08:00
}
batchDelete ( keys : KvKey [ ] ) : Promise < void > {
2024-01-14 01:12:48 +08:00
if ( keys . length === 0 ) {
return Promise . resolve ( ) ;
}
2023-12-10 20:23:42 +08:00
return this . kv . batchDelete ( keys ) ;
2023-09-04 03:15:17 +08:00
}
2024-02-03 02:19:07 +08:00
async query < T = any > (
query : KvQuery ,
variables : Record < string , any > = { } ,
) : Promise < KV < T > [ ] > {
2023-10-03 20:16:33 +08:00
const results : KV < T > [ ] = [ ] ;
2023-09-04 03:15:17 +08:00
let itemCount = 0 ;
2023-10-03 20:16:33 +08:00
// Accumulate results
let limit = Infinity ;
if ( query . limit ) {
2024-02-03 02:19:07 +08:00
limit = await evalQueryExpression (
query . limit ,
{ } ,
variables ,
this . functionMap ,
) ;
2023-10-03 20:16:33 +08:00
}
for await (
2023-12-10 20:23:42 +08:00
const entry of this . kv . query ( query )
2023-10-03 20:16:33 +08:00
) {
2024-07-30 03:21:16 +08:00
// Enrich
this . enrichObject ( entry . value ) ;
2023-09-04 03:15:17 +08:00
// Filter
2023-10-03 20:16:33 +08:00
if (
query . filter &&
2024-02-03 02:19:07 +08:00
! await evalQueryExpression (
query . filter ,
entry . value ,
variables ,
this . functionMap ,
)
2023-10-03 20:16:33 +08:00
) {
2023-09-04 03:15:17 +08:00
continue ;
}
results . push ( entry ) ;
itemCount ++ ;
// Stop when the limit has been reached
2023-10-03 22:54:03 +08:00
if ( itemCount === limit && ! query . orderBy ) {
// Only break when not also ordering in which case we need all results
2023-09-04 03:15:17 +08:00
break ;
}
}
2023-10-03 20:16:33 +08:00
// Apply order by, limit, and select
2024-02-03 02:19:07 +08:00
return applyQueryNoFilterKV (
query ,
results ,
variables ,
this . functionMap ,
) ;
2023-10-03 20:16:33 +08:00
}
2023-09-04 03:15:17 +08:00
2024-02-03 02:19:07 +08:00
async queryDelete (
query : KvQuery ,
variables : Record < string , any > = { } ,
) : Promise < void > {
2023-10-03 20:16:33 +08:00
const keys : KvKey [ ] = [ ] ;
for (
2024-02-03 02:19:07 +08:00
const { key } of await this . query ( query , variables )
2023-10-03 20:16:33 +08:00
) {
keys . push ( key ) ;
2023-09-04 03:15:17 +08:00
}
2023-10-03 20:16:33 +08:00
return this . batchDelete ( keys ) ;
}
2024-07-30 03:21:16 +08:00
/ * *
2024-07-30 17:46:42 +08:00
* Enriches the object with the attributes defined in the object enrichers on the fly .
* Will add a ` $ dynamicAttributes ` array to the object to keep track of the dynamic attributes set ( for cleanup )
2024-07-30 03:21:16 +08:00
* @param object
* @returns
* /
enrichObject ( object : any ) {
// Check if this object looks like an object value
if ( ! object || typeof object !== "object" ) {
// Skip
return ;
}
for ( const enricher of this . objectEnrichers ) {
const whereEvalResult = evalQueryExpression (
enricher . where ,
object ,
{ } , // We will not support variables in enrichers for now
this . functionMap ,
) ;
if ( whereEvalResult instanceof Promise ) {
// For performance reasons we can only allow synchronous where clauses
throw new Error (
` Enricher where clause cannot be an async function: ${ enricher . where } ` ,
) ;
}
if (
whereEvalResult
) {
// The `where` matches so we should enrich this object
for (
const [ attributeSelector , expression ] of Object . entries (
enricher . attributes ,
)
) {
// Recursively travel to the attribute based on the selector, which may contain .'s to go deeper
let objectValue = object ;
const selectorParts = attributeSelector . split ( "." ) ;
for ( const part of selectorParts . slice ( 0 , - 1 ) ) {
if ( typeof objectValue [ part ] !== "object" ) {
// Pre-create the object if it doesn't exist
objectValue [ part ] = { } ;
}
objectValue = objectValue [ part ] ;
}
const value = evalQueryExpression (
expression ,
object ,
{ } ,
this . functionMap ,
) ;
if ( value instanceof Promise ) {
// For performance reasons we can only allow synchronous expressions
throw new Error (
` Enricher dynamic attribute expression cannot be an async function: ${ expression } ` ,
) ;
}
2024-07-30 17:46:42 +08:00
const lastPart = selectorParts [ selectorParts . length - 1 ] ;
if ( objectValue [ lastPart ] !== undefined ) {
// The attribute already exists, we should merge the values if we can, or ignore the new value
if ( Array . isArray ( objectValue [ lastPart ] ) && Array . isArray ( value ) ) {
// If the attribute already exists and is an array, we should merge the arrays
objectValue [ lastPart ] = [ . . . objectValue [ lastPart ] , . . . value ] ;
} else {
// We can't merge the values, so we just ignore the new value
}
} else { // New attribute
objectValue [ lastPart ] = value ;
if ( ! object . $dynamicAttributes ) {
object . $dynamicAttributes = [ ] ;
}
object . $dynamicAttributes . push ( attributeSelector ) ;
}
2024-07-30 03:21:16 +08:00
}
}
}
}
/ * *
* Reverses the enriching of the object with the attributes defined in objectEnrichers
* @param object
* @returns
* /
cleanEnrichedObject ( object : any ) {
2024-07-30 17:46:42 +08:00
// Check if this is an enriched object
if ( ! object || ! object . $dynamicAttributes ) {
2024-07-30 03:21:16 +08:00
// Skip
return ;
}
2024-07-30 17:46:42 +08:00
for ( const attributeSelector of object . $dynamicAttributes ) {
// Recursively travel to the attribute based on the selector, which may contain .'s to go deeper
let objectValue = object ;
const selectorParts = attributeSelector . split ( "." ) ;
for ( const part of selectorParts . slice ( 0 , - 1 ) ) {
if ( typeof objectValue [ part ] !== "object" ) {
// This shouldn't happen, but let's back out
break ;
2024-07-30 03:21:16 +08:00
}
2024-07-30 17:46:42 +08:00
objectValue = objectValue [ part ] ;
2024-07-30 03:21:16 +08:00
}
2024-07-30 17:46:42 +08:00
delete objectValue [ selectorParts [ selectorParts . length - 1 ] ] ;
2024-07-30 03:21:16 +08:00
}
// Clean up empty objects, this is somewhat questionable, because it also means that if the user intentionally kept empty objects in there, these will be wiped
cleanupEmptyObjects ( object ) ;
2024-07-30 17:46:42 +08:00
delete object . $dynamicAttributes ;
2024-07-30 03:21:16 +08:00
}
}
export type ObjectEnricher = {
// If this expression evaluates to true for the given object
where : QueryExpression ;
// Dynamically add these attributes to the object, can use "." syntax for deeper attribute definition
attributes : Record < string , QueryExpression > ;
} ;
/ * *
* Recursively removes empty objects from the object
* @param object
* /
export function cleanupEmptyObjects ( object : any ) {
for ( const key in object ) {
// Skip arrays
if ( Array . isArray ( object [ key ] ) ) {
continue ;
}
if ( typeof object [ key ] === "object" ) {
cleanupEmptyObjects ( object [ key ] ) ;
if ( Object . keys ( object [ key ] ) . length === 0 ) {
delete object [ key ] ;
}
}
}
2023-09-04 03:15:17 +08:00
}