import { Knex } from "knex" ; import { Entity , EntityConstructor , entityLimit , EntityManager , EntityMetadata , getMetadata } from "../EntityManager" ; import { deTagId , keyToNumber , keyToString , maybeResolveReferenceToId , unsafeDeTagIds } from "../keys" ; import { FilterAndSettings , parseEntityFilter , parseValueFilter , ValueFilter } from "../QueryBuilder" ; import { ManyToManyCollection , OneToManyCollection , OneToOneReferenceImpl } from "../relations" ; import { JoinRow } from "../relations/ManyToManyCollection" ; import { hasSerde } from "../serde" ; import { JoinRowTodo , Todo } from "../Todo" ; import { fail , partition } from "../utils" ; import { Driver } from "./driver" ; export class InMemoryDriver implements Driver { private data : Record < string , Record < number , any > > = { } ; select ( tableName ) { return Object . values ( this . data [ tableName ] || { } ) ; } insert ( tableName , row ) { row . id ||= this . nextId ( tableName ) ; this . rowsOfTable ( tableName ) [ row . id ] = { ... row } ; } update ( tableName , row ) { const existingRow = this . rowsOfTable ( tableName ) [ row . id ] ; this . rowsOfTable ( tableName ) [ row . id ] = { ... existingRow , ... row } ; } delete ( tableName , id ) { delete this . rowsOfTable ( tableName ) [ id ] ; } clear ( ) { this . data = { } ; } async find < T extends Entity > ( em , type , queries ) { return queries . map ( ( query ) => { const { where , orderBy , limit , offset = 0 } = query ; const meta = getMetadata ( type ) ; const allRows = Object . values ( this . rowsOfTable ( meta . tableName ) ) ; const matched = allRows . filter ( ( row ) => rowMatches ( this , meta , row , where ) ) ; const sorted = ! orderBy ? matched : matched . sort ( ( a , b ) => sort ( this , meta , orderBy as any , a , b ) ) ; return ensureUnderLimit ( sorted . slice ( offset , offset + ( limit ?? sorted . length ) ) ) ; } ) ; } async flushEntities ( em , todos ) { const updatedAt = new Date ( ) ; Object . entries ( todos ) . forEach ( ( [ _ , todo ] ) => { todo . inserts . forEach ( ( i ) => { const row = { } ; Object . values ( todo . metadata . fields ) . filter ( hasSerde ) . flatMap ( ( f ) => f . serde . columns . map ( ( c ) => [ f , c ] as const ) ) . forEach ( ( [ f , c ] ) => { row [ c . columnName ] = c . mapToDb ( i . __orm . data [ f . fieldName ] ) ?? null ; } ) ; row . id = this . nextId ( todo . metadata . tableName ) ; this . rowsOfTable ( todo . metadata . tableName ) [ row . id ] = row ; i . __orm . data [ "id" ] = keyToString ( todo . metadata , row . id ) ; } ) ; todo . updates . forEach ( ( u ) => { u . __orm . data [ "updatedAt" ] = updatedAt ; const id = deTagId ( todo . metadata , u . idOrFail ) ; const row = { } ; Object . values ( todo . metadata . fields ) . filter ( hasSerde ) . flatMap ( ( f ) => f . serde . columns . map ( ( c ) => [ f , c ] as const ) ) . forEach ( ( [ f , c ] ) => { row [ c . columnName ] = c . mapToDb ( u . __orm . data [ f . fieldName ] ) ?? null ; } ) ; this . rowsOfTable ( todo . metadata . tableName ) [ id ] = row ; } ) ; todo . deletes . forEach ( ( d ) => { const id = deTagId ( todo . metadata , d . idOrFail ) ; delete this . rowsOfTable ( todo . metadata . tableName ) [ id ] ; d . __orm . deleted = "deleted" ; } ) ; } ) ; } async flushJoinTables ( em , joinRows ) { for ( const [ joinTableName , { m2m , newRows , deletedRows } ] of Object . entries ( joinRows ) ) { newRows . forEach ( ( row ) => { const { id , created_at , m2m , ... fkColumns } = row ; Object . keys ( fkColumns ) . forEach ( ( key ) => { const meta = key == m2m . columnName ? getMetadata ( m2m . entity ) : m2m . otherMeta ; fkColumns [ key ] = keyToNumber ( meta , maybeResolveReferenceToId ( fkColumns [ key ] ) ) ; } ) ; this . insert ( joinTableName , fkColumns ) ; row . id = fkColumns . id as number ; } ) ; const [ haveIds , noIds ] = partition ( deletedRows , ( r ) => r . id !== - 1 ) ; haveIds . forEach ( ( row ) => this . delete ( joinTableName , row . id ! ) ) ; noIds . forEach ( ( noIdRow ) => { const rows = Object . values ( this . rowsOfTable ( joinTableName ) ) ; rows . filter ( ( row ) => { const a = String ( row [ m2m . columnName ] ) === deTagId ( m2m . meta , maybeResolveReferenceToId ( noIdRow [ m2m . columnName ] ) ! ) ; const b = String ( row [ m2m . otherColumnName ] ) === deTagId ( m2m . otherMeta , maybeResolveReferenceToId ( noIdRow [ m2m . otherColumnName ] ) ! ) ; return a || b ; } ) . forEach ( ( found ) => this . delete ( joinTableName , found . id ) ) ; } ) ; } } async load < T extends Entity > ( em , meta , untaggedIds ) { const rows = Object . values ( this . data [ meta . tableName ] || { } ) ; return rows . filter ( ( row ) => untaggedIds . includes ( String ( row [ "id" ] ) ) ) ; } async loadManyToMany < T extends Entity , U extends Entity > ( em , collection , keys ) { const ids = { } ; keys . forEach ( ( key ) => { const [ column , id ] = key . split ( "=" ) ; ( ids [ column ] ||= [ ] ) . push ( unsafeDeTagIds ( [ id ] ) [ 0 ] ) ; } ) ; const rows = Object . values ( this . rowsOfTable ( collection . joinTableName ) ) ; return rows . filter ( ( row ) => { return Object . entries ( ids ) . some ( ( [ column , ids ] ) => ids . includes ( String ( row [ column ] ) ) ) ; } ) ; } async loadOneToMany < T extends Entity , U extends Entity > ( em , collection , untaggedIds ) { const rows = Object . values ( this . rowsOfTable ( collection . otherMeta . tableName ) ) ; return rows . filter ( ( row ) => untaggedIds . includes ( String ( row [ collection . otherColumnName ] ) ) ) ; } async loadOneToOne < T extends Entity , U extends Entity > ( em , reference , untaggedIds ) { const rows = Object . values ( this . rowsOfTable ( reference . otherMeta . tableName ) ) ; return rows . filter ( ( row ) => untaggedIds . includes ( String ( row [ reference . otherColumnName ] ) ) ) ; } transaction < T > ( em , fn , isolationLevel ? ) { return fn ( undefined ! ) ; } rowsOfTable ( tableName ) { return ( this . data [ tableName ] ||= { } ) ; } private nextId ( tableName ) { return Object . values ( this . data [ tableName ] || { } ) . length + 1 ; } } /** In SQL `a !== null` is false. */ function notEqual ( a , b ) { if ( a === null || b === null ) { return ! ( a === null && b === null ) ; } else { return a !== b ; } } const ops = { gt : ( a , b ) => a > b , gte : ( a , b ) => a >= b , lt : ( a , b ) => a < b , lte : ( a , b ) => a <= b , like : ( a , b ) => new RegExp ( b . replace ( "%" , ".*" ) ) . test ( a ) , ilike : ( a , b ) => new RegExp ( b . replace ( "%" , ".*" ) , "i" ) . test ( a ) , } ; function rowMatches ( driver , meta , row , where ) { return Object . entries ( where as any ) . filter ( ( [ _ , value ] ) => value !== undefined ) . every ( ( [ fieldName , value ] ) => { const field = meta . fields [ fieldName ] || fail ( ) ; const column = meta . fields [ field . fieldName ] || fail ( ) ; const currentValue = ( column . serde && row [ column . serde . columns [ 0 ] . columnName ] ) ?? null ; switch ( field . kind ) { case "primaryKey" : case "primitive" : case "enum" : let fn = ( a ) => a ; if ( field . kind === "enum" ) { fn = ( v ) => ( field . enumDetailType as any ) . getByCode ( v ) . id ; } else if ( field . kind === "primaryKey" ) { fn = ( v ) => keyToNumber ( meta , v as any ) ; } const filter = parseValueFilter ( value as ValueFilter < any , any > ) ; switch ( filter . kind ) { case "eq" : return currentValue === fn ( filter . value ) ; case "ne" : return notEqual ( currentValue , fn ( filter . value ) ) ; case "in" : return filter . value . map ( fn ) . includes ( currentValue ) ; case "gt" : case "gte" : case "lt" : case "lte" : case "like" : case "ilike" : const a = currentValue ; const b = fn ( filter . value ) ; const op = ops [ filter . kind ] ; return op ( a , b ) ; case "pass" : return true ; default : throw new Error ( "Unsupported" ) ; } case "m2o" : const otherMeta = field . otherMetadata ( ) ; const ef = parseEntityFilter ( otherMeta , value ) ; switch ( ef . kind ) { case "eq" : return currentValue === ef . id ; case "ne" : return notEqual ( currentValue , ef . id ) ; case "in" : return ef . ids . includes ( currentValue ) ; case "join" : if ( currentValue === null ) { return false ; } const otherRow = driver . rowsOfTable ( otherMeta . tableName ) [ currentValue ] ; return rowMatches ( driver , otherMeta , otherRow , ef . subFilter ) ; default : throw new Error ( "Unsupported" ) ; } default : throw new Error ( "Unsupported" ) ; } } ) ; } function sort ( driver , meta , orderBy , a , b ) { const fieldName = Object . keys ( orderBy ) [ 0 ] ; const value = ( orderBy as any ) [ fieldName ] ; if ( value !== "ASC" && value !== "DESC" ) { const field = meta . fields [ fieldName ] || fail ( ) ; if ( field . kind === "m2o" ) { const newMeta = field . otherMetadata ( ) ; const newA = driver . rowsOfTable ( newMeta . tableName ) [ a . id ] ; const newB = driver . rowsOfTable ( newMeta . tableName ) [ b . id ] ; return sort ( driver , newMeta , value , newA , newB ) ; } else { throw new Error ( `Unsupported order by field ${ fieldName }` ) ; } } const flip = value === "DESC" ? - 1 : 1 ; const column = meta . fields [ fieldName ] || fail ( ) ; const key = column . serde ! . columns [ 0 ] . columnName ; return a [ key ] . localeCompare ( b [ key ] ) * flip ; } function ensureUnderLimit ( rows ) { if ( rows . length >= entityLimit ) { throw new Error ( `Query returned more than ${ entityLimit } rows` ) ; } return rows ; }