import { ColumnDescription , DataType , RelationDescription , RelationType , } from "../model.ts" ; import type { DatabaseResult , DatabaseValues } from "../adapters/adapter.ts" ; import { Reflect } from "../utils/reflect.ts" ; import { metadata } from "../constants.ts" ; /** Model values from the query result */ interface ModelValues { [ key ] : DatabaseValues | ModelValues | ModelValues [ ] ; } /** Model values which can be sent to the database to insert or update */ interface ModelDatabaseValues { [ key ] : DatabaseValues ; } /** The result of comparing the current model with its original value. */ interface ModelComparisonResult { isDirty : boolean ; diff : ModelDatabaseValues ; } param modelClass export function getTableName ( modelClass ) { const tableName = Reflect . getMetadata ( metadata . tableName , modelClass ) ; if ( ! tableName ) { throw new Error ( `Class '${ modelClass . name }' must be wrapped with @Model decorator!` ) ; } return tableName ; } param modelClass export function getColumns ( modelClass ) { const columns = Reflect . getMetadata ( metadata . columns , modelClass . prototype ) ; if ( ! columns ) { throw new Error ( `Model '${ modelClass . name }' must have at least one column!` ) ; } return columns ; } param modelClass param propertyName export function findColumn ( modelClass , propertyName ) { return getColumns ( modelClass ) . find ( ( item ) => item . propertyKey === propertyName ) ; } param modelClass param includes export function getRelations ( modelClass , includes ? ) { const relations = Reflect . hasMetadata ( metadata . relations , modelClass . prototype ) ? Reflect . getMetadata ( metadata . relations , modelClass . prototype ) : [ ] ; return includes ? relations . filter ( ( item ) => includes . includes ( item . propertyKey ) ) : relations ; } param modelClass export function getPrimaryKeyInfo ( modelClass ) { const primaryKey = getColumns ( modelClass ) . find ( ( item ) => item . isPrimaryKey ) ; if ( ! primaryKey ) { throw new Error ( `Model '${ modelClass . name }' must have a primary key!` ) ; } return primaryKey ; } /** Map of models' original values */ const originalValues = new WeakMap < Object , ModelValues > ( ) ; /** Map of models' `isSaved` status */ const isSavedValues = new WeakMap < Object , boolean > ( ) ; param model export function isSaved ( model ) { return isSavedValues . get ( model ) ? true : false ; } param model param value export function setSaved ( model , value ) { isSavedValues . set ( model , value ) ; if ( value ) { const values = getValues ( model ) ; originalValues . set ( model , values ) ; Object . assign ( model , mapValueProperties ( model . constructor , values , "propertyKey" ) ) ; } else { originalValues . delete ( model ) ; } } param model param value export function getOriginal ( model ) { return originalValues . get ( model ) ; } param model export function compareWithOriginal ( model ) { const originalValue = getOriginal ( model ) ; if ( ! originalValue ) { return { isDirty : true , diff : { } } ; } let isDirty = false ; const diff = { } ; for ( const column of getColumns ( model . constructor ) ) { const value = ( model as any ) [ column . propertyKey ] ; if ( value !== originalValue [ column . name ] ) { isDirty = true ; diff [ column . name ] = getNormalizedValue ( column . type , value ) ; } } return { isDirty , diff } ; } param model export function getValues ( model ) { const columns = getColumns ( model . constructor ) . filter ( ( item ) => item . isPrimaryKey && ! isSaved ( model ) ? false : true ) ; const data = { } ; for ( const column of columns ) { const value = ( model as any ) [ column . propertyKey ] ; if ( column . isPrimaryKey ) { if ( isSaved ( model ) ) { data [ column . name ] = value as number ; } } else { if ( typeof value === "undefined" ) { if ( typeof column . default !== "undefined" ) { const defaultValue = typeof column . default === "function" ? column . default ( ) : column . default ; data [ column . name ] = getNormalizedValue ( column . type , defaultValue ) ; } else { data [ column . name ] = null ; } } else { data [ column . name ] = getNormalizedValue ( column . type , ( model as any ) [ column . propertyKey ] ) ; } } } return data ; } param model param relations export function getRelationValues ( model , relations ? ) { const data = [ ] ; for ( const relation of getRelations ( model . constructor , relations ) ) { const relationData = ( model as any ) [ relation . propertyKey ] ; if ( relation . type === RelationType . BelongsTo && relationData && relationData instanceof relation . getModel ( ) ) { if ( ! isSaved ( relationData ) ) { throw new Error ( `Unsaved relationships found when trying to insert '${ model . constructor . name }' model!` ) ; } const relationIdProperty = getPrimaryKeyInfo ( relation . getModel ( ) ) . propertyKey ; data . push ( { description : relation , value : relationData [ relationIdProperty ] } ) ; } if ( relation . type === RelationType . HasMany && Array . isArray ( relationData ) && relationData . length >= 1 ) { const relationIdProperty = getPrimaryKeyInfo ( relation . getModel ( ) ) . propertyKey ; const value = relationData . map ( ( item ) => { if ( ! isSaved ( item ) ) { throw new Error ( `Unsaved relationships found when trying to insert '${ model . constructor . name }' model!` ) ; } return item [ relationIdProperty ] ; } ) ; data . push ( { description : relation , value } ) ; } } return data ; } param modelClass param values param to export function mapValueProperties ( modelClass , values , to ) { const data = { } ; const columns = getColumns ( modelClass ) ; for ( const key in values ) { const column = columns . find ( ( item ) => item [ to === "name" ? "propertyKey" : "name" ] === key ) ; if ( column ) { data [ column [ to ] ] = values [ key ] ; } } return data ; } param type export function getDataType ( type ) { if ( type === String ) { return DataType . String ; } else if ( type === Number ) { return DataType . Number ; } else if ( type === Date ) { return DataType . Date ; } else if ( type === Boolean ) { return DataType . Boolean ; } else { return null ; } } param type param original param throws export function getNormalizedValue ( type , original ) { if ( typeof original === "undefined" || original === null ) { return null ; } if ( type === DataType . Date && ( typeof original === "string" || typeof original === "number" ) ) { return new Date ( original ) ; } if ( type === DataType . String && typeof original !== "string" ) { return String ( original ) ; } if ( type === DataType . Number && typeof original !== "number" ) { const num = Number ( original ) ; if ( isNaN ( num ) ) { throw new Error ( `Found NaN when converting '${ typeof original }' to number!` ) ; } return Number ( original ) ; } if ( type === DataType . Boolean && typeof original !== "boolean" ) { return Boolean ( original ) ; } return original ; } /**
 * Map the query builder result for creating a model.
 */ export function mapQueryResult ( modelClass , result , includes ? ) { const relations = includes ? getRelations ( modelClass , includes ) : [ ] ; const primaryKey = getPrimaryKeyInfo ( modelClass ) ; return result . reduce < ModelValues [ ] > ( ( prev , next ) => { if ( prev . length !== 0 && prev [ prev . length - 1 ] [ primaryKey . propertyKey ] === next [ getTableName ( modelClass ) + "__" + primaryKey . name ] ) { for ( const relation of relations ) { if ( relation . type === RelationType . HasMany ) { const data = mapSingleQueryResult ( relation . getModel ( ) , next ) ; const previousData = prev [ prev . length - 1 ] [ relation . propertyKey ] as ModelValues [ ] ; if ( Array . isArray ( previousData ) ) { prev [ prev . length - 1 ] [ relation . propertyKey ] = previousData . concat ( [ data ] ) ; } else { prev [ prev . length - 1 ] [ relation . propertyKey ] = [ data ] ; } } } } else { const data = mapSingleQueryResult ( modelClass , next ) ; for ( const relation of relations ) { const tableName = getTableName ( relation . getModel ( ) ) ; const relationPrimaryKey = getPrimaryKeyInfo ( relation . getModel ( ) ) . name ; if ( relation . type === RelationType . HasMany ) { if ( next [ tableName + "__" + relationPrimaryKey ] === null ) { data [ relation . propertyKey ] = [ ] ; } else { data [ relation . propertyKey ] = [ mapSingleQueryResult ( relation . getModel ( ) , next ) , ] ; } } else if ( relation . type === RelationType . BelongsTo ) { if ( next [ tableName + "__" + relationPrimaryKey ] === null ) { data [ relation . propertyKey ] = null ; } else { data [ relation . propertyKey ] = mapSingleQueryResult ( relation . getModel ( ) , next ) ; } } } prev . push ( data ) ; } return prev ; } , [ ] ) ; } /**
 * Map a single record from the query builder result for creating a model.
 */ export function mapSingleQueryResult ( modelClass , result ) { const values = { } ; const tableName = getTableName ( modelClass ) ; const columns = getColumns ( modelClass ) ; for ( const column in result ) { if ( column . startsWith ( tableName + "__" ) ) { const columnName = column . slice ( tableName . length + 2 ) ; const propertyKey = columns . find ( ( item ) => item . name === columnName ) ?. propertyKey ; if ( propertyKey ) { values [ propertyKey ] = result [ column ] ; } } } return values ; } param modelClass param data param fromDatabase export function createModel < T > ( modelClass , data , fromDatabase = false ) { const values = { } ; for ( const column of getColumns ( modelClass ) ) { const value = data [ column . propertyKey ] ; if ( column . isPrimaryKey ) { if ( fromDatabase ) { values [ column . propertyKey ] = value as number ; } } else { if ( value === undefined ) { if ( typeof column . default !== "undefined" ) { const defaultValue = typeof column . default === "function" ? column . default ( ) : column . default ; values [ column . propertyKey ] = getNormalizedValue ( column . type , defaultValue ) ; } } else { values [ column . propertyKey ] = getNormalizedValue ( column . type , value as DatabaseValues ) ; } } } for ( const relation of getRelations ( modelClass ) ) { const relationModel = relation . getModel ( ) ; const relationData = data [ relation . propertyKey ] as ModelValues ; if ( relation . type === RelationType . BelongsTo ) { if ( relationData ) { values [ relation . propertyKey ] = createModel ( relationModel , relationData , fromDatabase ) ; } } else if ( relation . type === RelationType . HasMany ) { if ( Array . isArray ( relationData ) ) { if ( relationData . length >= 1 ) { values [ relation . propertyKey ] = createModels ( relationModel , relationData , fromDatabase ) ; } else { values [ relation . propertyKey ] = [ ] ; } } } } const model = Object . assign ( Object . create ( modelClass . prototype ) , values ) ; if ( fromDatabase ) { setSaved ( model , true ) ; } return model ; } param modelClass param data export function createModels < T > ( modelClass , data , fromDatabase = false ) { return data . map ( ( item ) => createModel ( modelClass , item , fromDatabase ) ) ; }