@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
JavaScript
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);