Source

infohub/PropertySchema.js

// $Id$
// @ts-check

'use strict';

import { RestServices } from '../tools/RestServices.js';
import { ApiResponse } from '../tools/ApiResponse.js';
import { Context } from './Context.js';
import { Func } from '../tools/Func.js';
import { CatalogueServices } from './Catalogue.js'
import { cnObjectServices } from '../gom/cnObject.js'
import { NodeServices } from '../infohub/Node.js'


/**
 *  
 *  @category COLNEO infohub
 * 
 *  @classdesc
 *  An object of class PropertySchema' consists of metadata, an info block and the property schema itself.
 *  
 *  ps = {
 *      'info'      : 
 *      'meta'      :
 *      'schema'    :
 *  }
 */
export class PropertySchema {


    #_jsondata = null
    #_saved_schema = null

    constructor() {
    }

    getId() {
        return this.#_jsondata?.info?.object_id
    }

    /**
     * get name
     */
    getName() {
        return this.#_jsondata?.info?.object_name
    }

    getShortId() {
        return this.#_jsondata?.info?.shortid
    }

    getInfo() {
        return this.#_jsondata?.info
    }

    getMeta() {
        return this.#_jsondata?.meta
    }

    getSchema() {
        return this.#_jsondata?.schema
    }

    /**
     * 
     */
    clear() {
        console.log(`PropertySchema.clear() `)
        this.#_jsondata = null
    }

    /**
     * 
     * @param {*} grpid 
     * @returns {Object|null}
     */
    findGroup(grpid) {
        let s = this.getSchema()
        if (s) {
            for (let i in s.groups) {
                let g = s.groups[i]
                if (g.id == grpid) return g
            }
            return null
        }
    }

    /**
     * 
     * @param {*} typeid 
     * @returns {Object|null}
     */
    findPropertyType(typeid) {

        let pt = null

        const s = this.getSchema();

        if (s && s.propertytypes) {

            for (const i in s.propertytypes) {
                if (i.startsWith('$')) continue
                let p = s.propertytypes[i]
                let t = p.name + '##' + p.datatype
                if (t == typeid) {
                    pt = p
                    break
                }
            }
        }

        return pt

    }

    /**
     * 
     * Set schema by JSON object
     * 
     * @param {Object} json_object      
     * @returns {void}
     */
    setByJson(json_object) {

        // console.log("SET SCHEMA:")
        // console.log(JSON.stringify(json_object))

        this.#_jsondata = json_object

        // if (json_object.hasOwnProperty('schema')) {
        //     this.schema = json_object.schema
        // }

    }

    /**
     * 
     * @returns {Object|null}
     */
    getAsJson() {
        return this.#_jsondata
    }

    /**
     *  Get property types from key 'propertytypes'
     *  as list of 
     *  { 
     *      'property_type' : '~eq~ ...typeid... ',
     *      'value'         : '~notnull'     
     *  }
     *  to pass it as a parameter to endpoint which creates a queryid.
     * 
     */
    getPropertytypeListForQuery() {

        let proplist = []

        const pt = this.#_jsondata?.schema?.propertytypes

        if (pt) {
            for (let idx in pt) {

                if (idx.startsWith("$")) continue

                let prop = pt[idx]

                proplist.push({
                    'property_type': " ~eq~ '" + prop.name + "##" + prop.datatype + "' ",
                    'value': ' ~notnull~ '
                })

            }
        }

        return proplist

    }

    /**
     *  Save current schema internally. 
     *  The last saved schema can be restored by restoreSchemaState()
     * 
     */
    saveSchemaState() {
        this.#_saved_schema = Func.clone( this.getSchema() )
    }
    
    restoreSchemaState() {
        if ( this.#_jsondata?.schema ) {
            this.#_jsondata.schema = this.#_saved_schema
            this.#_saved_schema = null
        }        
    }

    /**
     *  Update property members in schema taking dependencies into account. <br>
     *  The current state is saved and can be restored by restoreSchemaState().
     *     
     *  @param {function}   cb
     *  @param {Object}             node    Optional node with properties. By default the values in property list are used.
     *  @returns {Object}                   updated schema as JSON
     */
    updateSchemaByDependencies( cb = null , node = null ) {

        console.log( `### [${this.getName()}] <updateSchemaByDependencies()> `)

        // if( node ) {
        //     console.log("UpdateSchema / GOT NODE")
        //     console.log( JSON.stringify(node) )
        // }

        // console.log( "HANDLE DEPENDENCIES ... ")
        // console.log( JSON.stringify(dependencies) )

        this.saveSchemaState()

        // Object.assign( {} , propertyschema ) // new Object(propertyschema)

        // console.log("NODE ++++:::::")
        // console.log( node )
      
        let schema = this.getSchema()

        if (node != null) {        

            for (const pid in schema.propertytypes) {

                // console.log("NODE :::::")
                // console.log( JSON.stringify(node) )

                if (pid === '$order') continue

                const p = schema.propertytypes[pid]
                const t = p.name + '##' + p.datatype
                const v = node.properties[t] || './.'
                p.displayvalue = v

            }
        }

        const dependencies = schema.dependencies

        if (dependencies) {
            for (const dep of dependencies) {
                this._handle_dep(dep)
            }
        }


        return cb ? cb(schema) : schema

    }

