UNPKG

@cute-dw/core

Version:

This TypeScript library is the main part of a more powerfull package designed for the fast WEB software development. The cornerstone of the library is the **DataStore** class, which might be useful when you need a full control of the data, but do not need

1,297 lines (1,295 loc) 565 kB
var _a; import { HttpParams } from "@angular/common/http"; import { EMPTY, BehaviorSubject, concat } from "rxjs"; import { takeUntil } from "rxjs/operators"; import { DEFAULT_CHECKBOXSTYLE_OPTIONS, DEFAULT_CONTROL_OPTIONS, DEFAULT_DDDWSTYLE_OPTIONS, DEFAULT_DDLBSTYLE_OPTIONS, DEFAULT_DDPLBSTYLE_OPTIONS, DEFAULT_EDITMASKSTYLE_OPTIONS, DEFAULT_EDITSTYLE_OPTIONS, DEFAULT_RADIOBUTTONSSTYLE_OPTIONS, DEFAULT_RICHEDITSTYLE_OPTIONS, UpdateWhereClause } from "./DataStoreOptions"; import { ResultCode } from "../util/enum/ResultCode"; import { CodeBlock } from "../util/evaluator/CodeBlock"; import { DSCodeBlock } from "./DSCodeBlock"; import { Objects } from "../util/Objects"; import { Types } from "../util/Types"; import { Strings } from "../util/Strings"; import { Ciphers } from "../util/Ciphers"; import { XmlDocs } from "../util/XmlDocs"; import { Decimal } from "../util/type/Decimal"; import { DateTime } from "../util/type/DateTime"; import { Time } from "../util/type/Time"; import { EventProducer } from "../util/rxjs/EventProducer"; import { DwDbmsErrorEvent, DwItemChangedEvent, DwRetrieveEndEvent, DwRetrieveStartEvent, DwRowChangedEvent, DwUpdateEndEvent, DwUpdateStartEvent } from "./events"; import { DataStoreService } from "./DataStoreService"; import { IllegalJsonException } from "../util/exception/IllegalJsonException"; import { TreeSet } from "../collections/TreeSet"; import { JsonRpc } from "../util/net/JsonRpc"; import { JsonRpcException } from "../util/exception/JsonRpcException"; export const ROW_ID_KEY = "$row_id$"; export const ROW_STATUS_KEY = "$row_stts$"; /** * A DataStore is a nonvisual DataWindow control. DataStores act just like DataWindow controls except that * many of the visual properties associated with DataWindow controls do not apply to DataStores. Because you can * print DataStores, **@cute-dw** provides some events and functions for DataStores that pertain to * the visual presentation of the data. * @version 1.0.0 * @author Alexander V. Strelkov <alv.strelkov@gmail.com> * @copyright © ALEXANDER STRELKOV 2022 * @license MIT * @see DataWindow */ export class DataStore { /** * @constructor */ constructor(options, http) { this._data = { primary: [], filtered: [], deleted: [] }; this._params = []; this._filter = ""; this._sort = null; //private _locale: string | undefined; // Sort this._lastFind = [null]; //private _lastEval: [string | null, DSCodeBlock?] = [null]; this._evalCache = new Map(); this._config = {}; // | null = null; this._controls = {}; this._columns = []; this._groups = []; this._sharedMainDS = null; this._sharedChilds = new Set(); this._selectedRows = new WeakSet(); this._newRowSet = new WeakSet(); this._originalsMap = new WeakMap(); // Row's object -> Object with cloned original values this._rowIndexes = new WeakMap(); // Row's object -> Row's index //private _rowIds = new Map(); //private _retrievePromise: Promise<T[]> | null = null; //private _retrieveObservable: Observable<T[]> | null = null; /** * Subscription to the changes that should trigger an update to the table's rendered rows, such * as filtering, sorting, pagination, or base data changes. */ this._dataStreamSubscription = null; /** Stream emitting rendering data to the table (depends on ordered data changes). */ this.dataStream$ = new BehaviorSubject([]); this._loading = false; this._updating = false; this._currentRow = -1; this._currentCol = -1; this._lastRowID = -1; // Row IDs counter this._getChangesTimestamp = -1; this._creationStamp = ""; // UUID of create() function this._UUID = Ciphers.uuid_v4(); // UUID of the instance // EVENTS this.createEvent$ = new EventProducer(); this.dbCancelEvent$ = new EventProducer(); this.dbErrorEvent$ = new EventProducer(); this.dwErrorEvent$ = new EventProducer(); this.httpErrorEvent$ = new EventProducer(); this.itemChangedEvent$ = new EventProducer(); this.itemErrorEvent$ = new EventProducer(); this.retrieveEndEvent$ = new EventProducer(); this.retrieveStartEvent$ = new EventProducer(); this.rowChangedEvent$ = new EventProducer(); this.updateEndEvent$ = new EventProducer(); this.updateStartEvent$ = new EventProducer(); this.setParams$ = new EventProducer(); this.onCreate = this.createEvent$.asObservable(); this.onDbCancel = this.dbCancelEvent$.asObservable(); this.onDbError = this.dbErrorEvent$.asObservable(); this.onDwError = this.dwErrorEvent$.asObservable(); this.onHttpError = this.httpErrorEvent$.asObservable(); this.onItemChanged = this.itemChangedEvent$.asObservable(); this.onItemError = this.itemErrorEvent$.asObservable(); this.onRetrieveEnd = this.retrieveEndEvent$.asObservable(); this.onRetrieveStart = this.retrieveStartEvent$.asObservable(); this.onRowChanged = this.rowChangedEvent$.asObservable(); this.onUpdateEnd = this.updateEndEvent$.asObservable(); this.onUpdateStart = this.updateStartEvent$.asObservable(); this.onSetParams = this.setParams$.asObservable(); /** * JavaScript's `typeof` operator returns `[object DataStore]` */ this[_a] = "DataStore"; this._service = new DataStoreService(this); this.setHttpService(http); this.create(options ?? {}); this._dataStreamSubscription = this.dataStream.subscribe(data => { if (data.length == 0) { this._currentRow = -1; } }); } /** * Returns initialization status */ get isInitialized() { return !Objects.isEmpty(this._config); } /** * Creates an internal controls array */ createControls() { let controlsObject = this._config.controls ?? {}; let control; this._controls = {}; if (Object.keys(controlsObject).length == 0) { // Controls were not specified in the config data // Create their from column's list let cols = this.getColumns(); for (let i = 0; i < cols.length; i++) { control = { ...DEFAULT_CONTROL_OPTIONS }; control.coltype = cols[i].type; control.width = 100; this._controls[cols[i].name] = control; } } else { for (const key in controlsObject) { this._controls[key] = Objects.fillIn({}, controlsObject[key], DEFAULT_CONTROL_OPTIONS); this._controls[key].coltype = undefined; // Manual definition is not allowed and ignored! } } let oCol; for (const key in this._controls) { oCol = this._controls[key]; if (!oCol.band) oCol.band = "detail"; if (!oCol.label) oCol.label = key; if (!oCol.header) oCol.header = oCol.label; if (!oCol.displayas) oCol.displayas = "default"; if (!oCol.editstyle) oCol.editstyle = { ...DEFAULT_EDITSTYLE_OPTIONS }; if (typeof oCol.expression === "string") oCol.expression = Strings.trimAll(oCol.expression) || "undefined"; else oCol.expression = "undefined"; if (oCol.type == "column") { if (oCol.coltype === undefined) { oCol.coltype = this.getColumnType(key); } oCol.column = this.getColumnObject(key); } else { oCol.column = null; oCol.coltype = undefined; } Object.defineProperty(oCol, "column", { writable: false }); Object.defineProperty(oCol, "coltype", { writable: false }); switch (oCol.editstyle.name) { case "edit": Objects.fillIn(oCol.editstyle, DEFAULT_EDITSTYLE_OPTIONS); break; case "editmask": Objects.fillIn(oCol.editstyle, DEFAULT_EDITMASKSTYLE_OPTIONS); break; case "ddlb": Objects.fillIn(oCol.editstyle, DEFAULT_DDLBSTYLE_OPTIONS); break; case "ddplb": Objects.fillIn(oCol.editstyle, DEFAULT_DDPLBSTYLE_OPTIONS); break; case "dddw": Objects.fillIn(oCol.editstyle, DEFAULT_DDDWSTYLE_OPTIONS); break; case "checkbox": Objects.fillIn(oCol.editstyle, DEFAULT_CHECKBOXSTYLE_OPTIONS); break; case "radiobuttons": Objects.fillIn(oCol.editstyle, DEFAULT_RADIOBUTTONSSTYLE_OPTIONS); break; case "richedit": Objects.fillIn(oCol.editstyle, DEFAULT_RICHEDITSTYLE_OPTIONS); break; } } } /** * Creates a new row object and initializes it * * @returns Empty row object */ createRowObject(rowID, withFields = false) { let oRow = {}; Object.defineProperty(oRow, ROW_ID_KEY, { value: rowID }); Object.defineProperty(oRow, ROW_STATUS_KEY, { value: undefined, writable: true }); /* Object.defineProperties(oRow, { ROW_ID_KEY: { value: rowID }, ROW_STATUS_KEY: {value: undefined, writable: true}, ROW_ISEDIT_KEY: {value: false, writable: true} }); */ if (withFields) { let colCount = this.columnCount(); let oCol; let colName; let j = -1; while (++j < colCount) { oCol = this.getColumnObject(j); colName = oCol.name; oRow[colName] = null; } } return oRow; } /** * Gets target DataStore (shared/Main) * @returns */ getMainDS() { // let primaryDS = this._sharedMainDS; // while (primaryDS && primaryDS._sharedMainDS) { // primaryDS = primaryDS._sharedMainDS; // } // return primaryDS ?? this; return this._sharedMainDS ?? this; } /** * Reports if DataWindow/DataStore updateable or not * @returns boolean */ isUpdateable() { const target = this.getMainDS(); const readonly = target._config.datawindow?.readonly || false; const updateTable = target._config.table?.updateTable || ""; return !readonly && (typeof (updateTable) === "string" && updateTable.trim().length > 0); } /** * Resets each group in the defined group list * @private */ resetGroups() { this._groups.map((g) => g.reset()); } /** * Finds a group object by its name * @private */ findGroup(name) { name = name.trim().toLowerCase(); return this._groups.find(g => g.name == name); } /** * Resets the internal map of the DataWindow row's data indexes */ resetRowIndexes() { this._rowIndexes = new WeakMap(); } /** * */ toLiteralValue(s) { let v; if (["true", "false"].includes(s)) v = Boolean(s); else if (["null", "undefined"].includes(s)) v = null; else if (Objects.isNumber(s)) v = +s; else v = s || ""; return v; } /** * Gets the value of specified control that must be a column or computed type * * @param context Data object * @param key Control's key * @returns Control's value * * @private */ getControlValue(context, key) { if (context && key && (key in this._controls)) { const control = this._controls[key] || DEFAULT_CONTROL_OPTIONS; switch (control.type) { case "column": { let val = context[key]; if (this.hasCodeTable(key)) { val = this.getDisplayValue(key, String(val)); } return val; } case "compute": { let cbk = control.$cbk; if (cbk && cbk instanceof DSCodeBlock) { if (cbk.error) { return "ERR!"; } } else { cbk = new DSCodeBlock(control.expression || "", this); //.getMainDS()); if (cbk.error) { this.dwErrorEvent$.trigger(cbk.error); return "ERR!"; } Object.defineProperty(control, "$cbk", { value: cbk, writable: true }); } return cbk.eval(context); } } } return null; } /** * Returns control object by its name or null if it was not found * @private */ getControl(key) { if (key in this._controls) { return this._controls[key]; } return null; } /** * Clears original value in the originals Map object * @param rowobj Row Object * @param column Column name or "*" for clear all values * @returns true/false * @private */ clearOriginalValue(rowobj, column) { const target = this.getMainDS(); if (target._originalsMap.has(rowobj)) { const origData = target._originalsMap.get(rowobj); if (column == "*") { return target._originalsMap.delete(rowobj); } else if (!(origData[column] === undefined)) { delete origData[column]; if (Object.keys(origData).length == 0) { target._originalsMap.delete(rowobj); } return true; } } return false; } /** * Proxify decorator * @private */ proxify(oRow) { let oProxyRow = new Proxy(oRow, { get: (target, prop, receiver) => { if (prop in target || typeof prop === "symbol") return Reflect.get(target, prop, receiver); //console.log("PropGet: " + String(prop)); return this.getControlValue(oProxyRow, prop); }, set: (target, prop, val) => { if (prop in target) { if (typeof prop === "string") { let type = null; const col = this.getColumnObject(prop); if (col) { type = col.type; } const oldVal = target[prop]; val = Types.toType(val, type || typeof (oldVal)); if (oldVal !== val) { const ds = this.getMainDS(); if (!ds._originalsMap.has(oProxyRow)) ds._originalsMap.set(oProxyRow, {}); let origData = ds._originalsMap.get(oProxyRow); if (origData[prop] === undefined) origData[prop] = oldVal; else if (origData[prop] === val) { // returns to original value delete origData[prop]; if (Object.keys(origData).length == 0) { ds._originalsMap.delete(oProxyRow); } } } } return Reflect.set(target, prop, val); } return true; // Silent ignore // } }); /* if (oRow[ROW_ID_KEY] instanceof Number) { this._rowIds.set(oRow[ROW_ID_KEY], oProxyRow); } */ return oProxyRow; } /** * Clears internal row buffers * @param clearPrimary {boolean} Clear or not Primary! buffer. Default is true * * @private */ clearBuffers(clearPrimary = true) { const target = this.getMainDS(); if (clearPrimary) { target._data.primary = []; } target._data.filtered = []; target._data.deleted = []; } /** * Fills internal primary row buffer * * @private */ setData(newData, clearBuffers = true) { if (!newData) newData = []; if (clearBuffers) { this.reset(); } const rows = newData.length; const cols = this.columnCount(); if (!(cols && rows)) { return; } let i = -1; let j; let oCol; let oRow; let colName; let buffer = this.getBuffer("Primary!"); let colArray = []; j = -1; while (++j < cols) { colArray[j] = this.getColumnObject(j); } buffer.length += rows; while (++i < rows) { oRow = this.createRowObject(newData[i][ROW_ID_KEY] || ++this._lastRowID); j = -1; while (++j < cols) { oCol = colArray[j]; colName = oCol.name; if (newData[i][colName] === undefined) oRow[colName] = null; else oRow[colName] = Types.toType(newData[i][colName], oCol.type || "string"); } buffer[i] = this.proxify(oRow); } // Start filter value applying if (Strings.isEmpty(this.filterValue)) { let startFilter = this._config.table?.startFilter ?? ""; if (typeof startFilter == "string" && !Strings.isEmpty(startFilter)) { this.filterValue = startFilter; this.applyFilterSync(); // clear filter buffer this._data.filtered = []; this.filterValue = ""; } } else { this.applyFilterSync(); } // Start sorting apply, if any if (Strings.isEmpty(this.sortValue)) { let startSort = this._config.table?.startSort ?? ""; if (typeof startSort == "string" && !Strings.isEmpty(startSort)) { this.sortValue = startSort; } } this.applySortSync(); this.resetRowIndexes(); /* // clear not primary buffers this.clearBuffers(false); this.filterValue = ""; */ if (this.rowCount() > 0) { this.setRow(0); // ?? } // post data to observers this.emitDataStream(); } /** * Gets an array of Table parameter objects * @private */ getParams() { return this._params; } /** * Gets an array of column objects * @private */ getColumns() { return this._columns; } /** * Gets the key columns of the updatable table * @returns Array of `TableColumn` objects * @private */ getKeyColumns() { const target = this.getMainDS(); const cols = target.getColumns(); let keyCols = []; cols.forEach(col => { if (col.key && this.isMainTableColumn(col)) { keyCols.push(col); } }); return keyCols; } /** * Gets database column name for column object * @private */ getColumnDbName(col) { if (col) { const dbName = col.dbname || col.name; let [name] = Strings.getLastToken(dbName, "."); name = name.trim(); return name; } return "!"; } /** * Checks the column for it belonging to the main (updatable) table * @param column `TableColumn` object * @returns true/false * @private */ isMainTableColumn(column) { if (column) { const target = this.getMainDS(); const tbl = target._config.table; if (tbl) { const tAlias = Objects.coalesce(tbl.updateAlias, tbl.updateTable, "").trim(); let cAlias; if (column.dbname && column.dbname.indexOf(".") > 0) { [cAlias] = Strings.getToken(column.dbname || "", "."); cAlias = (cAlias.trim()) || tAlias; } else { cAlias = tAlias; } return (cAlias.toLowerCase() == tAlias.toLowerCase()); } } return false; } /** * Checks the column if it updatable now * @param column `TableColumn` object * @returns true/false * @private */ isColumnUpdatable(column) { if (typeof column == "string" || typeof column == "number") { const target = this.getMainDS(); column = target.getColumnObject(column); } if (column && (column.updatable ?? true)) { return this.isMainTableColumn(column); } return false; } /** * Gets auto synchronized column names * @param includeIdentity Include `identity` column or not * @returns Array of the column names * @private */ getAutoSyncColumns(includeIdentity = true) { const cols = this.getColumns(); const colLen = cols.length; let colNames = []; const returningAllFields = this._config.table?.returningAllFields || false; if (returningAllFields) { colNames.push("*"); } else { for (let i = 0; i < colLen; i++) { if ((includeIdentity && cols[i].identity) || cols[i].autosync) { colNames.push(cols[i].name); } } } return colNames; } /** * Gets a column object by its name/index or `undefined` if the object was not found * @private */ getColumnObject(index) { const cols = this.getColumns(); if (cols.length > 0) { if (typeof index === "string") { index = index.trim().toLowerCase(); index = cols.findIndex(col => col.name == index); } if (index >= 0 && index < cols.length) { return cols[index]; } } return undefined; } /** * Gets a row buffer array of the specified type * @private */ getBuffer(bufferType) { let buffer; const target = this.getMainDS(); switch (bufferType) { case "Filter!": buffer = target._data.filtered; break; case "Delete!": buffer = target._data.deleted; break; default: buffer = target._data.primary; break; } return buffer; } /** * Posts data of `Primary!` buffer to primary and secondary DataStore/DataWindow observers * @private */ emitDataStream() { const target = this.getMainDS(); this.dataStream$.next(target.data); this._sharedChilds.forEach(child => Object.is(child, target) ? null : child.emitDataStream()); } /** * Emulates and posts an empty data array to observers. * This method was exposed to solve the screen gluch when generating group headers in the DataWindow (in @angular/material's table) * * @private */ emitEmptyDataStream() { const target = this.getMainDS(); target.dataStream$.next([]); } /** * @private */ createRpcRequestObject(action, table, params, id) { let methodFormat = this._config.table?.rpcMethodFormat ?? 0; let methodName = `${table}.${action}`; switch (methodFormat) { case 1: methodName = `${table}_${action}`; break; case 2: case 3: /* Updating actions will be transformed later to single updating request */ if (Strings.right(action, 3) == "Row") { action = Strings.left(action, action.length - 3); } methodName = `${action}Row`; // --> insertRow, deleteRow, updateRow, reselectRow params["table"] = table; break; } return JsonRpc.createRequest(methodName, params, id); //{ jsonrpc: JSONRPC_VERSION, method: methodName, params, id }; } /** * Gets an array of columns for the `WHERE` clause * @param obj Row object * @param forDelete `DELETE` statement/method will be used * @returns Array of the table columns that must participate in the `WHERE` clause * @private */ getWhereClauseColumns(obj, forDelete = false) { const target = this.getMainDS(); const tableCols = target.getColumns(); const colLen = tableCols.length; let whereClause = this._config.table?.updateWhere ?? UpdateWhereClause.KeyCols; // Update method let cols = []; let i; const originals = target._originalsMap.get(obj); if (originals === undefined && forDelete && whereClause == UpdateWhereClause.KeyAndModifiedCols) { // Row is not modified, do more fast db search whereClause = UpdateWhereClause.KeyCols; } switch (whereClause) { case UpdateWhereClause.KeyCols: // Key columns only (risk of overwriting another user's changes, but fast). for (i = 0; i < colLen; i++) { if (tableCols[i].key && this.isMainTableColumn(tableCols[i])) { cols.push(tableCols[i]); } } break; case UpdateWhereClause.KeyAndUpdatableCols: // Key columns and all updatable columns (risk of preventing valid updates; slow because SELECT statement is longer) for (i = 0; i < colLen; i++) { if (this.isMainTableColumn(tableCols[i]) && (tableCols[i].key || ((tableCols[i].updatable ?? true) && (tableCols[i].updatewhereclause ?? true)))) { cols.push(tableCols[i]); } } break; case UpdateWhereClause.KeyAndModifiedCols: // Key and modified columns (allows more valid updates than 1 and is faster, but not as fast as 0) { if (obj) { for (i = 0; i < colLen; i++) { if (this.isMainTableColumn(tableCols[i]) && (tableCols[i].key || ((tableCols[i].updatable ?? true) && (tableCols[i].updatewhereclause ?? true) && (originals ? !Objects.isUndefined(originals[tableCols[i].name]) : false)))) { cols.push(tableCols[i]); } } } } break; } return cols; } /** * Save original value(s) * @private */ makeOriginals(obj, oCol) { const target = this.getMainDS(); if (!target._originalsMap.has(obj)) target._originalsMap.set(obj, {}); const originals = target._originalsMap.get(obj); let colName; if (oCol) { colName = oCol.name; if (originals[colName] === undefined) { originals[colName] = obj[colName]; } } else { const colCount = this.columnCount(); for (let i = 0; i < colCount; i++) { colName = this.columnName(i); if (originals[colName] === undefined) originals[colName] = obj[colName]; } } } /* PROPERTIES */ //get retrievePromise(): Promise<T[]> | null { return this._retrievePromise; } //set retrievePromise(value: Promise<T[]> | null) { this._retrievePromise = value; } /** * Gets current loading status * @readonly * @since 0.0.0 * @see {@link retrieve} */ get loading() { const target = this.getMainDS(); return target._loading; } /** * Checks for any updates that may be pending * @readonly * @since 0.0.0 * @see {@link update} * @see {@link updating} */ get updatesPending() { if (this.isUpdateable()) { return this.deletedCount() > 0 || this.getNextModified(-1, "Primary!") >= 0 || this.getNextModified(-1, "Filter!") >= 0; } return false; } /** * Gets current updating status * @readonly * @since 0.0.0 * @see {@link update} */ get updating() { const target = this.getMainDS(); return target._updating; } /** * Gets the data from the primary buffer * @readonly * @since 0.0.0 */ get data() { return this.getBuffer("Primary!"); } /** * Gets the data stream as the Observable object * @readonly * @since 0.0.0 */ get dataStream() { const target = this.getMainDS(); return target.dataStream$.asObservable(); } /** * Returns internal http service object * @since 0.0.0 */ get httpService() { return this._httpService; } /** * Gets the current filter value * @since 0.0.0 * @see {@link applyFilter} */ get filterValue() { const target = this.getMainDS(); return target._filter; } /** * Sets filter expression * @since 0.0.0 * @see {@link applyFilter} */ set filterValue(express) { const target = this.getMainDS(); target._filter = Strings.trimAll(express) ?? ""; } /** * Current sort value * @since 0.0.0 * @see {@link applySort} */ get sortValue() { const target = this.getMainDS(); return target._sort; } /** * Specifies sort criteria for a DataWindow control or DataStore. * A DataStore object can have sort criteria specified as part of its definition. * Set `sortValue` overrides the definition, providing new sort criteria for the DataStore. * However, it does not actually sort the rows. Call the {@link applySort} method to perform the actual sorting. * @example * this.sortValue = "emp_name asc, div_code desc"; * this.sortValue = "upper(client_address)"; // ascending by default * this.sortValue = "article_title \t left(String(post_date,'yyyymmdd'),4) desc"; * this.applySort(); * @since 0.0.0 */ set sortValue(express) { const target = this.getMainDS(); target._sort = (express) ? Objects.nullIf(Strings.trimAll(express, ' '), "") : null; } /** * Gets copy of configured Controls as a plain objects' array * @readonly * @since 0.0.0 */ get controls() { const arr = []; let i = -1; for (const key in this._controls) { arr[++i] = Objects.deepFillIn({ name: key }, this._controls[key]); } return arr; } /** * The number of milliseconds between the internal timer events. Default is 0 (disabled) */ get timerInterval() { const ms = this._config.datawindow?.timer ?? 0; return Math.max(ms, 0); } /** * The unique UUID value of the last *create()* function invocation */ get creationStamp() { return this._creationStamp; } /* EVENTS */ /* METHODS */ /** * Creates a DataWindow object using {@link DataStoreOptions} object. It fully substitutes the current internal structure of a DataStore object. * * @param options An object of `DataStoreOptions` class * @returns 1 if it succeeds and -1 if an error occurs. * @since 0.0.0 */ create(options) { if (options) { this.shareDataOff(); this._config = Objects.deepClone(options) || {}; this._config.controls = this._config.controls || {}; this._params = this._config.table?.parameters || []; let cols = this._config.table?.columns || {}; if (Objects.isArray(cols)) { this._columns = cols; } else if (Objects.isObject(cols)) { this._columns = []; Object.keys(cols).forEach(key => this._columns.push({ name: key, ...cols[key] })); } else { this._columns = []; } this._groups = []; let garr = this._config.table?.groups || []; for (let i = 0; i < garr.length; i++) { this._groups.push(new DSRowGroup(this, garr[i], (i > 0) ? this._groups[i - 1] : null)); } this.createControls(); this.reset(); this._creationStamp = Ciphers.uuid_v4(); this.createEvent$.trigger(); return ResultCode.SUCCESS; } return ResultCode.FAILURE; } /** * Obtains the copy of the configured Controls as a map (plain JavaScript) object or an array of the `DwObject` objects * * @param band Optional. DataWindow control's band type. Default is `all`. * @param asArray Optional. Set to _true_ if you want to return an array of the `DwObject` objects, else the method returns a map object. Default is _false_. * @returns Array of `DwObject` objects or the plain JavaScript object as a key/value dictionary * @since 0.0.0 */ getControls(band = "all", asArray = false) { if (asArray) { if (band == "all") return [...this.controls]; return this.controls.filter(col => col.band == band); } let controls = {}; for (const key in this._controls) { if (band == "all" || this._controls[key].band == band) { controls[key] = { ...this._controls[key] }; } } return controls; } /** * Sets HttpService object for retrieve/update data on remote server * @param httpSvc HttpService object (optional) * @since 0.0.0 */ setHttpService(httpSvc) { if (httpSvc) { this._httpService = httpSvc; return ResultCode.SUCCESS; } return ResultCode.FAILURE; } /** * Gets row count in the primary buffer * @returns Row count * @since 0.0.0 * @see {@link deletedCount} * @see {@link filteredCount} */ rowCount() { return this.data.length; } /** * Gets row count in the deleted rows buffer * @returns Row count * @since 0.0.0 * @see {@link rowCount} * @see {@link filteredCount} * @see {@link modifiedCount} */ deletedCount() { return this.getBuffer("Delete!").length; } /** * Reports the number of rows that have been modified but not updated in a DataWindow or DataStore. * @returns {number} The number of rows that have been modified in the primary and filrer buffer. * Returns 0 if no rows have been modified or if all modified rows have been updated in the database table. * @since 0.0.0 * @see {@link rowCount} * @see {@link deletedCount} */ modifiedCount() { let nCount = 0; let bufferType; let row; bufferType = "Primary!"; row = this.getNextModified(-1, bufferType); while (row >= 0) { nCount++; row = this.getNextModified(row, bufferType); } bufferType = "Filter!"; row = this.getNextModified(-1, bufferType); while (row >= 0) { nCount++; row = this.getNextModified(row, bufferType); } /* let buffer: T[]; let rows; let i = -1; let status: ItemStatus | undefined; let bufferType: BufferType = "Primary!" buffer = this.getBuffer(bufferType); rows = buffer.length; while (++i < rows) { status = this.getItemStatus(i, null, bufferType); switch (status) { case "NewModified!": case "DataModified!": nCount++; break; } } i = -1; bufferType = "Filter!" buffer = this.getBuffer(bufferType); rows = buffer.length; while (++i < rows) { status = this.getItemStatus(i, null, bufferType); switch (status) { case "NewModified!": case "DataModified!": nCount++; break; } } */ return nCount; } /** * Gets row count in the filtered rows buffer * @returns Row count * @since 0.0.0 * @see {@link deletedCount} * @see {@link rowCount} */ filteredCount() { return this.getBuffer("Filter!").length; } /** * Gets group count in the DataWindow/DataStore object * @since 0.0.0 * @returns Group count */ groupCount() { const target = this.getMainDS(); return target._groups.length; } /** * Applies the contents of the DataWindow's edit control to the current item in the buffer of a DataWindow control or DataStore. * The data in the edit control must pass the validation rule for the column before it can be stored in the item. * @returns Returns 1 if it succeeds and -1 if it fails (for example, the data did not pass validation). * @since 0.0.0 */ acceptText() { return ResultCode.SUCCESS; } /** * Reports the values of properties of a DataWindow object and controls within the DataWindow object. * Each column and graphic control in the DataWindow has a set of properties. You specify one or more * properties as a string, and `describe` returns the values of the properties. * @param propertylist A string list (array) of properties or Evaluate functions * @returns * Returns array of values that includes a primitive value for each property or Evaluate function. * If the property list contains only one item, `describe` returns single value. * `Describe` returns an `undefined` value if there is no value for a property. * `Describe` returns `null` for property if its value is not a primitive. * @since 0.0.0 * @see {@link modify} */ describe(...propertylist) { if (this.isInitialized && propertylist) { const res = new Array(propertylist.length); if (this._config) { let obj; let prop; for (let i = 0; i < propertylist.length; i++) { if (propertylist[i] != null) { obj = this._config; prop = propertylist[i].trim(); if (prop.toLowerCase().startsWith("column.")) { obj = this._config.table?.columns; prop = prop.substring(7); } res[i] = Objects.getValue(obj, prop); if (!(Objects.isPrimitive(res[i]) || Objects.isArray(res[i]))) { res[i] = null; } } else { res[i] = undefined; } } } return res.length == 1 ? res[0] : res; } return undefined; } /** * Modifies a DataWindow object by applying specifications, given as a list of instructions, that * change the DataWindow object's definition. * @param propertylist A string array whose items ara the specifications for the modification. * @returns Returns the empty string ("") if it succeeds and an error message if an error occurs. * @since 0.0.0 * @see {@link describe} */ modify(...propertylist) { let cbk = null; if (propertylist) { let eqPos; let path; let expr; let val; for (let i = 0; i < propertylist.length; i++) { eqPos = propertylist[i].indexOf('='); if (eqPos > 0) { path = propertylist[i].substring(0, eqPos).trim(); expr = propertylist[i].substring(eqPos + 1).trim(); if (Objects.hasValue(this._config, path)) { cbk = new CodeBlock(expr); if (cbk.error) { return cbk.error; } else { try { val = cbk.eval({}); } catch (err) { return (err.message); } } Objects.setValue(this._config, path, val); } else { return `Invalid path: ${path}`; } } } } return ""; } /** * Gets cloned value of the current config options * @returns DataStoreOptions * @since 0.0.0 */ getOptions() { return Objects.deepClone(this._config); } /** * Searches for the next break for the specified group. A group break occurs when the value in the column for the group changes. * `findGroupChange` reports the row that begins the next section. * @description * If the starting row begins a new section at the specified level, then that row is the one returned. * To continue searching for subsequent breaks, increment the starting row so that the search resumes with * the second row in the group. * @param row A value identifying the row at which you want to begin searching for the group break. * @param level The number of the group for which you are searching. Groups are numbered in the order in which you defined them. * The return value observes these rules based on the value of row. If the starting row is: * · _The first row in a group_, then `findGroupChange` returns the starting row number * · _A row within a group_, other than the last group, then `findGroupChange` returns the row number of the first row of the next group * · _A row in the last group_, other than the first row of the last group, then `findGroupChange` returns -1 * @returns * Returns the number of the row whose group column has a new value, meaning that it begins a new group. * Returns -1 if the value in the group column did not change and -100 if an error occurs. * @since 0.0.0 */ findGroupChange(row, level) { if (row >= 0 && row < this.rowCount()) { if (level >= 0 && level < this.groupCount()) { const target = this.getMainDS(); return target._groups[level].findChange(row); } } return -100; } /** * Recalculates the breaks in the grouping levels in a DataStore/DataWindow. * * @description * Use `groupCalc` to force the DataWindow object to recalculate the breaks in the grouping levels * after you have added or modified rows in a DataWindow. `groupCalc` does not sort the data before it * recalculates the breaks. Therefore, unless you populated the DataWindow in a sorted order, * call the {@link applySort} method to sort the data before you call `groupCalc`. * @returns Returns 1 if it succeeds and -1 if an error occurs. * @since 0.0.0 */ groupCalc() { const target = this.getMainDS(); const groupCount = target.groupCount(); if (groupCount == 0) return ResultCode.NOTHING; // Step 1. Check for errors for (let i = 0; i < groupCount; i++) { if (target._groups[i].parseError != null) { return ResultCode.FAILURE; } } // Step 2. Resets target.resetGroups(); const nRows = this.rowCount(); let val = ""; let row = -1; while (++row < nRows) { val = ""; for (let g = 0; g < groupCount; g++) { val = target._groups[g].calc(row, val); } } return ResultCode.SUCCESS; } /** * Gets array of DataStore groups * @returns Readonly array of DSRowGroup structures * @since 0.0.0 */ getGroups() { const target = this.getMainDS(); return [...target._groups]; } /** * Reports the number of the current row in a DataWindow control or DataStore object. * @returns Returns the number of the current row in dwcontrol. Returns -1 if no row is current * @since 0.0.0 */ getRow() { return this.data.length > 0 ? this._currentRow : -1; } /** * Sets the current row in a DataWindow control or DataStore. * @param row The row you want to make current * @returns Returns 1 if it succeeds, 0 if the row is the current row indeed, and -1 if an error occurs. * @since 0.0.0 */ setRow(row) { if (row >= 0 && row < this.rowCount()) { if (this._currentRow == row) { return ResultCode.NOTHING; } this._currentRow = row; return ResultCode.SUCCESS; } return ResultCode.FAILURE; } /** * Gets the row object of specified row's number * @param {number} row Optional row number. Default is the current row number in DataWindow/DataStore * @returns Row object or _null_ if the number of the `row` is invalid * @since 0.0.0 */ getRowObject(row) { row = row ?? this.getRow(); if (row >= 0 && row < this.rowCount()) { return this.data[row]; } return null; } /** * Gets an index of the row object in the `Primary!` buffer * @param oRow {any} Row object * @returns {number} Index (row number) value if the object was found in the DataStore's main buffer and * -1 if the object does not exist in the buffer * @since 0.0.0 */ indexOf(oRow) { if (oRow) { const target = this.getMainDS(); let index = target._rowIndexes.get(oRow); if (index === undefined) { index = target.data.indexOf(oRow); if (index >= 0) { target._rowIndexes.set(oRow, index); } } return index; } return -1; } /** * Reports the next row and column that is required and contains a null value. * The method arguments that specify where to start searching also store the results of the search. * You can speed up the search by specifying that `findRequired` check only inserted and modified rows. * * @param bufferType A value indicating the DataWindow buffer you want to search for required columns. Valid buffers are: Primary! and Filter! * @param context (In/Out) Reference to an object of type {row:number, col:number, colname:string} * @param updateonly A value indicating whether you want to validate all rows and columns or only rows that have been inserted or modified * @returns Returns **1** if `findRequired` successfully checked the rows and **-1** if an error occurs * @since 0.0.0 */ findRequired(bufferType, context, updateonly) { if (context.row == null || (bufferType == "Delete!")) return ResultCode.FAILURE; if (context.col == null) return ResultCode.FAILURE; const buffer = this.getBuffer(bufferType);