import { TextWriter } from "graphql-ts-client-api" ; import { EntityChangeEvent , EntityEvictEvent } from ".." ; import { FlatRow } from "../meta/Configuration" ; import { FieldMetadata } from "../meta/impl/FieldMetadata" ; import { TypeMetadata } from "../meta/impl/TypeMetdata" ; import { VariableArgs } from "../state/impl/Args" ; import { SpaceSavingMap } from "../state/impl/SpaceSavingMap" ; import { compare } from "../state/impl/util" ; import { GraphField , GraphObject , ParameterizedValue , EvictReasonType } from "../state/Monitor" ; import { Association } from "./assocaition/Association" ; import { RecordConnection } from "./assocaition/AssociationConnectionValue" ; import { BackReferences } from "./assocaition/BackReferences" ; import { EntityManager , Garbage } from "./EntityManager" ; import { Pagination } from "./QueryArgs" ; export class Record { private scalarMap = new Map < string , any > ( ) ; private associationMap = new SpaceSavingMap < FieldMetadata , Association > ( ) ; readonly backReferences = new BackReferences ( ) ; private row ? : FlatRow < any > ; private gcVisited = false ; private derivedRecord ? : Record ; constructor ( readonly superRecord , readonly staticType , readonly runtimeType , readonly id , private deleted = false ) { if ( superRecord !== undefined ) { if ( superRecord . derivedRecord !== undefined ) { throw new Error ( `Internal bug: Both "${ staticType . name }" and "${ superRecord . derivedRecord . staticType . name }" extends "${ superRecord . staticType . name }"` ) ; } superRecord . derivedRecord = this ; } if ( staticType . name === 'Mutation' ) { throw new Error ( `Cannot create record for type 'Mutation'` ) ; } if ( staticType . name === 'Query' ) { if ( id !== QUERY_OBJECT_ID ) { throw new Error ( `The id of query object must be '${ QUERY_OBJECT_ID }'` ) ; } if ( deleted ) { throw new Error ( `The object of special type 'Query' cannot be deleted` ) ; } } } get isDeleted ( ) : boolean { return this . deleted ; } hasScalar ( fieldName , args ? ) { return fieldName === "__typename" ? true : this . scalarMap . has ( VariableArgs . fieldKey ( fieldName , args ) ) ; } getSalar ( fieldName , args ? ) { if ( fieldName === "__typename" ) { return this . runtimeType . name ; } return this . scalarMap . get ( VariableArgs . fieldKey ( fieldName , args ) ) ; } hasAssociation ( field , args ? ) { const fieldMetadata = typeof field === "string" ? this . runtimeType . fieldMap . get ( field ) : field ; if ( fieldMetadata === undefined ) { return false ; } return this . associationMap . get ( fieldMetadata ) ?. has ( args ) === true ; } getAssociation ( field , args ? ) { const fieldMetadata = typeof field === "string" ? this . runtimeType . fieldMap . get ( field ) : field ; if ( fieldMetadata === undefined ) { throw new Error ( `Illegal asscoaition field: "${ field }"` ) ; } return this . associationMap . get ( fieldMetadata ) ?. get ( args ) ; } set ( entityManager , field , args , value , pagination ? ) { if ( field . declaringType !== this . staticType ) { throw new Error ( `'${ field . fullName }' is not field of the type '${ this . staticType . name }' of current record` ) ; } if ( field . isAssociation ) { if ( field . category === "REFERENCE" && args ?. variables !== undefined && this . runtimeType . name !== "Query" ) { throw new Error ( 'reference fields of entity object does not support variables' ) ; } this . associationMap . computeIfAbsent ( field , f => new Association ( this , f ) ) . set ( entityManager , args , value , pagination ) ; } else { if ( args ?. variables !== undefined && this . runtimeType . name !== "Query" ) { throw new Error ( 'scalar fields of entity object does not support variables' ) ; } if ( field . name === "__typename" ) { if ( value !== this . runtimeType . name ) { throw new Error ( `Illegal new value value "${ value }" for "__typename", the runtime type of current record is "${ this . runtimeType . name }"` ) ; } return ; } const fieldKey = VariableArgs . fieldKey ( field . name , args ) ; if ( field === this . staticType . idField ) { if ( value !== this . id ) { throw new Error ( `Cannot chanage "${ this . staticType . idField . fullName } because its id field"` ) ; } } else { const oldValue = this . scalarMap . get ( fieldKey ) ; if ( oldValue !== value ) { this . scalarMap . set ( fieldKey , value ) ; entityManager . modificationContext . set ( this , fieldKey , args , oldValue , value ) ; } } } } link ( entityManager , associationField , record ) { this . associationMap . get ( associationField ) ?. link ( entityManager , record , undefined ) ; } unlink ( entityManager , associationField , record ) { this . associationMap . get ( associationField ) ?. unlink ( entityManager , record , undefined ) ; } contains ( field , args , target , tryMoreStrictArgs ) { return this . associationMap . get ( field ) ?. contains ( args , target , tryMoreStrictArgs ) === true ; } anyValueContains ( field , target ) { return this . associationMap . get ( field ) ?. anyValueContains ( target ) ; } evict ( entityManager , field , args , includeMoreStrictArgs = false , evictReason ? ) { if ( field . declaringType !== this . staticType ) { throw new Error ( `'${ field . fullName }' is not field of the type '${ this . staticType . name }' of current record` ) ; } if ( field . isAssociation ) { this . associationMap . get ( field ) ?. evict ( entityManager , args , includeMoreStrictArgs , evictReason ) ; } else { entityManager . modificationContext . unset ( this , field . name , args , evictReason ) ; this . scalarMap . delete ( VariableArgs . fieldKey ( field . name , args ) ) ; } } delete ( entityManager ) { if ( this . staticType . name === 'Query' ) { throw new Error ( `The object of special type 'Query' cannot be deleted` ) ; } if ( this . deleted ) { return ; } for ( let record = this . derivedRecord ; record !== undefined ; record = record . derivedRecord ) { record . delete ( entityManager ) ; } this . scalarMap . clear ( ) ; this . disposeAssocaitions ( entityManager ) ; this . backReferences . forEach ( ( field , _ , record ) => { if ( field . associationProperties ?. deleteCascade === true ) { record . delete ( entityManager ) ; } else if ( field . isUndefinable || field . category !== "REFERENCE" ) { record . associationMap . get ( field ) ?. unlinkAll ( entityManager , this ) ; } else { throw new Error ( `Cannot delete the record of "${ this . staticType . name }", ` + `the current object is referenced by other objects though the assocation "${ field . fullName }"` + `but that field is neither undefinable nor configured by 'deleteCase = true'` ) ; } } ) ; this . deleted = true ; for ( let record = this . superRecord ; record !== undefined ; record = record . superRecord ) { record . delete ( entityManager ) ; } } undelete ( ) { if ( this . deleted ) { this . deleted = false ; return true ; } return false ; } toRow ( ) { let row = this . row ; if ( row === undefined ) { this . row = row = new FlatRowImpl ( this ) ; } return row ; } createMap ( ) { const map = new Map < string , any > ( ) ; for ( const [ name , value ] of this . scalarMap ) { map . set ( name , value ) ; } this . associationMap . forEachValue ( association => { association . appendTo ( map ) ; } ) ; return map ; } dispose ( entityManager ) { this . disposeAssocaitions ( entityManager ) ; } private disposeAssocaitions ( entityManager ) { this . associationMap . clear ( ) ; } refreshBackReferencesByEvictEvent ( entityManager , event ) { this . backReferences . forEach ( ( field , _ , backReferenceRecord ) => { backReferenceRecord . associationMap . get ( field ) ?. refresh ( entityManager , event ) ; } ) ; } refreshByChangeEvent ( entityManager , field , e ) { this . associationMap . get ( field ) ?. refresh ( entityManager , e ) ; } gcVisit ( field , args ) { this . gcVisited = true ; for ( let record = this . superRecord ; record !== undefined ; record = record . superRecord ) { record . gcVisited = true ; } for ( let record = this . derivedRecord ; record !== undefined ; record = record . derivedRecord ) { record . gcVisited = true ; } if ( field . isAssociation ) { this . associationMap . get ( field ) ?. gcVisit ( args ) ; } } collectGarbages ( output ) { if ( this . gcVisited ) { this . gcVisited = false ; } else { output . push ( this ) ; return ; } this . associationMap . forEachValue ( association => { association . collectGarbages ( output ) ; } ) ; } toString ( ) { const writer = new TextWriter ( ) ; writer . scope ( { "type" : "BLOCK" , multiLines : true } , ( ) => { writer . seperator ( ) ; writer . text ( `"__typeame": ${ this . runtimeType . name }` ) ; writer . seperator ( ) ; writer . text ( `"id": ${ this . id }` ) ; this . writeTo ( writer ) ; } ) ; return writer . toString ( ) ; } private writeTo ( writer ) { this . superRecord ?. writeTo ( writer ) ; for ( const [ key , value ] of this . scalarMap ) { writer . seperator ( ) ; writer . text ( `"${ key }"": ${ JSON . stringify ( value ) }` ) ; } this . associationMap . forEachValue ( association => { association . writeTo ( writer ) ; } ) ; } monitor ( ) { const fields = [ ] ; const parameterizedScalarMap = new Map < string , Map < string , any > > ( ) ; for ( const [ k , v ] of this . scalarMap ) { const colonIndex = k . indexOf ( ":" ) ; if ( colonIndex !== - 1 ) { const name = k . substring ( 0 , colonIndex ) ; const parameter = k . substring ( colonIndex + 1 ) ; let subMap = parameterizedScalarMap . get ( name ) ; if ( subMap === undefined ) { subMap = new Map < string , any > ( ) ; parameterizedScalarMap . set ( name , subMap ) ; } subMap . set ( parameter , v ) ; } } for ( const [ k , v ] of this . scalarMap ) { const colonIndex = k . indexOf ( ":" ) ; if ( colonIndex === - 1 ) { fields . push ( { name : k , value : v } ) ; } } for ( const [ k , subMap ] of parameterizedScalarMap ) { const arr = [ ] ; for ( const [ parameter , value ] of subMap ) { arr . push ( { parameter , value } ) ; } arr . sort ( ( a , b ) => compare ( a , b , "parameter" ) ) ; fields . push ( { name : k , parameterizedValues : arr } ) ; } this . associationMap . forEachValue ( association => { fields . push ( association . monitor ( ) ) ; } ) ; fields . sort ( ( a , b ) => compare ( a , b , "name" ) ) ; const obj = { id : this . id , runtimeTypeName : this . runtimeType . name , fields } ; return obj ; } } export const QUERY_OBJECT_ID = "____QUERY_OBJECT____" ; export function objectWithOnlyId ( record ) { if ( record === undefined ) { return undefined ; } return record . staticType . createObject ( record . id ) ; } export class FlatRowImpl implements FlatRow < any > { constructor ( private record ) { } has ( fieldName ) { return this . record . hasScalar ( fieldName ) || this . record . hasAssociation ( fieldName , undefined ) ; } get ( fieldName ) { if ( this . record . hasScalar ( fieldName ) ) { return this . record . getSalar ( fieldName ) ; } if ( this . record . hasAssociation ( fieldName , undefined ) ) { return this . record . getAssociation ( fieldName , undefined ) ; } throw new Error ( `The field '${ fieldName }' is not cached in the scalar row` ) ; } toString ( ) { return this . record . toString ( ) ; } }