    _handle_dep(dep) {

        let deplist = dep.deplist

        const prop = this.getSchema().propertytypes[dep.$ref]

        // console.log("REF..........: " + dep.$ref)
        // console.log("PROP.........: " + prop.name + "##" + prop.datatype)
        // console.log("VALUES........: " + prop.values)
        // console.log("DISPLAY VALUE: " + prop.displayvalue)

        function find__(deplist, displayvalue, prop) {

            for (const k of deplist) {

                // console.log("check : = " + displayvalue )
                // console.log( JSON.stringify(k) )

                // if ( k.value == displayvalue) {
                if (Func.isEqual(k.value, displayvalue, prop)) {
                    return k
                }

            }
            return null
        }

        // const depval = deplist.find( e => e.value == prop.displayvalue)
        const depval = find__(deplist, prop?.displayvalue, prop)

        if (!depval) {
            // console.log(prop.displayvalue + " not found in deplist:")
            // deplist.forEach(dep => console.log(dep.value))
            return
        }

        if (!Array.isArray(depval.dependencies)) { // also checks for existence
            return
        }

        for (const dependency of depval.dependencies) {

            // try to get propertytype to dependency refid
            let prop = this.getSchema().propertytypes[dependency.$ref]

            if (prop != null) {

                // handle property overrides
                prop = this._overrideMembers(dependency.overrides, prop)

            } else {
                // try to get group to dependency refid
                let group = this.getSchema().groups[dependency.$ref]

                // still nothing found
                if (group == null) {
                    console.log("ERROR, property type id or group id not found: " + dependency.$ref)
                    continue
                }

                // handle group overrides
                group = this._overrideMembers(dependency.overrides, group)
                // this.handle_dep(propertyschema_with_dependencies, dependency)

            }

            this._handle_dep(dependency)

        }
    }

    _overrideMembers(overrides, obj) {

        if (!overrides) return

        // copy overridden property type key/values
        for (const key in overrides) {

            let newval = overrides[key]

            if (key === 'values') {

                // check list / single value / regex / arbitrary

                if (!Array.isArray(newval)) {
                    newval = newval.trim()
                    if (newval != '' && newval != '*' && !newval.startsWith('/')) {
                        newval = new Array(newval)
                    }
                }
            }

            obj[key] = newval

        }

        if (overrides.hasOwnProperty('values') && !overrides.hasOwnProperty('displayvalues')) {
            delete obj['displayvalues']
        }

        return obj

    }

}

/**
 *  @category COLNEO infohub
 * 
 *  @classdesc
 *      Service class for Property Schemas
 * 
 */
export class PropertySchemaServices {
    /** @type {Context} */
    #_ctx = new Context();


    /**
    * @since 1.0, 12.2025, jh
    * @param {Context} ctx - Context instance
    *
    */
    constructor(ctx) {
        this.#_ctx = ctx;
    }


    /**
     * 
     * Get schema from standard catalogue in scope: ID = 'cn_properties'
     * 
     * @param {*} name Schema name
     * @param {*} cb Promise<ApiResponse>
     * @returns {Promise<void>}
     */
    async getSchemaByName(name, cb) {

        console.log(`### PropertySchema.js <getSchemaByName( ${name} )>`)

        const catsrv = new CatalogueServices(this.#_ctx)

        catsrv.getCatalogue( 'cn_properties', (ret_cat) => {

            if (ret_cat.status < 300) {

                const catsid = ret_cat.data.getShortId()

                // console.log(`CAT SID : ${catsid}`)

                const objsrv = new cnObjectServices(this.#_ctx)
                const ndsrv = new NodeServices(this.#_ctx)

                const query = {
                    "filter": `$object_type ~eq~ 'doc_json' $AND $object_name ~eq~ '${name}' `
                }

                objsrv.getObjects(query, (ret_obj) => {

                    // console.log("SCHEMA LIST:")
                    // console.log(ret_obj.dump('SCHEMALIST'))

                    if (ret_obj.data.length == 1) {

                        let id = ret_obj.data[0].info.object_id
                        let node_sid = ret_obj.data[0].info.shortid

                        const pschema = new PropertySchema()
                        // console.log(`ID = ${id}`)

                        ndsrv.getNodeData(node_sid, { members: ['info'] }, (ret_nodedata) => {

                            // console.log("RET NODE")
                            // console.log(ret_nodedata.dump('RET NODE'))
                            // console.log(JSON.stringify(ret_nodedata.data))

                            ndsrv.getDocument(node_sid, { as_blob: false }, (ret) => {

                                // console.log("SCHEMA JSON:")
                                // console.log(ret.dump("S"))
                                // console.log( JSON.stringify(ret) )

                                let ps = {
                                    'info': ret_nodedata.data.info,
                                    'schema': ret.data.doc.schema,
                                    'meta': ret.data.doc.meta
                                }

                                pschema.setByJson(ps)
                                return cb(new ApiResponse(200, pschema))

                            })
                        })

                    } else {

                        return cb(new ApiResponse(404, null, 'name not unique'))

                    }

                }, catsid)

            } else {

                // return cb(new ApiResponse( ret_cat.status, null ))
                return cb(ret_cat)

            }
        })

    }

    /**
     * 
     *  Load schema from infohub
     * 
     *  @param {string} shortid Short id of schema          
     *  @param {*}      cb
     * 
     */
    async getSchemaByShortId(shortid, cb) {

        console.log("### PropertySchema.js <getSchema(" + shortid + ")>")

        const nd_srv = new NodeServices(this.#_ctx)

        nd_srv.getDocument(shortid, { as_blob: false }, (ret) => {

            if (ret.status < 300) {

                console.log("SCHEMA JSON:")
                console.log(JSON.stringify(ret))

                const pschema = new PropertySchema()
                pschema.setByJson(ret)

                return cb(new ApiResponse(200, pschema))

            } else {
                return cb(ret)
            }

        })

        // const pschema = new PropertySchema(  Func.createId() )



    }

}