diff --git a/Guides/AboutUnistack.md b/Guides/AboutUnistack.md new file mode 100644 index 00000000..a4a4eaa2 --- /dev/null +++ b/Guides/AboutUnistack.md @@ -0,0 +1,31 @@ +#NFX/UNISTACK Overview + +##Purpose +NFX provides a **single library** for any developer to create a complex/possibly distributed/large-scale +server system. It MAY be a web-related system, but does not have to be (i.e. database engine). + +Most features of NFX **have been used in production** for over a year, base features over 8 years. + +NFX is the first (that we are aware of) practical implementation +of **UNISTACK software development methodology/process**, where all tiers of code/system/team/business are +interfacing via the same unified protocol. Simply put - you achieve a great deal of efficiency by reducing numbers +of redundant standards that your code/team members have to support. The effect of **intellectual property compression** +is promoted by UNISTACK, and becomes self-evident after the great reduction (or even complete absence) of +typical code/meetings/problems/delays during project execution is observed. + +Another important aspect is **platform transparency**. + +NFX is **not a typical .NET** library as it **avoids all of the major Microsoft technologies** which are platform-specific, bloated with legacy support and patents. +NFX does not use/need: ASP.net, WCF, ActiveDirectory*, EntityFramework, MVC, Razor, EntLib, COM etc... + +NOTE: **We are not criticizing anyone** (Microsoft or any 3rd parties), we are just sharing our view under the different angle. We understand that our approach is not applicable to much of the existing code or corporate-regulated large companies. + +##NFX Features +NFX is a UNISTACK library. As such, it has a very broad horizon of covered features. This is because in NFX +everything is re-using as much as possible from existing code base. + +Practical example: + logging, glue, web server (and all other components) use NFX.Environment.Configuration, instead of relying + on log4net, nLog, EntLib, which all use different configuration mechanisms, in NFX all components are configured + in a unified way, consequently **one does not need to remember** "how to declare a variable in config file for A + logger vs B logger". diff --git a/Guides/AppModel/README.md b/Guides/AppModel/README.md new file mode 100644 index 00000000..356be1fe --- /dev/null +++ b/Guides/AppModel/README.md @@ -0,0 +1,2 @@ +# NFX Application Model +Application Model diff --git a/Guides/Config/README.md b/Guides/Config/README.md new file mode 100644 index 00000000..b2204add --- /dev/null +++ b/Guides/Config/README.md @@ -0,0 +1,17 @@ +# NFX Environment Configuration +Configuration for NFX components + +Formats: +XML, Laconic + +Features: + +* Binding config Values +* Factory Utils/Dependency Injection +* Navigation +* Variables +* Overrides/Merges +* Scripting +* Behaviours +* Includes + diff --git a/Guides/DataAccess/README.md b/Guides/DataAccess/README.md new file mode 100644 index 00000000..aefd0e21 --- /dev/null +++ b/Guides/DataAccess/README.md @@ -0,0 +1,195 @@ +# NFX Data Access +Accessing/Working with Data + +## Overview of NFX Data Access +NFX data access approach is a hybrid one. It is not a strict ORM or strict CRUD, rather a +combination of different approaches that are most beneficial to a particular application. + +NFX data access was designed with the following data-store types in mind: + +* Distributed data (i.e. web services/API sources) +* BigData (Hadoop, Hive etc.) +* Relational Data (SQL: MsSQL, MySQL, ORACLE etc.) +* NoSQL Data: Document and others (MongoDB, Riak, tuple spaces etc.) +* Unstructured data accessed via custom APIs(parse CSV files etc.) +* Non-homogenous data: all/some of the aforementioned sources may be needed in the same system + +The data access is facilitated via the `NFX.DataAccess.IDataStore` interface which is just a +marker interface for the application container (accessible via `NFX.App.DataStore` shortcut). + +Every system may select a combination of the following strategies that fit the particular case the best: + +* Calling 3rd party services (i.e. via REST) - pulling data via some API calls +* Read/Write some data via app-specific APIs (classes/props/methods) - similar to ORM +* Work with data via CRUD facade (i.e. DataStore.Insert(new Row{......}) - similar to Active Record pattern/Entity framework +* Work with higher-level facade to any of the aforementioned ways + +## Building Blocks: Rows, Schema, FieldDefs + +Any POCO (Plain CLR) class instance may be used to access data, as data stores are just interfaces, +for example: `MyCar car = MyApp.Data.GetCarsByDriver("Frank-123");`, as the function in the proceeding +example may return a domain object "MyCar". + +NFX.DataAccess.CRUD namespace provides a very convenient base for business logic/domain models + building blocks, which are typically used in a CRUD scenario: + +* NFX.DataAccess.CRUD.Schema +* NFX.DataAccess.CRUD.Row +* NFX.DataAccess.CRUD.RowsetBase/Rowset/Table +* NFX.DataAccess.CRUD.Query + +### CRUD.Schema +Schema defines the structure of the "table", it consists of FieldDef instances that define attributes +for every field. Fields may be of complex types (i.e. TypedRow). So Schema basically shapes the data +contained in Rows. + +### CRUD.Row +A row is a string of data, it consists of fields where every field is assigned a FieldDef from Schema. +A Schema is the property of a Row. FieldDef is a property of a field within the row. There are two +types of rows: + +* Dynamic Rows +* Typed Rows + +Dynamic rows are instances of `DynamicRow` class, they keep data internally in the `object[]`. +Typed rows are instances of sub-types of a `TypedRow`. The fields of typed row must be explicitly +declared in code and tagged with a `[Field]` attribute which defines field's detailed FieldDef. + +This design is very flexible, as both rows stem from `Row` abstract class, which has the following key +features: + + Row person = new DynamicRow(Schema.GetForTypedRow(PersonRow)); + person[0] = 123; + Assert.AreEqual(123, person["id"]); + person["name"] = "Frank Drebin"; + var error = person.Validate(); + Assert.IsNull(error); + + var person2 = new PersonRow();//no schema need to be passed as it is a typed row + person.CopyTo(person2); + ... + +### CRUD.Rowset + +Rowsets are what their name implies. There two types both inheriting from RowsetBase: + +* Rowset +* Table + +The difference between the two is the presence of primary key in the `NFX.DataAccess.CRUD.Table` +which allows for quick in-memory merges/findKey() calls, consequently table is not for sorting. It is +a pk-organized list of rows of the same schema. + +`NFX.DataAccess.CRUD.Rowset` does not have this limit - it allows to sort the data, however the +findkey() calls do linear search. + +An interesting feature of rowsets is the ability to mix Dynamic and Typed rows instances in one list +as long as their schemas are the same. + +Rowsets can track changes, if `RowsetBase.LogChanges=true`, then RowChange enumerable can be obtained +via `Rowset.Changes` property. The concept is somewhat simiar to .NET's DataSet, BUT there is a +**key difference** in the approach: **NFX Data Access is for accessing any data, not only relational**. + +### CRUD Virtual Query + +Queries are command objects that group parameters under some name. The queries are polymorphic (virtual), +that is: the backend provider (DataStore-implementor) is responsible for query to actual handler resolution. + +There are two types of handlers: +* Script QueryHandler +* Code Query Handler + +This design leads to infinite flexibility, as script queries may be written in backend-specific +scripting technology, i.e.: + + var qry = Query("GetUserById"){ new Query.Param("UID", 1234)}; + + //for My SQL, will get resolved into + SELECT T1.* FROM TBL_USER T1 WHERE T1.ID = ?UID + + //For MongoDB + #pragma + modify=user + + {"_id": "$$UID"}} + + //For Erlang MFA(module function arg) + nfx_test:exec_qry(get_user_bid, Uid:long()) + + +See NFX.NUnit.Integrations for more use-cases. + + +### CRUD Data Store + +`NFX.DataAccess.CRUD.Intfs.cs` contains the definitions of `ICRUDOPerations` which stipulate the contract +for working in a CRUD style: + + /// + /// Describes an entity that performs single (not in transaction/batch)CRUD operations + /// + public interface ICRUDOperations + { + /// + /// Returns true when backend supports true asynchronous operations, such as the ones that do + /// not create extra threads/empty tasks + /// + bool SupportsTrueAsynchrony { get;} + Schema GetSchema(Query query); + Task GetSchemaAsync(Query query); + List Load(params Query[] queries); + Task> LoadAsync(params Query[] queries); + RowsetBase LoadOneRowset(Query query); + Task LoadOneRowsetAsync(Query query); + Row LoadOneRow(Query query); + Task LoadOneRowAsync(Query query); + int Save(params RowsetBase[] rowsets); + Task SaveAsync(params RowsetBase[] rowsets); + int ExecuteWithoutFetch(params Query[] queries); + Task ExecuteWithoutFetchAsync(params Query[] queries); + int Insert(Row row); + Task InsertAsync(Row row); + int Upsert(Row row); + Task UpsertAsync(Row row); + int Update(Row row, IDataStoreKey key = null); + Task UpdateAsync(Row row, IDataStoreKey key = null); + int Delete(Row row, IDataStoreKey key = null); + Task DeleteAsync(Row row, IDataStoreKey key = null); + } + +This way of working with data backend is similar to the **"Active Record"** pattern. + +An example use case: + + var person = new PersonRow + { + ID = MyApp.Data.IDGenerator.GetNext(typeof(PersonRow)), + Name = "Jon Lord", + IsCertified = true + }; + + MyApp.Data.Upsert(person); + +Or a typical case of use with NFX.WAVE.MVC Web API: + + [Action("person", 1, "match{ methods='GET' accept-json='true'}"] + public object GetPerson(string id) + { + return MyApp.Data.LoadOneRow(Queries.PersonById(id)); + } + + [Action("person", 1, "match{ methods='POST' accept-json='true'}"] + public object PostPerson(Person person) + { + var err = person.Validate(); + if (err!=null) + return new {OK=false, Err = err.Message};//Or throw HttpStatus code exception + + MyApp.Data.Upsert(person); + return new {OK=true}; + } + +As illustrated above, the NFX.WAVE framework understands row injection into the MVC actions, +which is very convenient. + + diff --git a/Guides/Glue/README.md b/Guides/Glue/README.md new file mode 100644 index 00000000..0b9f97f6 --- /dev/null +++ b/Guides/Glue/README.md @@ -0,0 +1,2 @@ +# NFX Glue +Inter-process Communication Framework diff --git a/Guides/Log/README.md b/Guides/Log/README.md new file mode 100644 index 00000000..ce8fd87c --- /dev/null +++ b/Guides/Log/README.md @@ -0,0 +1,2 @@ +# NFX Logging +Logging Framework diff --git a/Guides/README.md b/Guides/README.md new file mode 100644 index 00000000..7d4209b6 --- /dev/null +++ b/Guides/README.md @@ -0,0 +1,39 @@ +# NFX +Server UNISTACK framework. + +License: Apache 2.0 + +This framework is written in C# from scratch and runs on Windows and Linux/Mono servers. + +**NUGET**: + https://www.nuget.org/packages/NFX/ + + `pm> install-package NFX` + +**Project Home**: + https://github.com/aumcode/nfx + +**Various Demo Projects**: + https://github.com/aumcode/nfx-demos + +## NFX Guides Index + +* About [UNISTACK](AboutUnistack.md) +* Application/Service/Component Models [App Container](AppModel/README.md) +* [Configuration](Config/README.md) +* [Logging](Log/README.md) +* Instrumentation and Telemetry +* Web Development [WAVE](WAVE/README.md) +* Interprocess Communication [Glue](Glue/README.md) +* [Data Access](DataAccess/README.md) +* Serialization +* Security +* Virtual File Systems +* Social Net Integration +* Payment Processing Integration +* Code Analysis/Textual Parsing +* Relational Model +* Templatization +* Erlang Integration + + diff --git a/Guides/WAVE/Field/readme.md b/Guides/WAVE/Field/readme.md new file mode 100644 index 00000000..63e530d9 --- /dev/null +++ b/Guides/WAVE/Field/readme.md @@ -0,0 +1,125 @@ +# Field Class +Field is the property of Record and represents a FieldDef from server-side NFX.DataAccess.CRUD.Schema on the client side along with view model/controller. + +## Field() +Constructor. Initializes a new instance using string field definition and value. +```js +new rec.Field(object fieldDef) +``` +### Examples +```js +var rec = new WAVE.RecordModel.Record("ID-123456", function(){ + new this.Field({Name: "FirstName", Type: "string", Required: true}); + new this.Field({Name: "LastName", Type: "string"}); + new this.Field({Name: "Age", Type: "int"}); + }); +``` +```js +var rec = new WAVE.RecordModel.Record("ID-123456"); +new rec.Field({Name: "FirstName", Type: "string"}); +new rec.Field({Name: "LastName", Type: "string"}); +new rec.Field({Name: "Age", Type: "int", Stored: false}); +``` + + +## drop() +Deletes this field from the record. + + +## deferValidation() +Defer or NOT validation event while changing of field’s value. +```js +deferValidation(bool flag) +``` +### Examples +```js +var rec = ... +var elog = ""; +rec.fldLastName.deferValidation(true); +rec.fldLastName.eventBind(WAVE.EventManager.ANY_EVENT, function(evtType, field, phase){ + elog += "|"+evtType + field.name() + phase + field.value(); + }); +rec.fldLastName.value("Brown"); +// elog = "|data-changeLastNamebeforeSmith|data-changeLastNameafterBrown" +``` +```js +var rec = ... +var elog = ""; +rec.fldLastName.deferValidation(false); +rec.fldLastName.eventBind(WAVE.EventManager.ANY_EVENT, function(evtType, field, phase){ + elog += "|"+evtType + field.name() + phase + field.value(); + }); +rec.fldLastName.value("Brown"); +// elog = "|data-changeLastNamebeforeSmith|validateLastNameundefinedBrown|validatedLastNameundefinedBrown|data-changeLastNameafterBrown" +``` + + +## eventBind() +Binds a function to the named event handler on field. +```js +eventBind(string evtName, function handler) +``` +| Parameter | Requirement | Description | +| --------- |:-----------:| --------------------------------------------- | +| evtName | required | name of event from WAVE.RecordModel namespace | +| handler | required | callback-function which fires on event | +### Examples +```js +var rec = ... +var elog = ""; +rec.eventBind(WAVE.RecordModel.EVT_FIELD_DROP, function(field, phase){ + elog += "|" + field.name() + phase; + }); +rec.fldLastName.drop(); +// elog = |LastNamebefore|LastNameafter +``` + + +## isGUIModified() +Returns true when this field was modified from an attached control. + + +## isModified() +Returns true when this field has been modified. + + +## loaded() +Returns true when this field has finished loading. + + +## toString() +Returns string like `[fieldType]Field(fieldName = 'fieldValue')`. + + +## stored() +Returns true if field must be stored back in the server (db). + + +## Validation functions +* valid() - returns true if field has not validation error. +* validate() - validates field and returns true if it is valid. +* validated() - returns true if field has been validated. +* validationError() - returns error thrown during validation. +### Examples +```js +var rec = new WAVE.RecordModel.Record("1", function(){ + new this.Field({Name: "A", Type: "string"}); + new this.Field({Name: "B", Type: "string", Required: true}); + }); + +rec.fldB.validated(); // false +rec.fldB.validate(); +rec.fldB.validated(); //true +rec.fldB.valid(); // false +var err = rec.fldB.validationError(); // contains: 'B' must have a value +rec.fldB.value("aaaaa"); +rec.fldB.valid(); // true +``` + + +## value() +`value()` - then returns field’s value. +`value(object value, bool fromGUI)` - Sets value and modified, validated flags if new value is different from an existing one. `fromGUI` indicates that field is being altered from an attached control. + + + diff --git a/Guides/WAVE/MVC.md b/Guides/WAVE/MVC.md new file mode 100644 index 00000000..d4206a8e --- /dev/null +++ b/Guides/WAVE/MVC.md @@ -0,0 +1,2 @@ +# NFX.Wave MVC +WAVE MVC Overview diff --git a/Guides/WAVE/README.md b/Guides/WAVE/README.md new file mode 100644 index 00000000..cc5efe69 --- /dev/null +++ b/Guides/WAVE/README.md @@ -0,0 +1,7 @@ +# NFX WAVE +Web Applications with Enhanced Views (Web, Pipeline, MVC, Filters etc.) + + +* Server-side [WAVE Server](Server.md) +* MVC [MVC](MVC.md) +* Client-side Java Script Library [WAVE.js](WVJS.md) diff --git a/Guides/WAVE/Record/readme.md b/Guides/WAVE/Record/readme.md new file mode 100644 index 00000000..89c10ff0 --- /dev/null +++ b/Guides/WAVE/Record/readme.md @@ -0,0 +1,155 @@ +# Record Class +The purpose of this class is to represent server-side NFX.DataAccess.CRUD.Schema on the client side along with view model/controller. +Record instances are initialized using server NFX.Wave.Client.RecordModelGenerator class that turns Schema into JSON object suitable for record initialization on client. + +## Record() +Constructor. Initializes a new instance using record id with optional field initialization function +or complex initialization vector: +```js +new WAVE.RecordModel.Record(string recID, function fieldFunc) +``` +or +```js +new WAVE.RecordModel.Record(object initVector) +``` +| Parameter | Requirement | Description | +| ---------- |:-----------:| ----------------------------------------------------------------- | +| fieldFunc | optional | callback-function which contains fields initialization statements | +| recID | required | unique record id | +| initVector | required | contains record id and fields' definitions with values | + +**Notes** +In both cases fields will be named as concatenation string 'fld' and field name from definition. + +### Examples +```js +var rec = new WAVE.RecordModel.Record("ID-123456"); +``` +```js +var rec = new WAVE.RecordModel.Record("ID-123456", function(){ + new this.Field({Name: "FirstName", Type: "string"}); + new this.Field({Name: "LastName", Type: "string"}); + new this.Field({Name: "Age", Type: "int", Required: true, MinValue: 10, MaxValue: 99}); + }); +``` +```js +var rec = new WAVE.RecordModel.Record({ID: 'REC-1', + fields: [ + {def: {Name: 'FirstName', Type: 'string'}, val: 'John'}, + {def: {Name: 'LastName', Type: 'string'}, val: 'Smith'}, + {def: {Name: 'Age', Type: 'int'}, val: 33}, + {def: {Name: 'Helper', Type: 'string', Stored: false}} + ]}); +``` + + +### Examples +```js +var rec = new WAVE.RecordModel.Record("1", function(){ + new this.Field({Name: "A", Type: "string"}); + new this.Field({Name: "B", Type: "string", Required: true}); + }); + +rec.validate(); +var allErr = rec.allValidationErrorStrings(); +// allErr contains: 'B' must have a value +``` + + +## data() +Returns a map of fields: `{fieldName:fieldValue,...}`. + +```js +data(bool modifiedOnly, bool includeNonStored) +``` +| Parameter | Requirement | Description | +| ---------------- |:-----------:| ----------------------------------- | +| modifiedOnly | optional | only get fields that have changed | +| includeNonStored | optional | include fields that are not stored | +### Examples +```js +var rec = new WAVE.RecordModel.Record({ID: 'REC-1', + fields: [ + {def: {Name: 'FirstName', Type: 'string'}, val: 'John'}, + {def: {Name: 'LastName', Type: 'string'}, val: 'Smith'}, + {def: {Name: 'Age', Type: 'int'}, val: 33}, + {def: {Name: 'Helper', Type: 'string', Stored: false}} + ]}); + +var d = JSON.stringify(rec.data(false, true)); +// d = {"FirstName":"John","LastName":"Smith","Age":33,"Helper":null} + +rec.fldLastName.value("Brown"); +var d = JSON.stringify(rec.data(true)); +// d = {"LastName":"Brown"} +``` + + +## eventBind() +Binds a function to the named event handler on record. +```js +eventBind(string evtName, function handler) +``` +| Parameter | Requirement | Description | +| --------- |:-----------:| --------------------------------------------- | +| evtName | required | name of event from WAVE.RecordModel namespace | +| handler | required | callback-function which fires on event | +### Examples +```js +var rec = new WAVE.RecordModel.Record("ID-123456", function(){ + new this.Field({Name: "FirstName", Type: "string"}); + new this.Field({Name: "LastName", Type: "string"}); + }); + +var elog = ""; +rec.eventBind(WAVE.RecordModel.EVT_FIELD_DROP, function(rec, field, phase){ + elog += "|" + field.name() + phase; + }); +rec.fldLastName.drop(); +// elog = |LastNamebefore|LastNameafter +``` + +## fieldByName() +Returns a field by its case-insensitive name or null. +```js +fieldByName(string fieldName) +``` +### Examples +```js +var rec = new WAVE.RecordModel.Record("ID-123456", function(){ + new this.Field({Name: "FirstName", Type: "string"}); + new this.Field({Name: "Age", Type: "int"}); + }); + +rec.fieldByName("FirstName").value("John"); +var v = rec.fldFirstName.value(); +// v = John +``` + + +## loaded() +Returns true when record has finished loading data and constructing fields. + + +##toString() +Returns string like `Record[RecID]`. + + +## Validation functions +* allValidationErrorStrings() - returns all record and field-level validation errors. +* validate() - validates record and returns true is everything is valid. +### Examples +```js +var rec = new WAVE.RecordModel.Record("1", function(){ + new this.Field({Name: "A", Type: "string"}); + new this.Field({Name: "B", Type: "string", Required: true}); + new this.Field({Name: "C", Type: "int", Required: true}); + }); + +rec.validate(); +var all = rec.allValidationErrorStrings(); +// all contains 'B' must have a value +// all contains 'C' must have a value +``` + + diff --git a/Guides/WAVE/RecordView/readme.md b/Guides/WAVE/RecordView/readme.md new file mode 100644 index 00000000..d419c108 --- /dev/null +++ b/Guides/WAVE/RecordView/readme.md @@ -0,0 +1,26 @@ +# RecordView +Binds Record model (an instance of Record class) with UI builder/rendering library which dynamically builds the DOM in the attached view components (div placeholders). Thus, changes made to view model/data model in record instance will get automatically represented in the attached UI. + +## RecordView +Constructor. Initializes a new instance using string field definition and value. +```js +new WAVE.RecordModel.Record(string ID, object record, object gui, bool manualViews) +``` +| Parameter | Requirement | Description | +| ----------- |:-----------:| --------------------------------------------------------------------- | +| ID | required | id - unique in page id of the view | +| record | required | data record instance | +| gui | optional | GUI library, if null then default script "wv.gui.js" must be included | +| manualViews | optional | if true then view controls will not be auto-constructed | +### Examples +```js +var REC = new WAVE.RecordModel.Record({ID: 'R1', + fields: [ + {def: {Name: 'FirstName', Type: 'string', Required: true}, val: 'John'}, + {def: {Name: 'LastName', Type: 'string', Required: true}, val: 'Smith'}, + {def: {Name: 'Age', Type: 'int', MinValue: 10}, val: 33}, + {def: {Name: 'MusicType', Type: 'string', LookupDict: {HRK: "Hard Rock", CRK: "Classic Rock", RAP: "Rap", CMU: "Classical music"}}}, + ]} + ); +var RVIEW = new WAVE.RecordModel.RecordView("V1", REC); +``` diff --git a/Guides/WAVE/Server.md b/Guides/WAVE/Server.md new file mode 100644 index 00000000..b4150c18 --- /dev/null +++ b/Guides/WAVE/Server.md @@ -0,0 +1,2 @@ +# NFX.Wave Server +WAVE Server Overview diff --git a/Guides/WAVE/WVJS.md b/Guides/WAVE/WVJS.md new file mode 100644 index 00000000..8ecf9692 --- /dev/null +++ b/Guides/WAVE/WVJS.md @@ -0,0 +1,223 @@ +# NFX Wave.js +Java Script Client Library + +## General-Purpose Functions + +### Array functions + +##### arrayDelete(array, object element): bool +Delete `element` from `array` and return true if `element` was found. + +##### arrayShallowCopy(source_array): object +Return new array - copy of `source_array`. + +##### arrayClear(array) +Delete all elements from `array`. + +##### inArray(array, object element): bool +Returns true when `array` contains `element`. + + +### Operations with objects and functions + +##### clone(object obj): object +Deep clones data object (not functions). + +##### extend(object obj, object ext, bool keepExisting): object +Mixin behavior - extends `obj` with properties of `ext`. +If flag `keepExisting=true` then existing object key is preserved, even if it is null. + +##### isArray(object obj): bool +Returns true when the passed parameter is an array, not a map or function. + +##### isFunction(object obj): bool +Returns true when the passed parameter is a function, not a map object or an array. + +##### isMapOrArray(obj): bool +Returns true when the passed parameter is an array, or map but not a function. + +##### isObject(object obj): bool +Returns true when the passed parameter is a map, not an array or function. + +##### isSame(object obj1, object obj2): bool +Returns true if both objects represent the same scalar value or complex structure/map that is keys/values of maps/arrays. Nulls are considered equivalent. + +##### overrideFunction(function original, function fn): function +Overrides existing function by wrapping in new one. May call base like so: +```js +object.about = WAVE.overrideFun(object.about, function(){ return this.baseFunction() + "overridden" }); +``` + +##### propStrAsObject(object obj, string prop) +Checks object property for string value and if it is then converts it to object (map). +Does nothing if prop does not exist, is null or not a string value. + + +### String functions + +##### charIsAZLetterOrDigit(char c): bool +Returns true if `c` is `[a-zA-Z0-9]`. + +##### intValid(string val): bool +Returns true if `val` is valid integer number. + +##### intValidPositive(string val): bool +Returns true if `val` is valid positive integer number. + +##### intValidPositiveOrZero(string val) +Returns true if `val` is valid positive integer number or zero. + +##### siNum(number num, int decimalPlaces): string +Converts `num` to its string representation in SI (Le Système International d’Unités) with precision desired. `1000 = "1.00k", .1="100.00m", 23.55 = "23.55", 999.999="1.00k"`. + +##### strCaps(string s, bool normalize): string +Capitalizes first chars after spaces or dots, optionally converting chars in between to lower case. + +##### strContains(string str, string seg, bool considCase): bool +Returns true when `str` contains a `seg` optionally respecting case. + +##### strDefault(string s, string dflt): string +If `s` is null or undefined then assigns `dflt` to it. If `dflt` is null or undefined then empty string. + +##### strEmpty(string s): bool +Returns true if string is undefined, null or empty. + +##### strEnsureEnding(string s, string ending): string +Ensures that string ends with the specified string: `strEnsureEnding("path",'/')`. + +##### strOneOf(string str, array set, string del): bool +Returns true if the case-insensitive trimmed string is in the set of values. Neither string nor set value may contain delimiter which is '|' by default: `strOneOf("car", ["car", "house", "tax"], ';')`. + +##### strTrim / strLTrim / strRTrim(string s): string +Remove spaces from both/left/right sides of `s`. + +##### strTrunc(string s, int maxLen, string endWith): string +Truncates `s` if its length exceeds `maxLen` and adds `endWith` string to result end. + +##### strSame(string str1, string str2): bool +Returns true if both string contain the same trimmed case-insensitive value. This method is useful for tasks like searches of components by name. + +##### strStartsWith / strEndsWith(string str, string seg, bool considCase): bool +Returns true if `str` starts/ends with `seg` optionally respecting case. + +##### strEscapeHTML(content): string +Replace html escape characters in `content` with special entities: `& --> &`. + +##### strHTMLTemplate(string tpl, object args): string +Returns content like `'@name@' --> 'Alex & Helen'` provided that `args = {name: 'Alex & Helen'}`. Data is HTML escaped. + +##### strTemplate(string tpl, object args): string +Returns content like `'{a: "@name@"}' -> '{a: "Alex & Boris"}'` provided that `args = {name: 'Alex & Boris'}`. Data is not HTML escaped. + +##### strIsEMail(string str): bool +Returns true if `str` is valid email like `a@bc.de` + +##### isValidScreenNameLetter(char c): bool +Returns true if `c` is allowable screen symbol including diacritic. + +##### isValidScreenNameLetterOrDigit(char c): bool +Returns true if `c` is number (0-9) or allowable screen symbol including diacritic. + +##### isValidScreenNameSeparator(char c): bool +Returns true if `c` is one from following set: ['.', '-', '_']. + +##### strIsScreenName(string s): bool +Returns true if `s` contains valid screen name/ID. + +##### strNormalizeUSPhone(string s) +Normalizes US phone string so it looks like (999) 999-9999x9999. If `s` starts with '+'(international phone, just return as-is). + +##### strLocalize(string iso, string schema, string fld, string val): string +Localizes string per supplied lang iso code within schema/field. + +##### toISODateTimeString(datetime dt): string +Returns '`dt` as string in ISO format (e.g. "2012-01-22T12:30:15.120Z"). + +##### toUSDateTimeString(datetime dt): string +Returns `dt` as string in "MM/DD/YYYY HH:MM:SS" format. + +##### toUSDateString(datetime dt): string +Returns `dt` as string in "MM/DD/YYYY" format. + +##### toSeconds(string dur): int +Parses duration string to total seconds. Example of duration string: "1d 10h 7m 13s". + +##### genRndKey(int keyLen, string alphabet): string +Generates random key with specified length from the alphabet of possible characters: `rndKey(10,"abcdefzq2")`. + +##### genAutoincKey(string seq, int val) +Returns next value of named sequence as result of: previous value (0 if function was not called for this sequence) + val. + +##### isScalar(val): bool +Returns true for scalar vars and false for arrays and objects. + +##### rnd(int min, int max): int +Returns random number in the range of min/max where min=0 max=100 by default: `rnd(10,57); rnd(30);` + +##### id(string s): object +`document.getElementById(s)`. + +##### getCookie(string name): string +Returns cookie by name or false if not found; + +##### setCookie(string name, string value) +Set cookie as `name = escape(value) + "; path=/"`. + +##### deleteCookie(string name) +Set expires as -1. + +##### Checking types +Returns true if `tp` is in: +* **isObjectType(tp)** - ["object", "json", "map", "array"] +* **isIntType(tp)** - ["int", "integer"] +* **isRealType(tp)** - ["float", "real", "double", "money"] +* **isBoolType(tp)** - ["bool", "boolean", "logical"] +* **isStringType(tp)** - ["str", "string", "char[]", "char", "varchar", "text"] +* **isDateType(tp)** - ["date", "datetime", "time", "timestamp"] + +##### convertScalarType(bool nullable, value, string type, dflt) +Converts scalar value into the specified type: +```js +convertScalarType(false, 13, "string", ""); +``` + + +## Event manager mixin +Implements the event-handler mechanism and can be added to any class. +For examples of using look at [Record](Record/readme.md) class description. + +##### eventInvocationSuspendCount: 0 +Increase to disable event firing for all events, decrease to enable, events are enabled again when value is <=0. This property is useful for batch updates to suppress many event firings that are not needed. + +##### eventBind(evtName, func) +Binds a function to the named event handler. + +##### eventUnbind(evtName, func) +Un-Binds a function from the named event handler. + +##### eventClear(evtName) +Clears all functions from the named event handler. + +##### eventInvoke(evtName) +Invokes all functions bound to the named event handler. + +##### eventSinkBind(sink) +Binds a sink instance (an object) that will receive all events dispatched by this manager. The `sink` must have a function called `eventNotify(evtName, sender, args)` that will be invoked. + +##### eventSinkUnbind(sink) +Un-Binds an object that received all events from this manager. + +##### eventSinkClear() +Clears all objects that cat as event sinks bound to this instance. + +##### eventSinks() +Returns a list of sink object that receive event notifications from this manager. + + +## Record Model +Record model is in the WAVE.RecordModel namespace. + +### Classes +* [Record](Record/readme.md) +* [Field](Field/readme.md) +* [RecordView](RecordView/readme.md) diff --git a/README.md b/README.md index 5aa3c941..ed32628e 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,10 @@ License: Apache 2.0 This framework is written in C# from scratch and runs on Windows and Linux/Mono servers. +**GUIDES**: +NEW 20160117, we are adding: + [NFX Documentation/Guides are here](Guides/README.md) + **NUGET**: https://www.nuget.org/packages/NFX/ diff --git a/Source/NFX.Wave/Filters/PortalFilter.cs b/Source/NFX.Wave/Filters/PortalFilter.cs index 2891a533..51b7922c 100644 --- a/Source/NFX.Wave/Filters/PortalFilter.cs +++ b/Source/NFX.Wave/Filters/PortalFilter.cs @@ -33,6 +33,9 @@ public class PortalFilter : WorkFilter { #region CONSTS public const string VAR_PORTAL_NAME = "portal-name"; + public const string CONF_THEME_COOKIE_NAME_ATTR = "theme-cookie-name"; + public const string CONF_USE_THEME_COOKIE_ATTR = "use-theme-cookie"; + public const string DEFAULT_THEME_COOKIE_NAME = "UIT"; #endregion #region .ctor @@ -43,6 +46,10 @@ public PortalFilter(WorkHandler handler, string name, int order) : base(handler, private void ctor(IConfigSectionNode confNode) { + + m_UseThemeCookie = confNode.AttrByName(CONF_USE_THEME_COOKIE_ATTR).ValueAsBool(true); + m_ThemeCookieName = confNode.AttrByName(CONF_THEME_COOKIE_NAME_ATTR).ValueAsString(DEFAULT_THEME_COOKIE_NAME); + //read matches foreach(var cn in confNode.Children.Where(cn=>cn.IsSameName(WorkMatch.CONFIG_MATCH_SECTION))) if(!m_PortalMatches.Register( FactoryUtils.Make(cn, typeof(WorkMatch), args: new object[]{ cn })) ) @@ -54,6 +61,9 @@ private void ctor(IConfigSectionNode confNode) #region Fields private OrderedRegistry m_PortalMatches = new OrderedRegistry(); + + private bool m_UseThemeCookie; + private string m_ThemeCookieName = DEFAULT_THEME_COOKIE_NAME; #endregion @@ -64,6 +74,24 @@ private void ctor(IConfigSectionNode confNode) /// public OrderedRegistry PortalMatches { get{ return m_PortalMatches;}} + + /// + /// Specifies true to interpret ThemeCookieName + /// + public bool UseThemeCookie + { + get { return m_UseThemeCookie;} + set { m_UseThemeCookie = value; } + } + + /// + /// Specifies theme cookie name + /// + public string ThemeCookieName + { + get { return m_ThemeCookieName ?? DEFAULT_THEME_COOKIE_NAME;} + set { m_ThemeCookieName = value; } + } #endregion #region Protected @@ -105,6 +133,18 @@ protected sealed override void DoFilterWork(WorkContext work, IList } } + if (m_UseThemeCookie && work.m_Portal!=null) + { + //Use regular cookies so client JS can set it up + var tcv = work.Request.Cookies[m_ThemeCookieName];//work.Response.GetClientVar(m_ThemeCookieName); + if (tcv!=null && tcv.Value.IsNotNullOrWhiteSpace()) + { + var theme = work.m_Portal.Themes[tcv.Value]; + if (theme!=null) + work.m_PortalTheme = theme; + } + } + if (Server.m_InstrumentationEnabled && work.m_Portal!=null && work.m_Portal.InstrumentationEnabled) diff --git a/Source/NFX.Wave/Handlers/FileDownloadHandler.cs b/Source/NFX.Wave/Handlers/FileDownloadHandler.cs index 180c76b1..a4cc9cd2 100644 --- a/Source/NFX.Wave/Handlers/FileDownloadHandler.cs +++ b/Source/NFX.Wave/Handlers/FileDownloadHandler.cs @@ -160,6 +160,8 @@ protected override void DoHandleWork(WorkContext work) work.Response.WriteFile(fileName, attachment: attachment); else { + var ext = Path.GetExtension(fsFile.Name); + work.Response.ContentType = NFX.Web.ContentType.ExtensionToContentType(ext, NFX.Web.ContentType.BINARY); work.Response.WriteStream(fsFile.FileStream, attachmentName: attachment ? Path.GetFileName(fileName) : null); } } diff --git a/Source/NFX.Wave/Handlers/MVCHandler.cs b/Source/NFX.Wave/Handlers/MVCHandler.cs index 1d1be104..09537e93 100644 --- a/Source/NFX.Wave/Handlers/MVCHandler.cs +++ b/Source/NFX.Wave/Handlers/MVCHandler.cs @@ -93,6 +93,7 @@ protected override void DoTargetWork(Controller target, WorkContext work) } finally { + target.m_WorkContext = null; } } diff --git a/Source/NFX.Wave/MVC/SessionCSRFCheckAttribute.cs b/Source/NFX.Wave/MVC/SessionCSRFCheckAttribute.cs new file mode 100644 index 00000000..557eeb25 --- /dev/null +++ b/Source/NFX.Wave/MVC/SessionCSRFCheckAttribute.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + +namespace NFX.Wave.MVC +{ + /// + /// Decorates controller actions that need to check CSRF token against the user session + /// + public sealed class SessionCSRFCheckAttribute : ActionFilterAttribute + { + public const string DEFAULT_TOKEN_NAME = "token"; + + + public SessionCSRFCheckAttribute() + { + TokenName = DEFAULT_TOKEN_NAME; + } + + public SessionCSRFCheckAttribute(string tokenName) : this(tokenName, true) + { + } + + public SessionCSRFCheckAttribute(string tokenName, bool onlyExistingSession) + { + TokenName = tokenName; + + if (TokenName.IsNullOrWhiteSpace()) + TokenName = DEFAULT_TOKEN_NAME; + + OnlyExistingSession = onlyExistingSession; + } + + + public string TokenName{ get; private set; } + public bool OnlyExistingSession{ get; private set; } + + + protected internal override bool BeforeActionInvocation(Controller controller, WorkContext work, string action, MethodInfo method, object[] args, ref object result) + { + if (work.IsGET) return false; + + work.NeedsSession(OnlyExistingSession); + + var session = work.Session; + var supplied = work.MatchedVars[TokenName].AsString(); + + if (session==null || + !session.CSRFToken.EqualsOrdIgnoreCase(supplied)) + throw new HTTPStatusException(NFX.Wave.SysConsts.STATUS_400, NFX.Wave.SysConsts.STATUS_400_DESCRIPTION, "CSRF failed"); + + return false; + } + + protected internal override bool AfterActionInvocation(Controller controller, WorkContext work, string action, MethodInfo method, object[] args, ref object result) + { + return false; + } + } +} diff --git a/Source/NFX.Wave/NFX.Wave.csproj b/Source/NFX.Wave/NFX.Wave.csproj index 27db9b33..dfaa7634 100644 --- a/Source/NFX.Wave/NFX.Wave.csproj +++ b/Source/NFX.Wave/NFX.Wave.csproj @@ -73,6 +73,7 @@ + @@ -102,16 +103,19 @@ + + + + + - - @@ -400,9 +404,11 @@ java -jar "$(SolutionDir)lib\closure-compiler\compiler.jar" ^ --js "$(ProjectDir)Templatization\StockContent\Embedded\script\wv.js" ^ "$(ProjectDir)Templatization\StockContent\Embedded\script\wv.gui.js" ^ "$(ProjectDir)Templatization\StockContent\Embedded\script\wv.chart.svg.js" ^ + "$(ProjectDir)Templatization\StockContent\Embedded\script\wv.braintree.js" ^ + "$(ProjectDir)Templatization\StockContent\Embedded\script\wv.stripe.js" ^ --js_output_file "$(ProjectDir)Templatization\StockContent\Embedded\script\wv.all.min.js" ^ --compilation_level SIMPLE_OPTIMIZATIONS ^ - --language_in ECMASCRIPT5 > "$(ProjectDir)Templatization\StockContent\Embedded\script\CLOSURE_ERROR_OUT.txt" 2>&1 + --language_in ECMASCRIPT5_STRICT > "$(ProjectDir)Templatization\StockContent\Embedded\script\CLOSURE_ERROR_OUT.txt" 2>&1 :NO_JAVA exit 0 diff --git a/Source/NFX.Wave/Templatization/StockContent/Embedded/script/payment.providers.test.html b/Source/NFX.Wave/Templatization/StockContent/Embedded/script/payment.providers.test.html new file mode 100644 index 00000000..fe1e5db2 --- /dev/null +++ b/Source/NFX.Wave/Templatization/StockContent/Embedded/script/payment.providers.test.html @@ -0,0 +1,217 @@ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + +
CARD
Number: + +
Exp. Month: + +
Exp. Year: + +
CVC: + +
Client-side Validation:
+ +
+ + + + + + + + + + + + + + + + + +
STRIPE
Publishable Key: + + + +
+ + + + + + + + + + +
BRAINTREE
+
+
Server-side
+
+ Server-side is responsible for generation client tokens. + In case of .NET server SDK can be found in .NET SDK. + Below is a .NET server-side implementation contains server's private information shown here for a testing purposes only. + You should never expose private tokens on your client-side. +
+
+ var gateway = new BraintreeGateway
+ {
+
+ Environment = Braintree.Environment.SANDBOX,
+ MerchantId = "qc4hnd8qh7n32zj5",
+ PublicKey = "f5jhgpmmxmzmdvjf",
+ PrivateKey = "7b51b94780d0b01e9a162054fa8f8126"
+
+ };
+ var clientToken = gateway.ClientToken.generate();
+
+
+ +
+ + + + + + + + + + + + + + + +
Client-side
Client Token: + + + +
+
+ +
+ + + + \ No newline at end of file diff --git a/Source/NFX.Wave/Templatization/StockContent/Embedded/script/utest/wv.data.tests.htm b/Source/NFX.Wave/Templatization/StockContent/Embedded/script/utest/wv.data.tests.htm index 7c9a449c..036a9838 100644 --- a/Source/NFX.Wave/Templatization/StockContent/Embedded/script/utest/wv.data.tests.htm +++ b/Source/NFX.Wave/Templatization/StockContent/Embedded/script/utest/wv.data.tests.htm @@ -6,6 +6,8 @@ WV Data/RecordModel Unit Test + +