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
  #_classname = null

  /**
   *
   *
   * @since 08.2025, jh
   *
   */
  constructor() {
    if( this.constructor.className ) {
      this.#_classname = this.constructor.className
    } else {
      this.#_classname = "cnObject"
    }

  }

  get className() {
    return this.#_classname;
  }

  /**
   *  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 + ' '
    }

    console.log( `${prefix}type: [${this,this.#_classname}]`)

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

  }

  /**
   * standard function used by JSON.stringify(). This way JSON.stringify gives us all the info and properties of the object.
   * @returns json representation of this object
   */
  toJSON(){
    return this.getAsJson();
  }

  /**
   *  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;
  }


  get info() {
    return this.get('info', {});
  }

  get properties() {
    return this.get('properties', {});
  }

/**
 *
 *  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]

    return v

  }

  /**
   *
   * 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 from infohub. <br>
   *  If context node is null the current project is used as context (root node for search). <br>
   *  If object does not exist and context_sid and object_data is given, a new object is created 
   *  by using 'object_data'.
   *
   *  *  Example:
   * ```js
      let container_data = {
          "info": {
              "object_id": "FTW",
              "object_name": "11-FERTIGTEILWERKE",
              "object_type": "res_container##FTW"
          }
      }
                
      Infohub.ObjectServices.getObject("FTW", async (ret_obj) => {

          ...

      }, null, null, null, container_data)

   * ```
  * 
   *  @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 - Optional session or context identifier
   *  @param {String[]|null} members - Optional members to retrieve (info, properties, properties_comments, geometry, etc.), if null info only is retrieved
   *  @param {String[]|null} properties - Optional properties to retrieve, if null all properties are retrieved
   *  @param {String|null}  context_sid Search in this context (root) object. If null use current project. Create a new object with this parent if object 
   *                                    could not be found.
   *  @param {Object|null}  object_data If object could not be found use this object data to create new object
   *  @returns {Promise<ApiResponse>} Since this uses a callback, the function itself resolves with no value
   *
   *  @since
   *    1.0, 09.2025, jh. update 07.12.2025, ar
   *
   */
  async getObject( object_id , cb , members = null, properties = null, context_sid = null , object_data = null ) {

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

    try {

      let q = ''

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

      q += '&filter=' + encodeURIComponent(`$object_id ~eq~ '${object_id}'`)

      if (members) {
        let mstr = members.join(',')
        q += '&members=' + encodeURIComponent(mstr)
      }

      if (properties) {
        let pstr = properties.join(',')
        q += '&properties=' + encodeURIComponent(pstr)
      }


      //
      // RESOLVE MEMBERS

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

      console.log( `target: [${url}]` )

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

      if ( resp.getStatus() < 300 ) {

        let data = resp.getData()

        console.log( `found ${data.length} objects`)

        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 if (data.length == 0) {

          if (context_sid && object_data) {

            // if object does not exist and object_data is given, create object as child of context_sid ...

            console.log(`create object ${object_id} ... `)

            this.createObject(object_data, context_sid, (ret) => {
              return cb(ret)
            })

          } else {
            return cb( new ApiResponse( 404 , null , `objects not found with given ID = [${object_id}].`) )
          }

        } else {

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

        }

      } else {

        if ( context_sid && object_data ) {


          // if object does not exist and object_data is given, create object as child of context_sid ...

          console.log( `create object ${object_id} ... `)

          this.createObject( object_data, context_sid , (ret) => {
            return cb(ret)
          })

        } 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) {

    console.log('### <cnObjectServices.createObject()>')
    // /{scope}/nodes/{node}

    console.log( `create object: parent = ${parent_sid}`)
    console.log( JSON.stringify(object_data) )

    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)

    }
    
  }

  /**
   *
   *  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 context node for search, if null the context for search is set to ptoject shortid.
   *
   *  @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 )

    }

  }
 
  /**
   *
   * 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 )

  }  

}