Source

gom/cnObject.js

//
// $Id$
//

import { RestServices, RestResponse } from "../tools/RestServices.js"
import { Context } from "./../infohub/Context.js"
import { ApiResponse } from "../tools/ApiResponse.js"

/**
 *  
 *  @category COLNEO gom <br>
 *    <font size="6pt">Generic Object Model</font>
 * 
 *  @classdesc Base Class for nodes/objects on COLNEO infohub. <br>
    Uses the infohub GOM (Generic Object Model)
      
 */
export class cnObject {

  // GOM json data
  #_data = null
  
  /**
   *   
   * 
   * @since 08.2025, jh
   * 
   */
  constructor() {        
  }

  /**
   *  Set object from JSON in COLNEO Generic Object Model (GOM) format.
   * 
   *  @param {object} obj_as_data 
   *  @param {Array|null} members 
   *    Take only members into account, if null use all members. <br>   
   * 
   */
  setFromJson( obj_as_json , members = null ) {
    
    if( members == null ) {
      this.#_data = obj_as_json
    } else {
      this.#_data = {}
      for( const m of members ) {
        this.#_data[m] = obj_as_json[m]
      }
    }
  }

  /**
   * 
   *  Get object as JSON in COLNEO Generic Object Model format (cnGOM).
   *
   *  @param {Array|null} members 
   *  Take only members into account, if null use all members. <br>
   *  Example:
   *  o.getAsJson( [ 'info' , 'properties' ] )
   * 
   *  @returns {object} Object as JSON in GOM format (Generic Object Model)
   * 
   */
  getAsJson( members = null ) {
    
    if( members ) {
      
      let retobj = {}

      for ( const m of members ) {
        retobj[m] = this.#_data[m]
      }

      return retobj

    } else {
      return this.#_data
    }
    
  }

  /**
   *  Print current object to console.
   * 
   *  @returns {void}
   * 
   */
  dump( prefix = '' ) {
    
    if( prefix.length > 0 ) {
      prefix = prefix + ' '
    }

    if (this.#_data) {
      console.log(`${prefix}data:`)
      console.log(JSON.stringify(this.#_data, null, 2))
    } else {
      console.log('-empty-')
    }

  }
  
 
  /**
   *  Global unique ID
   */
  get id() { return this.getId(); }
  
  /**
   *  Node ID / Short ID
   */
  get shortid() { return this.getShortId(); }


  get name() { return this.getName(); }
   
  set name(value) {
    this.setName(value)
  }

  /**
   *  Get ID
   * 
   *  @returns {string|null} Object ID, null if object is empty
   * 
   *  @since 1.0, 09.2025, jh
   * 
   */
  getId() {
    return this.#_data?.info?.object_id
  }
  
  /**
   *  
   *  Retrieves the short object ID from the internal json object.
   * 
   *  @returns {string|null} The short ID if available, otherwise null.
   * 
   *  @since 1.0, 09.2025, jh
   * 
   */
  getShortId() {
    return this.#_data?.info?.shortid
  }
    
  /**
   * 
   * Get 'info.object_name'
   * 
   * @returns info.object_name
   */
  getName() {
    return this.#_data?.info?.object_name
  }
  
  /**
   *  Set info.object_name
   * 
   *  @param {string} n Set object_name to n
   * 
   */
  setName( n ) {
    this.#_data.info.object_name = n
  }

  /**
  *   Retrieves a top-level member of the internal #_data object by key.
  *
  *   Common keys include: "info", "properties", "geometry", etc.
  *
  *   @param {string} key - The name of the member to retrieve.
  *   @param {*} [defaultValue={}] - Value returned if the key does not exist.
  *   @returns {*} The value of the specified member, or the defaultValue if missing.
 */
  get(key, defaultValue = {} ) {
    return this.#_data?.[key] ?? defaultValue;
  }

/**
 * 
 *  Retrieves the value of a property from the internal json object.
 *
 *  @param {string} typeid - The property key to retrieve.
 *  @param {*} [defaultValue=null] - A fallback value returned if the property does not exist.
 * 
 *  @returns {*} The property value if found, otherwise the defaultValue.
 * 
 */
  getPropertyValue( typeid , defaultValue = null ) {
    
    console.log("### <getPropertyValue()>")
    // return this.#_data?.properties?.[typeid] ?? defaultValue;
    
    console.log( JSON.stringify(this.#_data) )
    console.log( JSON.stringify(this.#_data.properties) )

    let v = this.#_data.properties[typeid]

    console.log( `return typeid = ${typeid}`)
    console.log( `return v = ${v}`)

    return v

    // return (this.#_data && this.#_data.properties && this.#_data.properties[typeid]) ?? defaultValue;

  }

  /**
   * 
   * Set the property value to this object.<br>
   * Value is not syncronised with infohub.
   * 
   * @param {string} typeid 
   * @param {any} value 
   */
  setPropertyValue(typeid, value) {
    this.#_data ??= {};
    this.#_data.properties ??= {};
    this.#_data.properties[typeid] = value;  
  }

}


// ###################################################################################################


/**
 *
 *  @category COLNEO gom <br>
 *    <font size="6pt">Generic Object Model</font>
 * 
 *  @classdesc
 *    Service Class for COLNEO infohub objects/nodes
 * 
 *  @since 
 *    1.0, 09.2025, jh
 * 
 */
export class cnObjectServices {

  #_ctx = null 

   /**
   *   
   * @param {Context} ctx Infohub Context
   * 
   * @since 08.2025, jh
   * 
   */
  constructor( ctx ) {      
    this.#_ctx = ctx  
  }

  /**
   * 
   *  Fetch an object by ID and (optional) context. <br>
   *  If context node is null the current project as used as context (root node for search).
   *      
   *  @param {string}   object_id           - The ID of the object to retrieve
   *  @param {function} cb                  - Callback function that receives the result object, 
   *    {
   *      data: cnObject
   *    }
   *  @param {string}   [context_sid=null]  - Optional session or context identifier
   * 
   *  @returns {Promise<ApiResponse>} Since this uses a callback, the function itself resolves with no value
   * 
   *  @since
   *    1.0, 09.2025, jh
   * 
   */
  async getObject( object_id , cb , context_sid = null ) {

    console.log( '### <cnObjectservices.getObject()> ')

    try {

      let q = ''
      let c = ''

      if (context_sid) {
        q += `context=$node ~eq~ '${context_sid}'`
      } else {
        q += `context=$node ~eq~ '${this.#_ctx.getProjectShortId()}'`
      }

      c = '&'

      let f = `$object_id ~eq~ '${object_id}' `
      q += c + `filter=${f}`

      //
      // RESOLVE MEMBERS

      const url = `${this.#_ctx.getServiceUrl(Context.SERVICE.HUB)}/${this.#_ctx.getScope()}/objects?${q}`;

      //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
      /* RestResponse */ const resp = await RestServices.makeApiCall( this.#_ctx.getToken() , RestServices.METHODS.GET , url )
      //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

      if ( resp.getStatus() < 300 ) {
                
        let data = resp.getData()

        if (data.length == 1) {

          // console.log("DATA:")
          // console.log(JSON.stringify(data))

          let obj = new cnObject()
          obj.setFromJson( data[0] )

          return cb( new ApiResponse( 200 , obj ) )

        } else {

          return cb( new ApiResponse( 406 , null , `${data.length} objects found with given ID = [${object_id}].`) )

        }

      } else {
        
        return cb( resp )

      }
      
    } catch (error) {
      // console.error("getObject failed:", error);
      return cb( new ApiResponse( 400 , null , "Unknown error") )
    }

  }

  /**
   * 
   *  Delete Object including all its versions in parent object 'parent_sid'.
   * 
   *  @param {string}   object_id 
   *  @param {string}   parent_sid 
   *  @param {function} cb 
   *  
   *  @returns {Promise<ApiResponse>} Since this uses a callback, the function itself resolves with no value   
   *  @since 1.0, 09.2025, jh
   * 
   */
  async deleteObject( object_id, parent_sid, cb ) {

    let q = `object_id=${object_id}`

    const url = `${this.#_ctx.getServiceUrl(Context.SERVICE.HUB)}/${this.#_ctx.getScope()}/objects/${parent_sid}/object?${q}`;
  
    //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
    /* RestResponse */ const resp = await RestServices.makeApiCall(this.#_ctx.getToken(), RestServices.METHODS.DELETE, url)
    //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
      
    return cb(resp)
        
  }

  /**
   *  Save object on infohub.
   * 
   *  Take 'info' block of object (GOM) into account. 
   *  <br>   
   *  Use object_id to create a new or update an existing object, <br>
   *  Use parent_shortid as parent to create a new object or identify an existing object. <br>
   *  If also parent_shortid is missing, use project_shortid or catalogue_shortid as parent. <br>
   *  In any other cases the object is created in the current project stored in the Context object of this object service.
   *  
   *  PUT - /{scope}/objects/{parent}/object
   * 
   *  @param {cnObject} cnObj
   *  @param {Array|null} members 
   *    Save/Write these members which is one of [ 'info' , 'properties' , etc. ] <br>
   *    If null, write all members
   * 
   *  @param {function} cb 
   * 
   *  @returns {Promise<ApiResponse>} Since this uses a callback, the function itself resolves with no value   
   *  @since 1.0, 09.2025, jh
   * 
   */
  async saveObject( cnObj , members , cb ) {
         
    let parent_sid = cnObj.get('info')['parent_shortid']
      
    if (parent_sid == undefined) {

      parent_sid = cnObj.get('info')['project_shortid']

      if (parent_sid == undefined) {
        parent_sid = cnObj.get('info')['catalogue_shortid']
      }

      if (parent_sid == undefined) {
        parent_sid = this.#_ctx.getProjectShortId()
      }

    } 
        
    // create object/node ...

    let object_data = cnObj.getAsJson( members )
        
    const url = `${this.#_ctx.getServiceUrl(Context.SERVICE.HUB)}/${this.#_ctx.getScope()}/objects/${parent_sid}/object`;

    //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
    /* RestResponse */ const resp = await RestServices.makeApiCall(this.#_ctx.getToken(), RestServices.METHODS.PUT, url, object_data )
    //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

    // resp.dump("RETURN UPDATE OBJECT")

    if( resp.status < 300 ) {

      let obj = new cnObject()
      obj.setFromJson( resp.data )

      return cb(new ApiResponse(resp.status, obj))

    } else {

      return cb(resp)

    }

  }

  /**
   * 
   *  Create new object on infohub.
   *  <br>
   *  Create a new object if object_id (in 'info') does not exist, create a new version if object_id exist. <br>
   *  Function uses endpoint `POST .../{scope}/nodes/{node}` on service 'hub'.
   * 
   *  @param {object} object_data 
   *    Object in GOM format. <br>
   *    Contains object_id in info block.
   * 
   *  @param {string} parent_sid 
   *  @param {function} cb Returns AüiResponse object with an cnObject as data if REST call has been sucessful
   * 
   *  @returns {Promise<ApiResponse>} 
   *    Since this uses a callback, the function itself resolves with no value
   * 
   *  @since 1.0, 09.2025, jh
   * 
   */
  async createObject(object_data, parent_sid, cb) {

    // /{scope}/nodes/{node}

    const url = `${this.#_ctx.getServiceUrl(Context.SERVICE.HUB)}/${this.#_ctx.getScope()}/nodes/${parent_sid}`;

    //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
    /* RestResponse */ const resp = await RestServices.makeApiCall(this.#_ctx.getToken(), RestServices.METHODS.POST, url, object_data )
    //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

    // resp.dump("RETURN CREATE OBJECT")

    if( resp.status < 300 ) {

      let obj = new cnObject()
      obj.setFromJson( resp.data )

      return cb(new ApiResponse(resp.status, obj))

    } else {

      return cb(resp)

    }

    // return cb(resp)

  }

  /**
   * 
   *  Factory method to instantiate a new object of class cnObject. <br>
   *  The object is not created or written to infohub. To save the object on infohub
   *  use method saveObject().
   * 
   *  @param {object} node_json Object as json in GOM format
   * 
   *  @returns {cnObject} New Object
   * 
   */
  newObjectFromJson( node_json , members = null ) {
    let obj = new cnObject()
    obj.setFromJson(node_json,members)
    return obj
  }
    
  /**
   * 
   *  Fetch objects by query and (optional) context. <br>
   *  If context node is null use project as a context (root node for search).
   *      
   *  @param {object}   query               - Query for search
   *  @example
   *  {
   * 
   *    "object_list"     : [ 'objid1' , 'objid2' , ... ]  // optional
   * 
   *    // parameters of webservice
   *    "filter"          : 
   *    "members"         : 
   *    "properties"      :
   *    "geometry"        :
   *    "nodes"           :
   *    "relations"       :
   *    "relations_right" :
   *  }
   * 
   *  @param {function} cb                  - Callback function that receives the result object
   *  @param {string}   [context_sid=null]  - Optional session or context identifier
   * 
   *  @returns {Promise<ApiResponse>} Since this uses a callback, the function itself resolves with no value. <br>
   *    Array<cnObject>
   * 
   *  @since
   *    1.0, 09.2025, jh
   * 
   */
  async getObjects( query, cb, context_sid = null) {
    
    let q = ''
    let c = '?'

    if (context_sid) {
      q += c + `context=$node ~eq~ '${context_sid}'`
    } else {
      q += c + `context=$node ~eq~ '${this.#_ctx.getProjectShortId()}'`
    }

    c = '&'

    let filter = ''

    if (query['object_list'] && query['object_list'].length > 0) {

      filter = "$object_id ~in~ [ "

      filter += " '" + query['object_list'][0] + "' "

      for (let i = 1; i < query['object_list'].length; i++) {
        filter += " , '" + query['object_list'][i] + "' "
      }

      filter += " ] "

    }

    if (query['filter']) {

      if (filter.length > 0) {
        filter += ' $AND '
      }

      q += c + 'filter=' + encodeURIComponent(filter + query['filter'])
      c = '&'

    } else {

      if (filter.length > 0) {
        q += c + 'filter=' + encodeURIComponent(filter)
        c = '&'
      }

    }

    if (query['members']) {
      let plist = query['members'].join(',')
      q += c + 'members=' + encodeURIComponent(plist)
      c = '&'
    }

    const url = `${this.#_ctx.getServiceUrl(Context.SERVICE.HUB)}/${this.#_ctx.getScope()}/objects${q}`;

    console.log( `GET : ${url}`)

    //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////    
    /* RestResponse */ const resp = await RestServices.makeApiCall(this.#_ctx.getToken(), RestServices.METHODS.GET, url)
    //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

    if (resp.status < 300) {

      let array = []

      for (const obj_json of resp.data) {

        let obj = new cnObject()
        obj.setFromJson(obj_json)
        
        array.push(obj)

      }

      return cb(new ApiResponse(resp.status, array))

    } else {
      
      return cb( resp )

    }

  }

  /**
   * 
   * Get path of node hierarchy from root to node.
   * 
   * @param {*} node_sid 
   * @param {*} query 
   * @param {*} cb 
   * @returns {Promise<ApiResponse>}
   */
  async getNodePath( node_sid , query , cb ) {
    
    let q = ''
    let c = '?'
    
    if (query['members']) {
      let plist = query['members'].join(',')
      q += c + 'members=' + encodeURIComponent(plist)
      c = '&'
    }

    const url = `${this.#_ctx.getServiceUrl(Context.SERVICE.HUB)}/${this.#_ctx.getScope()}/nodes/${node_sid}/path/data${q}`;

    console.log( `GET : ${url}`)

    //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////    
    /* RestResponse */ const resp = await RestServices.makeApiCall(this.#_ctx.getToken(), RestServices.METHODS.GET, url)
    //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

    if (resp.status < 300) {

      let array = []

      for (const obj_json of resp.data) {

        let obj = new cnObject()
        obj.setFromJson(obj_json)
        
        array.push(obj)

      }

      return cb(new ApiResponse(resp.status, array))

    } else {
      
      return cb( resp )

    }

  }

  /**
   * 
   * Set property values in propertylist for node (shortid) on infohub. <br>
   * 
   * 
   * @param {*} node_sid Node ShortID
   *  
   * @param {*} propertylist = [
   *      {
   *        'typeid'  : "abc##xs:string",
   *        'value'   : "123 abc",
   *        'comment' : "Gesetzt durch xyz"
   *      }
   *  ]   
   * 
   * @param {*} cb        Call Back {
   *    ApiResponse<>
   * }
   * 
   * @param {*} cmt       Comment (optional)
   * 
   * @returns {Promise<ApiResponse>} Since this uses a callback, the function itself resolves with no value. <br>
   * 
   *  @since
   *    1.0, 09.2025, jh
   * 
   */
  async setPropertyValues( node_sid , propertylist , cb ) {
    
    const url = `${this.#_ctx.getServiceUrl(Context.SERVICE.HUB)}/${this.#_ctx.getScope()}/nodes/${node_sid}/properties`;

    // const reqBody = [
    //   {
    //     'type'    : typeid,
    //     'value'   : val,
    //     'comment' : cmt        
    //   }
    // ]

    const reqBody = propertylist

    // console.log( `POST : ${url}`)

    //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////    
    /* RestResponse */ const resp = await RestServices.makeApiCall(this.#_ctx.getToken(), RestServices.METHODS.POST , url , reqBody )
    //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
    
    return cb( resp )

  }

}