<commit_before>import {bindable, inject, processContent, ObserverLocator, customElement, skipContentProcessing } from 'aurelia-framework';
import {GridColumn} from './grid-column';
import {Compiler} from 'CharlesPockert/aurelia-compiler';
import './aurelia-bs-grid.css!';

@customElement('grid')
@skipContentProcessing()
@inject(Element, Compiler, ObserverLocator)
export class Grid {

	/* == Styling == */
	@bindable gridHeight = 0;

	/* == Options == */

	// Initial load flag (for client side)
	@bindable initialLoad = false;

	// Filtering
	@bindable showColumnFilters = false;
	@bindable serverFiltering = false;
	@bindable filterDebounce = 500;

	// Pagination
	@bindable serverPaging = false;
	@bindable pageable = true;
	@bindable pageSize = 10;
	@bindable page = 1;
	@bindable pagerSize = 10;

	@bindable showPageSizeBox = true;
	@bindable showPagingSummary = true;
	@bindable showFirstLastButtons = true;
	@bindable showJumpButtons = true;

	@bindable pageSizes = [10, 25, 50];

	firstVisibleItem = 0;
	lastVisibleItem = 0;

	pageNumber = 1;

	// Sortination
	@bindable serverSorting = false;
	@bindable sortable = true;
	sortProcessingOrder = []; // Represents which order to apply sorts to each column
	sorting = {};

	// Burnination?
	Trogdor = true;

	// Column defs
	@bindable autoGenerateColumns;
	@bindable showColumnHeaders = true;
	columnHeaders = [];
	columns = [];

	// Selection
	@bindable selectable = false;
	@bindable selectedItem = null;

	// Misc
	@bindable noRowsMessage = "";

	// Data ....
	@bindable autoLoad = true;
	loading = false;
	@bindable loadingMessage = "Loading...";

	// Read
	@bindable read = null;
	@bindable onReadError = null;

	// Tracking
	cache = [];
	data = [];
	count = 0;

	// Subscription handling
	unbinding = false;

	// Visual
	// TODO: calc scrollbar width using browser
	scrollBarWidth = 16;

	constructor(element, compiler, observerLocator) {
		this.element = element;
		this.compiler = compiler;
		this.observerLocator = observerLocator;

		// Grab user template from element
		this.processUserTemplate();
	}

	processUserTemplate() {

		// Get any col tags from the content
		var rowElement = this.element.querySelector("grid-row");
		var columnElements = Array.prototype.slice.call(rowElement.querySelectorAll("grid-col"));

		columnElements.forEach(c => {

			var attrs = Array.prototype.slice.call(c.attributes), colHash = {};
			attrs.forEach(a => colHash[a.name] = a.value);

			var col = new GridColumn(colHash, c.innerHTML);

			this.addColumn(col);
		});

		// Pull any row attrs into a hash object
		this.rowAttrs = {};
		var attrs = Array.prototype.slice.call(rowElement.attributes);
		attrs.forEach(a => this.rowAttrs[a.name] = a.value);

		// Remove all children
		while(this.element.childNodes.length > 0)
			this.element.removeChild(this.element.childNodes[0]);
	}

	/* === Lifecycle === */
	attached() {
		this.gridHeightChanged();

		if(this.autoLoad)
		    this.refresh();
	}

	bind(executionContext) {

		// Listen for window resize so we can re-flow the grid layout
		this.resizeListener = window.addEventListener('resize', this.syncColumnHeadersWithColumns.bind(this));

		this["$parent"] = executionContext;

		// Ensure the grid settings
		// If we can page on the server and we can't server sort, we can't sort locally
		if(this.serverPaging && !this.serverSorting)
			this.sortable = false;

		// Build the rows then dynamically compile the table
		// Get the table...
		var table = this.element.querySelector("table>tbody");
		var rowTemplate = table.querySelector("tr");

		// Create a fragment we will manipulate the DOM in
		var fragment = document.createDocumentFragment();

		// Move the row template to the fragment
		fragment.appendChild(rowTemplate);

		// Create the repeater
		rowTemplate.setAttribute("repeat.for", "$item of data");
		rowTemplate.setAttribute("class", "${ $item === $parent.selectedItem ? 'info' : '' }");

		// Copy any user specified row attributes to the row template
		for (var prop in this.rowAttrs) {
    		if (this.rowAttrs.hasOwnProperty(prop)) {
				rowTemplate.setAttribute(prop, this.rowAttrs[prop]);
        	}
		}

		// Create the columns
		this.columns.forEach(c => {
			var td = document.createElement("td");

			// Set attributes
			for (var prop in c) {
	    		if (c.hasOwnProperty(prop)) {

	    			if(prop == "template")
	    				td.innerHTML = c[prop];
	    			else
		    			td.setAttribute(prop, c[prop]);
	        	}
			}

			rowTemplate.appendChild(td);
		});

		// Compile
		this.compiler.compile(table, this, undefined, fragment);

		// HACK: why is the change handler not firing for noRowsMessage?
		this.noRowsMessageChanged();
	}

	unbind() {
		this.unbinding = true;
		window.removeEventListener('resize', this.resizeListener);
		this.dontWatchForChanges();
	}

	/* === Column handling === */
	addColumn(col) {

		// No-sort if grid is not sortable
		if(!this.sortable)
			col.nosort = true;

		this.columns.push(col);
	}

	/* === Paging === */
	pageChanged(page, oldValue) {

		if(page === oldValue) return;

		this.pageNumber = Number(page);
		this.refresh();
	}

	pageSizeChanged(newValue, oldValue) {

		if(newValue === oldValue) return;

		//this.pageChanged(1);
		this.updatePager();
	}

	filterSortPage(data) {
		// Applies filter, sort then page
		// 1. First filter the data down to the set we want, if we are using local data
		var tempData = data;

		if(this.showColumnFilters && !this.serverFiltering)
			tempData = this.applyFilter(tempData);

		// Count the data now before the sort/page
		this.count = tempData.length;

		// 2. Now sort the data
		if(this.sortable && !this.serverSorting)
			tempData = this.applySort(tempData);

		// 3. Now apply paging
		if(this.pageable && !this.serverPaging)
			tempData = this.applyPage(tempData);

		this.data = tempData;

		this.updatePager();

		this.watchForChanges();

		setTimeout(this.syncColumnHeadersWithColumns.bind(this), 0);
	}

	applyPage(data) {
		var start = (Number(this.pageNumber) - 1) * Number(this.pageSize);
		data = data.slice(start, start + Number(this.pageSize));

		return data;
	}

	updatePager() {
		if(this.pager)
			this.pager.update(this.pageNumber, Number(this.pageSize), Number(this.count));

		this.firstVisibleItem = (this.pageNumber - 1) * Number(this.pageSize) + 1;
		this.lastVisibleItem = Math.min((this.pageNumber) * Number(this.pageSize), this.count);
	}

	/* === Sorting === */
	fieldSorter(fields) {
	    return function (a, b) {
	        return fields
	            .map(function (o) {
	                var dir = 1;
	                if (o[0] === '-') {
	                   dir = -1;
	                   o = o.substring(1);
	                }
	                if (a[o] > b[o]) return dir;
	                if (a[o] < b[o]) return -(dir);
	                return 0;
	            })
	            .reduce(function firstNonZeroValue (p,n) {
	                return p ? p : n;
	            }, 0);
	    };
	}

	pageSizesChanged() {
		this.refresh();
	}

	sortChanged(field) {

		// Determine new sort
		var newSort = undefined;

		// Figure out which way this field should be sorting
	    switch(this.sorting[field]) {
	    	case "asc":
	    			newSort = "desc";
	    		break;
	    		case "desc":
	    			newSort = "";
	    		break;
	    		default:
	    			newSort = "asc";
	    		break;
	    }

	    this.sorting[field] = newSort;

	    // If the sort is present in the sort stack, slice it to the back of the stack, otherwise just add it
	    var pos = this.sortProcessingOrder.indexOf(field);

	    if(pos > -1)
	    	this.sortProcessingOrder.splice(pos, 1);

		this.sortProcessingOrder.push(field);

	    // Apply the new sort
		this.refresh();
	}

	applySort(data) {

		// Format the sort fields
		var fields = [];

		// Get the fields in the "sortingORder"
		for (var i = 0; i < this.sortProcessingOrder.length; i++) {
			var sort = this.sortProcessingOrder[i];

			for(var prop in this.sorting) {
				if(sort == prop && this.sorting[prop] !== "")
					fields.push(this.sorting[prop] === "asc" ? (prop) : ("-" + prop));
			}
		};

		// If server sort, just refresh
		data = data.sort(this.fieldSorter(fields));

		return data;
	}

	/* === Filtering === */
	applyFilter(data) {
		return data.filter((row) => {
			var include = true;

			for (var i = this.columns.length - 1; i >= 0; i--) {
				var col = this.columns[i];

				if(col.filterValue !== "" && row[col.field].toString().indexOf(col.filterValue) === -1) {
					include = false;
				}
			}

			return include;
		});
	}

	getFilterColumns() {
		var cols = {};

		for (var i = this.columns.length - 1; i >= 0; i--) {
			var col = this.columns[i];

			if(col.filterValue !== "")
				cols[col.field] = col.filterValue;
		}

		return cols;
	}

	debounce(func, wait) {
	    var timeout;

	    // the debounced function
	    return function() {

	        var context = this,
	            args = arguments;

	        // nulls out timer and calls original function
	        var later = function() {
	            timeout = null;
	            func.apply(context, args);
	        };

	        // restart the timer to call last function
	        clearTimeout(timeout);
	        timeout = setTimeout(later, wait);
	    };
	}

	updateFilters() {
		// Debounce

		if(!this.debouncedUpdateFilters) {
			this.debouncedUpdateFilters = this.debounce(this.refresh.bind(this), this.filterDebounce || 100);
		}

		this.debouncedUpdateFilters();
	}

	/* === Data === */
	refresh() {

		// If we have any server side stuff we need to get the data first
		this.dontWatchForChanges();

		if(this.serverPaging || this.serverSorting || this.serverFiltering || !this.initialLoad)
			this.getData();
		else
			this.filterSortPage(this.cache);

	}

	getData() {
		if(!this.read)
			throw new Error("No read method specified for grid");

		this.initialLoad = true;

		// TODO: Implement progress indicator
		this.loading = true;

		// Try to read from the data adapter
		this.read({
			sorting: this.sorting,
			paging: { page: this.pageNumber, size: Number(this.pageSize) },
			filtering: this.getFilterColumns()
		})
		.then((result) => {

			// Data should be in the result so grab it and assign it to the data property
			this.handleResult(result);

			this.loading = false;
		}, (result) => {
			// Something went terribly wrong, notify the consumer
			if(this.onReadError)
				this.onReadError(result);

			this.loading = false;
		});
	}

	handleResult(result) {

		// TODO: Check valid stuff was returned
		var data = result.data;

		// Is the data being paginated on the client side?
		// TODO: Work out when we should we use the cache... ever? If it's local data
		if(this.pageable && !this.serverPaging && !this.serverSorting && !this.serverFiltering) {
			// Cache the data
			this.cache = result.data;
			this.filterSortPage(this.cache);
		} else {
			this.data = result.data;
			this.filterSortPage(this.data);
		}

	    this.count = result.count;

	    // Update the pager - maybe the grid options should contain an update callback instead of reffing the
	    // pager into the current VM?
	    this.updatePager();
	}

	watchForChanges() {

		this.dontWatchForChanges();

		// Guard against data refresh events hitting after the user does anything that unloads the grid
		if(!this.unbinding)
		    // We can update the pager automagically
		    this.subscription = this.observerLocator
		        .getArrayObserver(this.cache)
		        .subscribe((splices) => {
					this.refresh();
		        });
	}

	dontWatchForChanges() {
		if(this.subscription)
			this.subscription();
	}

	/* === Selection === */

	select(item) {
		if(this.selectable)
			this.selectedItem = item;

		return true;
	}

	/* === Change handlers === */
	noRowsMessageChanged() {
		this.showNoRowsMessage = this.noRowsMessage !== "";
	}

	gridHeightChanged() {

		// TODO: Make this a one off
		var cont = this.element.querySelector(".grid-content-container");

		if(this.gridHeight > 0) {
			cont.setAttribute("style", "height:" + this.gridHeight + "px");
		} else {
			cont.removeAttribute("style");
		}
	}

	/* === Visual === */
	syncColumnHeadersWithColumns() {
		// Get the header row
		var headers = this.element.querySelectorAll("table>thead>tr:first-child>th");
		var filters = this.element.querySelectorAll("table>thead>tr:last-child>th");

		// Get the first row from the data if there is one...
		var cells = this.element.querySelectorAll("table>tbody>tr:first-child>td");

		for (var i = headers.length - 1; i >= 0; i--) {
			var header = headers[i];
			var filter = filters[i];
			var cell = cells[i];

			if(cell && header && filter) {
				var overflow = this.isBodyOverflowing();
				var tgtWidth = cell.offsetWidth + (i == headers.length - 1 && overflow ? this.scrollBarWidth : 0);

				// Make the header the same width as the cell...
				header.setAttribute("style", "width: " + tgtWidth + "px");
				filter.setAttribute("style", "width: " + tgtWidth + "px");
			}
		};
	}

	isBodyOverflowing() {
		var body = this.element.querySelector(".grid-content-container");
		return body.offsetHeight < body.scrollHeight || body.offsetWidth < body.scrollWidth;
	}
}

<commit_msg>Update grid.js<commit_after>import {bindable, inject, processContent, ObserverLocator, customElement, skipContentProcessing } from 'aurelia-framework';
import {GridColumn} from './grid-column';
import {Compiler} from 'charlespockert/aurelia-compiler';
import './aurelia-bs-grid.css!';

@customElement('grid')
@skipContentProcessing()
@inject(Element, Compiler, ObserverLocator)
export class Grid {

	/* == Styling == */
	@bindable gridHeight = 0;

	/* == Options == */

	// Initial load flag (for client side)
	@bindable initialLoad = false;

	// Filtering
	@bindable showColumnFilters = false;
	@bindable serverFiltering = false;
	@bindable filterDebounce = 500;

	// Pagination
	@bindable serverPaging = false;
	@bindable pageable = true;
	@bindable pageSize = 10;
	@bindable page = 1;
	@bindable pagerSize = 10;

	@bindable showPageSizeBox = true;
	@bindable showPagingSummary = true;
	@bindable showFirstLastButtons = true;
	@bindable showJumpButtons = true;

	@bindable pageSizes = [10, 25, 50];

	firstVisibleItem = 0;
	lastVisibleItem = 0;

	pageNumber = 1;

	// Sortination
	@bindable serverSorting = false;
	@bindable sortable = true;
	sortProcessingOrder = []; // Represents which order to apply sorts to each column
	sorting = {};

	// Burnination?
	Trogdor = true;

	// Column defs
	@bindable autoGenerateColumns;
	@bindable showColumnHeaders = true;
	columnHeaders = [];
	columns = [];

	// Selection
	@bindable selectable = false;
	@bindable selectedItem = null;

	// Misc
	@bindable noRowsMessage = "";

	// Data ....
	@bindable autoLoad = true;
	loading = false;
	@bindable loadingMessage = "Loading...";

	// Read
	@bindable read = null;
	@bindable onReadError = null;

	// Tracking
	cache = [];
	data = [];
	count = 0;

	// Subscription handling
	unbinding = false;

	// Visual
	// TODO: calc scrollbar width using browser
	scrollBarWidth = 16;

	constructor(element, compiler, observerLocator) {
		this.element = element;
		this.compiler = compiler;
		this.observerLocator = observerLocator;

		// Grab user template from element
		this.processUserTemplate();
	}

	processUserTemplate() {

		// Get any col tags from the content
		var rowElement = this.element.querySelector("grid-row");
		var columnElements = Array.prototype.slice.call(rowElement.querySelectorAll("grid-col"));

		columnElements.forEach(c => {

			var attrs = Array.prototype.slice.call(c.attributes), colHash = {};
			attrs.forEach(a => colHash[a.name] = a.value);

			var col = new GridColumn(colHash, c.innerHTML);

			this.addColumn(col);
		});

		// Pull any row attrs into a hash object
		this.rowAttrs = {};
		var attrs = Array.prototype.slice.call(rowElement.attributes);
		attrs.forEach(a => this.rowAttrs[a.name] = a.value);

		// Remove all children
		while(this.element.childNodes.length > 0)
			this.element.removeChild(this.element.childNodes[0]);
	}

	/* === Lifecycle === */
	attached() {
		this.gridHeightChanged();

		if(this.autoLoad)
		    this.refresh();
	}

	bind(executionContext) {

		// Listen for window resize so we can re-flow the grid layout
		this.resizeListener = window.addEventListener('resize', this.syncColumnHeadersWithColumns.bind(this));

		this["$parent"] = executionContext;

		// Ensure the grid settings
		// If we can page on the server and we can't server sort, we can't sort locally
		if(this.serverPaging && !this.serverSorting)
			this.sortable = false;

		// Build the rows then dynamically compile the table
		// Get the table...
		var table = this.element.querySelector("table>tbody");
		var rowTemplate = table.querySelector("tr");

		// Create a fragment we will manipulate the DOM in
		var fragment = document.createDocumentFragment();

		// Move the row template to the fragment
		fragment.appendChild(rowTemplate);

		// Create the repeater
		rowTemplate.setAttribute("repeat.for", "$item of data");
		rowTemplate.setAttribute("class", "${ $item === $parent.selectedItem ? 'info' : '' }");

		// Copy any user specified row attributes to the row template
		for (var prop in this.rowAttrs) {
    		if (this.rowAttrs.hasOwnProperty(prop)) {
				rowTemplate.setAttribute(prop, this.rowAttrs[prop]);
        	}
		}

		// Create the columns
		this.columns.forEach(c => {
			var td = document.createElement("td");

			// Set attributes
			for (var prop in c) {
	    		if (c.hasOwnProperty(prop)) {

	    			if(prop == "template")
	    				td.innerHTML = c[prop];
	    			else
		    			td.setAttribute(prop, c[prop]);
	        	}
			}

			rowTemplate.appendChild(td);
		});

		// Compile
		this.compiler.compile(table, this, undefined, fragment);

		// HACK: why is the change handler not firing for noRowsMessage?
		this.noRowsMessageChanged();
	}

	unbind() {
		this.unbinding = true;
		window.removeEventListener('resize', this.resizeListener);
		this.dontWatchForChanges();
	}

	/* === Column handling === */
	addColumn(col) {

		// No-sort if grid is not sortable
		if(!this.sortable)
			col.nosort = true;

		this.columns.push(col);
	}

	/* === Paging === */
	pageChanged(page, oldValue) {

		if(page === oldValue) return;

		this.pageNumber = Number(page);
		this.refresh();
	}

	pageSizeChanged(newValue, oldValue) {

		if(newValue === oldValue) return;

		//this.pageChanged(1);
		this.updatePager();
	}

	filterSortPage(data) {
		// Applies filter, sort then page
		// 1. First filter the data down to the set we want, if we are using local data
		var tempData = data;

		if(this.showColumnFilters && !this.serverFiltering)
			tempData = this.applyFilter(tempData);

		// Count the data now before the sort/page
		this.count = tempData.length;

		// 2. Now sort the data
		if(this.sortable && !this.serverSorting)
			tempData = this.applySort(tempData);

		// 3. Now apply paging
		if(this.pageable && !this.serverPaging)
			tempData = this.applyPage(tempData);

		this.data = tempData;

		this.updatePager();

		this.watchForChanges();

		setTimeout(this.syncColumnHeadersWithColumns.bind(this), 0);
	}

	applyPage(data) {
		var start = (Number(this.pageNumber) - 1) * Number(this.pageSize);
		data = data.slice(start, start + Number(this.pageSize));

		return data;
	}

	updatePager() {
		if(this.pager)
			this.pager.update(this.pageNumber, Number(this.pageSize), Number(this.count));

		this.firstVisibleItem = (this.pageNumber - 1) * Number(this.pageSize) + 1;
		this.lastVisibleItem = Math.min((this.pageNumber) * Number(this.pageSize), this.count);
	}

	/* === Sorting === */
	fieldSorter(fields) {
	    return function (a, b) {
	        return fields
	            .map(function (o) {
	                var dir = 1;
	                if (o[0] === '-') {
	                   dir = -1;
	                   o = o.substring(1);
	                }
	                if (a[o] > b[o]) return dir;
	                if (a[o] < b[o]) return -(dir);
	                return 0;
	            })
	            .reduce(function firstNonZeroValue (p,n) {
	                return p ? p : n;
	            }, 0);
	    };
	}

	pageSizesChanged() {
		this.refresh();
	}

	sortChanged(field) {

		// Determine new sort
		var newSort = undefined;

		// Figure out which way this field should be sorting
	    switch(this.sorting[field]) {
	    	case "asc":
	    			newSort = "desc";
	    		break;
	    		case "desc":
	    			newSort = "";
	    		break;
	    		default:
	    			newSort = "asc";
	    		break;
	    }

	    this.sorting[field] = newSort;

	    // If the sort is present in the sort stack, slice it to the back of the stack, otherwise just add it
	    var pos = this.sortProcessingOrder.indexOf(field);

	    if(pos > -1)
	    	this.sortProcessingOrder.splice(pos, 1);

		this.sortProcessingOrder.push(field);

	    // Apply the new sort
		this.refresh();
	}

	applySort(data) {

		// Format the sort fields
		var fields = [];

		// Get the fields in the "sortingORder"
		for (var i = 0; i < this.sortProcessingOrder.length; i++) {
			var sort = this.sortProcessingOrder[i];

			for(var prop in this.sorting) {
				if(sort == prop && this.sorting[prop] !== "")
					fields.push(this.sorting[prop] === "asc" ? (prop) : ("-" + prop));
			}
		};

		// If server sort, just refresh
		data = data.sort(this.fieldSorter(fields));

		return data;
	}

	/* === Filtering === */
	applyFilter(data) {
		return data.filter((row) => {
			var include = true;

			for (var i = this.columns.length - 1; i >= 0; i--) {
				var col = this.columns[i];

				if(col.filterValue !== "" && row[col.field].toString().indexOf(col.filterValue) === -1) {
					include = false;
				}
			}

			return include;
		});
	}

	getFilterColumns() {
		var cols = {};

		for (var i = this.columns.length - 1; i >= 0; i--) {
			var col = this.columns[i];

			if(col.filterValue !== "")
				cols[col.field] = col.filterValue;
		}

		return cols;
	}

	debounce(func, wait) {
	    var timeout;

	    // the debounced function
	    return function() {

	        var context = this,
	            args = arguments;

	        // nulls out timer and calls original function
	        var later = function() {
	            timeout = null;
	            func.apply(context, args);
	        };

	        // restart the timer to call last function
	        clearTimeout(timeout);
	        timeout = setTimeout(later, wait);
	    };
	}

	updateFilters() {
		// Debounce

		if(!this.debouncedUpdateFilters) {
			this.debouncedUpdateFilters = this.debounce(this.refresh.bind(this), this.filterDebounce || 100);
		}

		this.debouncedUpdateFilters();
	}

	/* === Data === */
	refresh() {

		// If we have any server side stuff we need to get the data first
		this.dontWatchForChanges();

		if(this.serverPaging || this.serverSorting || this.serverFiltering || !this.initialLoad)
			this.getData();
		else
			this.filterSortPage(this.cache);

	}

	getData() {
		if(!this.read)
			throw new Error("No read method specified for grid");

		this.initialLoad = true;

		// TODO: Implement progress indicator
		this.loading = true;

		// Try to read from the data adapter
		this.read({
			sorting: this.sorting,
			paging: { page: this.pageNumber, size: Number(this.pageSize) },
			filtering: this.getFilterColumns()
		})
		.then((result) => {

			// Data should be in the result so grab it and assign it to the data property
			this.handleResult(result);

			this.loading = false;
		}, (result) => {
			// Something went terribly wrong, notify the consumer
			if(this.onReadError)
				this.onReadError(result);

			this.loading = false;
		});
	}

	handleResult(result) {

		// TODO: Check valid stuff was returned
		var data = result.data;

		// Is the data being paginated on the client side?
		// TODO: Work out when we should we use the cache... ever? If it's local data
		if(this.pageable && !this.serverPaging && !this.serverSorting && !this.serverFiltering) {
			// Cache the data
			this.cache = result.data;
			this.filterSortPage(this.cache);
		} else {
			this.data = result.data;
			this.filterSortPage(this.data);
		}

	    this.count = result.count;

	    // Update the pager - maybe the grid options should contain an update callback instead of reffing the
	    // pager into the current VM?
	    this.updatePager();
	}

	watchForChanges() {

		this.dontWatchForChanges();

		// Guard against data refresh events hitting after the user does anything that unloads the grid
		if(!this.unbinding)
		    // We can update the pager automagically
		    this.subscription = this.observerLocator
		        .getArrayObserver(this.cache)
		        .subscribe((splices) => {
					this.refresh();
		        });
	}

	dontWatchForChanges() {
		if(this.subscription)
			this.subscription();
	}

	/* === Selection === */

	select(item) {
		if(this.selectable)
			this.selectedItem = item;

		return true;
	}

	/* === Change handlers === */
	noRowsMessageChanged() {
		this.showNoRowsMessage = this.noRowsMessage !== "";
	}

	gridHeightChanged() {

		// TODO: Make this a one off
		var cont = this.element.querySelector(".grid-content-container");

		if(this.gridHeight > 0) {
			cont.setAttribute("style", "height:" + this.gridHeight + "px");
		} else {
			cont.removeAttribute("style");
		}
	}

	/* === Visual === */
	syncColumnHeadersWithColumns() {
		// Get the header row
		var headers = this.element.querySelectorAll("table>thead>tr:first-child>th");
		var filters = this.element.querySelectorAll("table>thead>tr:last-child>th");

		// Get the first row from the data if there is one...
		var cells = this.element.querySelectorAll("table>tbody>tr:first-child>td");

		for (var i = headers.length - 1; i >= 0; i--) {
			var header = headers[i];
			var filter = filters[i];
			var cell = cells[i];

			if(cell && header && filter) {
				var overflow = this.isBodyOverflowing();
				var tgtWidth = cell.offsetWidth + (i == headers.length - 1 && overflow ? this.scrollBarWidth : 0);

				// Make the header the same width as the cell...
				header.setAttribute("style", "width: " + tgtWidth + "px");
				filter.setAttribute("style", "width: " + tgtWidth + "px");
			}
		};
	}

	isBodyOverflowing() {
		var body = this.element.querySelector(".grid-content-container");
		return body.offsetHeight < body.scrollHeight || body.offsetWidth < body.scrollWidth;
	}
}

<|endoftext|>


<commit_before>import gulp from 'gulp'
import rump from '..'
import {green, magenta} from 'chalk'
import {version} from '../../package'

const name = ::rump.taskName,
      task = ::gulp.task,
      {environment} = rump.configs.main

task(name('info:core'), () => {
  console.log()
  console.log(magenta(`--- Core v${version}`))
  console.log(`Environment is ${green(environment)}`)
  console.log()
})

task(name('info'), [name('info:core')])
task(name('info:prod'), [name('prod:setup'), name('info')])
<commit_msg>Fetch environment in task for info task<commit_after>import gulp from 'gulp'
import rump from '..'
import {green, magenta} from 'chalk'
import {version} from '../../package'

const name = ::rump.taskName,
      task = ::gulp.task

task(name('info:core'), () => {
  const {environment} = rump.configs.main
  console.log()
  console.log(magenta(`--- Core v${version}`))
  console.log(`Environment is ${green(environment)}`)
  console.log()
})

task(name('info'), [name('info:core')])
task(name('info:prod'), [name('prod:setup'), name('info')])
<|endoftext|>


<commit_before>import cssSelectorExtract from 'css-selector-extract';
import fs from 'fs';
import path from 'path';
import postcssScss from 'postcss-scss';

/**
 * Import only certain CSS selectors form a file.
 */
export default class SelectorImporter {
  /**
   * @param {Object} options - Configuration options.
   */
  constructor(options = {}) {
    const defaultOptions = {
      includePaths: [process.cwd()]
    };
    /** @type {Object} */
    this.options = Object.assign({}, defaultOptions, options);
  }

  /**
   * Parse a url for selector filters.
   * @param {string} url - Import url from node-sass.
   * @return {Object} Cleaned up url and selector filter array.
   */
  parseUrl(url) {
    // Find selectors in the import url and
    // return a cleaned up url and the selectors.
    let cleanUrl = url;
    let selectorFilters;
    const selectorFiltersMatch = url.match(/{([^}]+)}/);
    if (selectorFiltersMatch) {
      cleanUrl = url.replace(/(\r\n|\n|\r)/gm, ` `).split(` from `)[1].trim();
      // Create an array with selectors and replacement as one value.
      selectorFilters = selectorFiltersMatch[1].split(`,`)
        // Split selectors and replacement selectors into an array.
        .map((filter) => {
          const filterArray = filter.trim().split(` as `)
            .map(Function.prototype.call, String.prototype.trim);

          let selector = filterArray[0];
          const replacement = filterArray[1];

          const matchRegExpSelector = /^\/(.+)\/([a-z]*)$/.exec(selector);
          if (matchRegExpSelector) {
            const pattern = matchRegExpSelector[1];
            const flags = matchRegExpSelector[2];
            selector = new RegExp(pattern, flags);
          }

          return {
            selector,
            replacement
          };
        });
    }
    return { url: cleanUrl, selectorFilters };
  }

  /**
   * Extract and replace selectors from a file with the given url.
   * @param {string} cleanUrl - Cleaned up import url from node-sass.
   * @param {Array} selectorFilters - Selector filter array.
   * @return {string} Contents string or null.
   */
  extractSelectors(cleanUrl, selectorFilters) {
    let contents = null;

    if (!selectorFilters) {
      return contents;
    }

    this.options.includePaths.some((includePath) => {
      try {
        const css = fs.readFileSync(path.join(includePath, cleanUrl), { encoding: `utf8` });
        if (css) {
          contents = cssSelectorExtract.processSync(css, selectorFilters, postcssScss);
          return true;
        }
      } catch (e) {}
      return false;
    });

    return contents;
  }

  /**
   * Synchronously resolve filtered contentes from a file with the given url.
   * @param {string} url - Import url from node-sass.
   * @return {string} Contents string or null.
   */
  resolveSync(url) {
    const data = this.parseUrl(url);
    const cleanUrl = data.url;
    const selectorFilters = data.selectorFilters;

    return this.extractSelectors(cleanUrl, selectorFilters);
  }

  /**
   * Asynchronously resolve filtered contentes from a file with the given url.
   * @param {string} url - Import url from node-sass.
   * @return {Promise} Promise for a contents string.
   */
  resolve(url) {
    return new Promise((promiseResolve) => {
      promiseResolve(this.resolveSync(url));
    });
  }
}
<commit_msg>Fix typo, fixes #16<commit_after>import cssSelectorExtract from 'css-selector-extract';
import fs from 'fs';
import path from 'path';
import postcssScss from 'postcss-scss';

/**
 * Import only certain CSS selectors form a file.
 */
export default class SelectorImporter {
  /**
   * @param {Object} options - Configuration options.
   */
  constructor(options = {}) {
    const defaultOptions = {
      includePaths: [process.cwd()]
    };
    /** @type {Object} */
    this.options = Object.assign({}, defaultOptions, options);
  }

  /**
   * Parse a url for selector filters.
   * @param {string} url - Import url from node-sass.
   * @return {Object} Cleaned up url and selector filter array.
   */
  parseUrl(url) {
    // Find selectors in the import url and
    // return a cleaned up url and the selectors.
    let cleanUrl = url;
    let selectorFilters;
    const selectorFiltersMatch = url.match(/{([^}]+)}/);
    if (selectorFiltersMatch) {
      cleanUrl = url.replace(/(\r\n|\n|\r)/gm, ` `).split(` from `)[1].trim();
      // Create an array with selectors and replacement as one value.
      selectorFilters = selectorFiltersMatch[1].split(`,`)
        // Split selectors and replacement selectors into an array.
        .map((filter) => {
          const filterArray = filter.trim().split(` as `)
            .map(Function.prototype.call, String.prototype.trim);

          let selector = filterArray[0];
          const replacement = filterArray[1];

          const matchRegExpSelector = /^\/(.+)\/([a-z]*)$/.exec(selector);
          if (matchRegExpSelector) {
            const pattern = matchRegExpSelector[1];
            const flags = matchRegExpSelector[2];
            selector = new RegExp(pattern, flags);
          }

          return {
            selector,
            replacement
          };
        });
    }
    return { url: cleanUrl, selectorFilters };
  }

  /**
   * Extract and replace selectors from a file with the given url.
   * @param {string} cleanUrl - Cleaned up import url from node-sass.
   * @param {Array} selectorFilters - Selector filter array.
   * @return {string} Contents string or null.
   */
  extractSelectors(cleanUrl, selectorFilters) {
    let contents = null;

    if (!selectorFilters) {
      return contents;
    }

    this.options.includePaths.some((includePath) => {
      try {
        const css = fs.readFileSync(path.join(includePath, cleanUrl), { encoding: `utf8` });
        if (css) {
          contents = cssSelectorExtract.processSync(css, selectorFilters, postcssScss);
          return true;
        }
      } catch (e) {}
      return false;
    });

    return contents;
  }

  /**
   * Synchronously resolve filtered contents from a file with the given url.
   * @param {string} url - Import url from node-sass.
   * @return {string} Contents string or null.
   */
  resolveSync(url) {
    const data = this.parseUrl(url);
    const cleanUrl = data.url;
    const selectorFilters = data.selectorFilters;

    return this.extractSelectors(cleanUrl, selectorFilters);
  }

  /**
   * Asynchronously resolve filtered contents from a file with the given url.
   * @param {string} url - Import url from node-sass.
   * @return {Promise} Promise for a contents string.
   */
  resolve(url) {
    return new Promise((promiseResolve) => {
      promiseResolve(this.resolveSync(url));
    });
  }
}
<|endoftext|>


<commit_before>Meteor.publish('teamDirectChannels', function(search) {
  check(search, String);
  if (this.userId) {
    var team = Teams.findOne({ $or: [{ _id: search }, { slug: search }] });
    const teamId = team ? team._id : null;
    return Channels.find({ teamId: teamId, direct: true});
  }
  this.ready();
});
<commit_msg>Restrict our direct channel pub to only the logged in users channels.<commit_after>Meteor.publish('teamDirectChannels', function(search) {
  check(search, String);
  if (this.userId) {
    var team = Teams.findOne({ $or: [{ _id: search }, { slug: search }] });
    const teamId = team ? team._id : null;
    return Channels.find({ teamId: teamId, direct: true, allowedUsers: {$in: [this.userId]}});
  }
  this.ready();
});
<|endoftext|>


<commit_before>import sqlite from 'sqlite3';

module.exports = {

    setupRestApiGet : function(app) {

        // get sent a lat, long, send all items near it
        app.get('/parkItems', function(req, res) {
            var lat = +req.query.lat;
            var lng = +req.query.lng;

            var items = findAllNearbyItems(lat, lng, res);
        });
    }
}

function findAllNearbyItems(lat, lng, res) {

    var db = new sqlite.Database('UUUYou.db');
    db.all(`SELECT * FROM Items WHERE latitude BETWEEN ${lat-0.05} AND ${lat+0.05} AND longitude BETWEEN ${lng-0.05} AND ${lng+0.05}`, function(err, rows) {
        res.send(JSON.stringify(rows));
    });
    db.close();
}<commit_msg>Adding threshold to get request on parkItems<commit_after>import sqlite from 'sqlite3';

module.exports = {

    setupRestApiGet : function(app) {

        // get sent a lat, long, send all items near it
        app.get('/parkItems', function(req, res) {
            let lat = +req.query.lat;
            let lng = +req.query.lng;
            let thresh = +req.query.thresh || 0.05;

            findAndReturnAllNearbyItems(lat, lng, thresh, res);
        });
    }
}

function findAllNearbyItems(lat, lng, thresh, res) {

    var db = new sqlite.Database('UUUYou.db');
    db.all(`SELECT * FROM Items WHERE latitude BETWEEN ${lat-thresh} AND ${lat+thresh} AND longitude BETWEEN ${lng-thresh} AND ${lng+thresh}`, function(err, rows) {
        res.send(JSON.stringify(rows));
    });
    db.close();
}<|endoftext|>


<commit_before>/**
 * Copyright 2015-present Tuan Le.
 *
 * Licensed under the MIT License.
 * You may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://opensource.org/licenses/mit-license.html
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 *------------------------------------------------------------------------
 *
 * @module Hyperflow (Hflow) (A toolkit & library for developing universal app)
 * @description - Hflow namespace setup. Initialize Hflow, adding core modules, and apply settings.
 *
 * @author Tuan Le (tuan.t.lei@gmail.com)
 */
/* @flow */
'use strict'; // eslint-disable-line

/* load CommonElement */
import CommonElement from './core/elements/common-element';

/* load DataElement */
import DataElement from './core/elements/data-element';

/* load CompositeElement */
import CompositeElement from './core/elements/composite-element';

/* load Composer */
import Composer from './core/composer';

/* load StoreFactory */
import StoreFactory from './core/factories/store-factory';

/* load DomainFactory */
import DomainFactory from './core/factories/domain-factory';

/* load InterfaceFactory */
import InterfaceFactory from './core/factories/interface-factory';

/* load ServiceFactory */
import ServiceFactory from './core/factories/service-factory';

/* load AppFactory */
import AppFactory from './core/factories/app-factory';

/* load test AgentFactory */
import AgentFactory from './core/factories/agent-factory';

/* load test FixtureFactory and composites */
// import FixtureFactory from './core/factories/fixture-factory';
// import DomainTestFixtureComposite from './core/factories/composites/test-fixtures/domain-test-fixture-composite';
// import ServiceTestFixtureComposite from './core/factories/composites/test-fixtures/service-test-fixture-composite';
// import StoreTestFixtureComposite from './core/factories/composites/test-fixtures/store-test-fixture-composite';
// import InterfaceTestFixtureComposite from './core/factories/composites/test-fixtures/interface-test-fixture-composite';

/* load test runner composites library */
// import TapeTestRunnerComposite from './core/factories/composites/tape-test-runner-composite';

/* load state composites library */
import StateReducerComposite from './core/factories/composites/state-reducer-composite';
import StateTimeTraversalComposite from './core/factories/composites/state-time-traversal-composite';

/* load React composites library */
import ReactComponentComposite from './composites/interfaces/react-component-composite';
import ReactClientNativeAppComponentComposite from './composites/apps/client-native/react-app-component-composite';
import ReactClientWebAppComponentComposite from './composites/apps/client-web/react-app-component-composite';
import ReactServerAppComponentComposite from './composites/apps/server/react-app-component-composite';
import ReactClientWebAppRendererComposite from './composites/apps/client-web/react-app-renderer-composite';
import ReactClientNativeAppRendererComposite from './composites/apps/client-native/react-app-renderer-composite';
import ReactServerAppRendererComposite from './composites/apps/server/react-app-renderer-composite';

/* load service library */
import WebStorageComposite from './composites/services/client-web/web-storage-composite';
import ASyncStorageComposite from './composites/services/client-native/async-storage-composite';
import PGComposite from './composites/services/server/pg-composite';

let Hflow = null;

/**
 * @description - Setup and initialize modules & dependencies for Hflow`s namespaces.
 *
 * @function init
 * @param {object} setting - Hflow's global settings.
 * @returns {object}
 */
const init = function init ({
    TARGET = `client-web`,
    DEVELOPMENT = true,
    ENABLE_INFO_MESSAGE = true,
    ENABLE_WARN_LVL0_MESSAGE = false,
    ENABLE_WARN_LVL1_MESSAGE = true
} = {}) {
    if (Hflow === null) {
        const common = CommonElement({
            devleopment: DEVELOPMENT,
            enableInfoMessage: ENABLE_INFO_MESSAGE,
            enableWarn0Message: ENABLE_WARN_LVL0_MESSAGE,
            enableWarn1Message: ENABLE_WARN_LVL1_MESSAGE
        });
        const HflowProperty = {
            VERSION: `0.1.0-beta6`,
            ENV: TARGET === `server` ? process.env : {}, // eslint-disable-line
            TARGET,
            DEVELOPMENT,
            /* set composer factory namespace */
            Composer,
            /* set data and composite element namespaces */
            Data: DataElement,
            Composite: CompositeElement,
            /* set factory event stream Ids creator */
            Event: {
                /**
                 * @description - Function to contruct an event Id structure for factory event stream.
                 *
                 * @function eventIdCreate
                 * @param {object} eventIdObj - Event Id contructor object
                 * @returns {object}
                 */
                create: function create (sourceEventMap) {
                    if (!common.isSchema({
                        as: `array|undefined`,
                        on: `array|undefined`,
                        do: `array|undefined`,
                        request: `array|undefined`,
                        broadcast: `array|undefined`
                    }).of(sourceEventMap)) {
                        common.log(`error`, `Event.create - Input event map is invalid.`);
                    } else {
                        /* helper function to convert camel case to dash */
                        const camelcaseToDash = function camelcaseToDash (str) {
                            return str.replace(/(?:^|\.?)([A-Z])/g, (match, word) => {
                                return `-${word.toLowerCase()}`;
                            }).replace(/^_/, ``);
                        };
                        return Object.keys(sourceEventMap).reduce((outputEventMap, key) => {
                            if (key === `as`) {
                                if (sourceEventMap[key].every((_key) => common.isString(_key))) {
                                    outputEventMap[`AS`] = sourceEventMap[key].reduce((asEventMap, _key) => {
                                        asEventMap[
                                            common.camelcaseToUnderscore(_key).toUpperCase()
                                        ] = `as-${camelcaseToDash(_key)}`;
                                        return asEventMap;
                                    }, {});
                                } else {
                                    common.log(`error`, `Event.create - Input 'as' event keys are invalid.`);
                                }
                            }
                            if (key === `on`) {
                                if (sourceEventMap[key].every((_key) => common.isString(_key))) {
                                    outputEventMap[`ON`] = sourceEventMap[key].reduce((onEventMap, _key) => {
                                        onEventMap[
                                            common.camelcaseToUnderscore(_key).toUpperCase()
                                        ] = `on-${camelcaseToDash(_key)}`;
                                        return onEventMap;
                                    }, {});
                                } else {
                                    common.log(`error`, `Event.create - Input 'on' event keys are invalid.`);
                                }
                            }
                            if (key === `do`) {
                                if (sourceEventMap[key].every((_key) => common.isString(_key))) {
                                    outputEventMap[`DO`] = sourceEventMap[key].reduce((doEventMap, _key) => {
                                        doEventMap[
                                            common.camelcaseToUnderscore(_key).toUpperCase()
                                        ] = `do-${camelcaseToDash(_key)}`;
                                        return doEventMap;
                                    }, {});
                                } else {
                                    common.log(`error`, `Event.create - Input 'do' event keys are invalid.`);
                                }
                            }
                            if (key === `broadcast`) {
                                if (sourceEventMap[key].every((_key) => common.isString(_key))) {
                                    outputEventMap[`BROADCAST`] = sourceEventMap[key].reduce((broadcastEventMap, _key) => {
                                        broadcastEventMap[
                                            common.camelcaseToUnderscore(_key).toUpperCase()
                                        ] = `broadcast-${camelcaseToDash(_key)}`;
                                        return broadcastEventMap;
                                    }, {});
                                } else {
                                    common.log(`error`, `Event.create - Input 'broadcast' event keys are invalid.`);
                                }
                            }
                            if (key === `request`) {
                                if (sourceEventMap[key].every((_key) => common.isString(_key))) {
                                    outputEventMap[`REQUEST`] = sourceEventMap[key].reduce((requestForEventMap, _key) => {
                                        requestForEventMap[
                                            common.camelcaseToUnderscore(_key).toUpperCase()
                                        ] = `request-for-${camelcaseToDash(_key)}`;
                                        return requestForEventMap;
                                    }, {});
                                    outputEventMap[`RESPONSE`] = {
                                        WITH: sourceEventMap[key].reduce((responseToEventMap, _key) => {
                                            responseToEventMap[
                                                common.camelcaseToUnderscore(_key).toUpperCase()
                                            ] = `response-with-${camelcaseToDash(_key)}`;
                                            return responseToEventMap;
                                        }, {}),
                                        TO: sourceEventMap[key].reduce((responseToEventMap, _key) => {
                                            responseToEventMap[
                                                common.camelcaseToUnderscore(_key).toUpperCase()
                                            ] = {
                                                OK: `response-to-${camelcaseToDash(_key)}-ok`,
                                                NOT_FOUND: `response-to-${camelcaseToDash(_key)}-not-found`,
                                                CONFLICT: `response-to-${camelcaseToDash(_key)}-conflict`,
                                                ERROR: `response-to-${camelcaseToDash(_key)}-error`
                                            };
                                            return responseToEventMap;
                                        }, {})
                                    };
                                } else {
                                    common.log(`error`, `Event.create - Input 'request-for' event keys are invalid.`);
                                }
                            }
                            return outputEventMap;
                        }, {});
                    }
                }
            },
            /* set store, interface, domain, and service factory namespaces */
            Domain: DomainFactory,
            Store: StoreFactory,
            Interface: InterfaceFactory,
            Service: ServiceFactory,
            /* set app factory namespace */
            App: AppFactory,
            /* set test agent & fixtures factory namespace */
            Agent: AgentFactory,
            // Fixture: FixtureFactory,
            /* set state composite factory namespace */
            State: {
                ReducerComposite: StateReducerComposite,
                TimeTraversalComposite: StateTimeTraversalComposite
            },
            /* set test fixtures composites namespace */
            // Test: {
            //     TapeRunnerComposite: TapeTestRunnerComposite,
            //     DomainFixtureComposite: DomainTestFixtureComposite,
            //     StoreFixtureComposite: StoreTestFixtureComposite,
            //     InterfaceFixtureComposite: InterfaceTestFixtureComposite,
            //     ServiceFixtureComposite: ServiceTestFixtureComposite
            // },
            /* set composite library namespace */
            React: {
                ComponentComposite: ReactComponentComposite,
                AppComponentComposite: (() => {
                    switch (TARGET) { // eslint-disable-line
                    case `client-native`:
                        return ReactClientNativeAppComponentComposite;
                    case `client-web`:
                        return ReactClientWebAppComponentComposite;
                    case `server`:
                        return ReactServerAppComponentComposite;
                    }
                })(),
                AppRendererComposite: (() => {
                    switch (TARGET) { // eslint-disable-line
                    case `client-native`:
                        return ReactClientNativeAppRendererComposite;
                    case `client-web`:
                        return ReactClientWebAppRendererComposite;
                    case `server`:
                        return ReactServerAppRendererComposite;
                    }
                })()
            },
            Storage: (() => {
                switch (TARGET) { // eslint-disable-line
                case `client-native`:
                    return {
                        ASyncStorageComposite
                    };
                case `client-web`:
                    return {
                        WebStorageComposite
                    };
                case `server`:
                    return {
                        PGComposite
                    };
                }
            })()
        };

        /* create an initialized Hflow object */
        Hflow = Object.freeze(common.mix(common, {
            exclusion: {
                /* hide reveal method, reveal is for internal use only */
                keys: [
                    `reveal`
                ]
            }
        }).with(HflowProperty));
    }
    return Hflow;
};

export {
    Hflow,
    init
};
<commit_msg>Updated Hflow.Event.create method. Bumped to 0.1.0-beta6 version.<commit_after>/**
 * Copyright 2015-present Tuan Le.
 *
 * Licensed under the MIT License.
 * You may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://opensource.org/licenses/mit-license.html
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 *------------------------------------------------------------------------
 *
 * @module Hyperflow (Hflow) (A toolkit & library for developing universal app)
 * @description - Hflow namespace setup. Initialize Hflow, adding core modules, and apply settings.
 *
 * @author Tuan Le (tuan.t.lei@gmail.com)
 */
/* @flow */
'use strict'; // eslint-disable-line

/* load CommonElement */
import CommonElement from './core/elements/common-element';

/* load DataElement */
import DataElement from './core/elements/data-element';

/* load CompositeElement */
import CompositeElement from './core/elements/composite-element';

/* load Composer */
import Composer from './core/composer';

/* load StoreFactory */
import StoreFactory from './core/factories/store-factory';

/* load DomainFactory */
import DomainFactory from './core/factories/domain-factory';

/* load InterfaceFactory */
import InterfaceFactory from './core/factories/interface-factory';

/* load ServiceFactory */
import ServiceFactory from './core/factories/service-factory';

/* load AppFactory */
import AppFactory from './core/factories/app-factory';

/* load test AgentFactory */
import AgentFactory from './core/factories/agent-factory';

/* load test FixtureFactory and composites */
// import FixtureFactory from './core/factories/fixture-factory';
// import DomainTestFixtureComposite from './core/factories/composites/test-fixtures/domain-test-fixture-composite';
// import ServiceTestFixtureComposite from './core/factories/composites/test-fixtures/service-test-fixture-composite';
// import StoreTestFixtureComposite from './core/factories/composites/test-fixtures/store-test-fixture-composite';
// import InterfaceTestFixtureComposite from './core/factories/composites/test-fixtures/interface-test-fixture-composite';

/* load test runner composites library */
// import TapeTestRunnerComposite from './core/factories/composites/tape-test-runner-composite';

/* load state composites library */
import StateReducerComposite from './core/factories/composites/state-reducer-composite';
import StateTimeTraversalComposite from './core/factories/composites/state-time-traversal-composite';

/* load React composites library */
import ReactComponentComposite from './composites/interfaces/react-component-composite';
import ReactClientNativeAppComponentComposite from './composites/apps/client-native/react-app-component-composite';
import ReactClientWebAppComponentComposite from './composites/apps/client-web/react-app-component-composite';
import ReactServerAppComponentComposite from './composites/apps/server/react-app-component-composite';
import ReactClientWebAppRendererComposite from './composites/apps/client-web/react-app-renderer-composite';
import ReactClientNativeAppRendererComposite from './composites/apps/client-native/react-app-renderer-composite';
import ReactServerAppRendererComposite from './composites/apps/server/react-app-renderer-composite';

/* load service library */
import WebStorageComposite from './composites/services/client-web/web-storage-composite';
import ASyncStorageComposite from './composites/services/client-native/async-storage-composite';
import PGComposite from './composites/services/server/pg-composite';

let Hflow = null;

/**
 * @description - Setup and initialize modules & dependencies for Hflow`s namespaces.
 *
 * @function init
 * @param {object} setting - Hflow's global settings.
 * @returns {object}
 */
const init = function init ({
    TARGET = `client-web`,
    DEVELOPMENT = true,
    ENABLE_INFO_MESSAGE = true,
    ENABLE_WARN_LVL0_MESSAGE = false,
    ENABLE_WARN_LVL1_MESSAGE = true
} = {}) {
    if (Hflow === null) {
        const common = CommonElement({
            devleopment: DEVELOPMENT,
            enableInfoMessage: ENABLE_INFO_MESSAGE,
            enableWarn0Message: ENABLE_WARN_LVL0_MESSAGE,
            enableWarn1Message: ENABLE_WARN_LVL1_MESSAGE
        });
        const HflowProperty = {
            VERSION: `0.1.0-beta6`,
            ENV: TARGET === `server` ? process.env : {}, // eslint-disable-line
            TARGET,
            DEVELOPMENT,
            /* set composer factory namespace */
            Composer,
            /* set data and composite element namespaces */
            Data: DataElement,
            Composite: CompositeElement,
            /* set factory event stream Ids creator */
            Event: {
                /**
                 * @description - Function to contruct an event Id structure for factory event stream.
                 *
                 * @function eventIdCreate
                 * @param {object} eventIdObj - Event Id contructor object
                 * @returns {object}
                 */
                create: function create (sourceEventMap) {
                    if (!common.isSchema({
                        as: `array|undefined`,
                        on: `array|undefined`,
                        do: `array|undefined`,
                        request: `array|undefined`,
                        broadcast: `array|undefined`
                    }).of(sourceEventMap)) {
                        common.log(`error`, `Event.create - Input event map is invalid.`);
                    } else {
                        /* helper function to convert dash to uppercase underscore */
                        const dashToUpperCaseUnderscore = function dashToUpperCaseUnderscore (str) {
                            return str.replace(/-([a-z])/g, (match, word) => {
                                return `_${word}`;
                            }).toUpperCase();
                        };

                        const outputEventMap = Object.keys(sourceEventMap).reduce((_outputEventMap, key) => {
                            if (key === `as`) {
                                if (sourceEventMap[key].every((_key) => common.isString(_key))) {
                                    _outputEventMap[`AS`] = sourceEventMap[key].reduce((asEventMap, _key) => {
                                        asEventMap[
                                            dashToUpperCaseUnderscore(_key)
                                        ] = `as-${_key}`;
                                        return asEventMap;
                                    }, {});
                                } else {
                                    common.log(`error`, `Event.create - Input 'as' event keys are invalid.`);
                                }
                            }
                            if (key === `on`) {
                                if (sourceEventMap[key].every((_key) => common.isString(_key))) {
                                    _outputEventMap[`ON`] = sourceEventMap[key].reduce((onEventMap, _key) => {
                                        onEventMap[
                                            dashToUpperCaseUnderscore(_key)
                                        ] = `on-${_key}`;
                                        return onEventMap;
                                    }, {});
                                } else {
                                    common.log(`error`, `Event.create - Input 'on' event keys are invalid.`);
                                }
                            }
                            if (key === `do`) {
                                if (sourceEventMap[key].every((_key) => common.isString(_key))) {
                                    _outputEventMap[`DO`] = sourceEventMap[key].reduce((doEventMap, _key) => {
                                        doEventMap[
                                            dashToUpperCaseUnderscore(_key)
                                        ] = `do-${_key}`;
                                        return doEventMap;
                                    }, {});
                                } else {
                                    common.log(`error`, `Event.create - Input 'do' event keys are invalid.`);
                                }
                            }
                            if (key === `broadcast`) {
                                if (sourceEventMap[key].every((_key) => common.isString(_key))) {
                                    _outputEventMap[`BROADCAST`] = sourceEventMap[key].reduce((broadcastEventMap, _key) => {
                                        broadcastEventMap[
                                            dashToUpperCaseUnderscore(_key)
                                        ] = `broadcast-${_key}`;
                                        return broadcastEventMap;
                                    }, {});
                                } else {
                                    common.log(`error`, `Event.create - Input 'broadcast' event keys are invalid.`);
                                }
                            }
                            if (key === `request`) {
                                if (sourceEventMap[key].every((_key) => common.isString(_key))) {
                                    _outputEventMap[`REQUEST`] = sourceEventMap[key].reduce((requestForEventMap, _key) => {
                                        requestForEventMap[
                                            dashToUpperCaseUnderscore(_key)
                                        ] = `request-for-${_key}`;
                                        return requestForEventMap;
                                    }, {});
                                    _outputEventMap[`RESPONSE`] = {
                                        WITH: sourceEventMap[key].reduce((responseToEventMap, _key) => {
                                            responseToEventMap[
                                                dashToUpperCaseUnderscore(_key)
                                            ] = `response-with-${_key}`;
                                            return responseToEventMap;
                                        }, {}),
                                        TO: sourceEventMap[key].reduce((responseToEventMap, _key) => {
                                            responseToEventMap[
                                                dashToUpperCaseUnderscore(_key)
                                            ] = {
                                                OK: `response-to-${_key}-ok`,
                                                UNAUTHORIZED: `response-to-${_key}-unauthorized`,
                                                NOT_FOUND: `response-to-${_key}-not-found`,
                                                CONFLICT: `response-to-${_key}-conflict`,
                                                ERROR: `response-to-${key}-error`
                                            };
                                            return responseToEventMap;
                                        }, {})
                                    };
                                } else {
                                    common.log(`error`, `Event.create - Input 'request-for' event keys are invalid.`);
                                }
                            }
                            return _outputEventMap;
                        }, {});
                        return Object.freeze(outputEventMap);
                    }
                }
            },
            /* set store, interface, domain, and service factory namespaces */
            Domain: DomainFactory,
            Store: StoreFactory,
            Interface: InterfaceFactory,
            Service: ServiceFactory,
            /* set app factory namespace */
            App: AppFactory,
            /* set test agent & fixtures factory namespace */
            Agent: AgentFactory,
            // Fixture: FixtureFactory,
            /* set state composite factory namespace */
            State: {
                ReducerComposite: StateReducerComposite,
                TimeTraversalComposite: StateTimeTraversalComposite
            },
            /* set test fixtures composites namespace */
            // Test: {
            //     TapeRunnerComposite: TapeTestRunnerComposite,
            //     DomainFixtureComposite: DomainTestFixtureComposite,
            //     StoreFixtureComposite: StoreTestFixtureComposite,
            //     InterfaceFixtureComposite: InterfaceTestFixtureComposite,
            //     ServiceFixtureComposite: ServiceTestFixtureComposite
            // },
            /* set composite library namespace */
            React: {
                ComponentComposite: ReactComponentComposite,
                AppComponentComposite: (() => {
                    switch (TARGET) { // eslint-disable-line
                    case `client-native`:
                        return ReactClientNativeAppComponentComposite;
                    case `client-web`:
                        return ReactClientWebAppComponentComposite;
                    case `server`:
                        return ReactServerAppComponentComposite;
                    }
                })(),
                AppRendererComposite: (() => {
                    switch (TARGET) { // eslint-disable-line
                    case `client-native`:
                        return ReactClientNativeAppRendererComposite;
                    case `client-web`:
                        return ReactClientWebAppRendererComposite;
                    case `server`:
                        return ReactServerAppRendererComposite;
                    }
                })()
            },
            Storage: (() => {
                switch (TARGET) { // eslint-disable-line
                case `client-native`:
                    return {
                        ASyncStorageComposite
                    };
                case `client-web`:
                    return {
                        WebStorageComposite
                    };
                case `server`:
                    return {
                        PGComposite
                    };
                }
            })()
        };

        /* create an initialized Hflow object */
        Hflow = Object.freeze(common.mix(common, {
            exclusion: {
                /* hide reveal method, reveal is for internal use only */
                keys: [
                    `reveal`
                ]
            }
        }).with(HflowProperty));
    }
    return Hflow;
};

export {
    Hflow,
    init
};
<|endoftext|>


<commit_before>/*global Ember*/
/*global DS*/
(function () {
  'use strict';

  DS.LSSerializer = DS.JSONSerializer.extend({

    serializeHasMany: function(record, json, relationship) {
      var key = relationship.key;
      var payloadKey = this.keyForRelationship ? this.keyForRelationship(key, "hasMany") : key;
      var relationshipType = DS.RelationshipChange.determineRelationshipType(record.constructor, relationship);

      if (relationshipType === 'manyToNone' ||
          relationshipType === 'manyToMany' ||
          relationshipType === 'manyToOne') {
        json[payloadKey] = record.get(key).mapBy('id');
        // TODO support for polymorphic manyToNone and manyToMany relationships
      }
    },

    /**
     * Extracts whatever was returned from the adapter.
     *
     * If the adapter returns relationships in an embedded way, such as follows:
     *
     * ```js
     * {
     *   "id": 1,
     *   "title": "Rails Rambo",
     *
     *   "_embedded": {
     *     "comment": [{
     *       "id": 1,
     *       "comment_title": "FIRST"
     *     }, {
     *       "id": 2,
     *       "comment_title": "Rails is unagi"
     *     }]
     *   }
     * }
     *
     * this method will create separated JSON for each resource and then push
     * them individually to the Store.
     *
     * In the end, only the main resource will remain, containing the ids of its
     * relationships. Given the relations are already in the Store, we will
     * return a JSON with the main resource alone. The Store will sort out the
     * associations by itself.
     *
     * @method extractSingle
     * @private
     * @param {DS.Store} store the returned store
     * @param {DS.Model} type the type/model
     * @param {Object} payload returned JSON
     */
    extractSingle: function(store, type, payload) {
      if (payload && payload._embedded) {
        for (var relation in payload._embedded) {
          var relType = type.typeForRelationship(relation);
          var typeName = relType.typeKey,
              embeddedPayload = payload._embedded[relation];

          if (embeddedPayload) {
            if (Ember.isArray(embeddedPayload)) {
              store.pushMany(typeName, embeddedPayload);
            } else {
              store.push(typeName, embeddedPayload);
            }
          }
        }

        delete payload._embedded;
      }

      return this.normalize(type, payload);
    },

    /**
     * This is exactly the same as extractSingle, but used in an array.
     *
     * @method extractSingle
     * @private
     * @param {DS.Store} store the returned store
     * @param {DS.Model} type the type/model
     * @param {Array} payload returned JSONs
     */
    extractArray: function(store, type, payload) {
      return payload.map(function(json) {
        return this.extractSingle(store, type, json);
      }, this);
    }

  });

  DS.LSAdapter = DS.Adapter.extend(Ember.Evented, {
    /**
      This is the main entry point into finding records. The first parameter to
      this method is the model's name as a string.

      @method find
      @param {DS.Model} type
      @param {Object|String|Integer|null} id
      */
    find: function(store, type, id, opts) {
      var allowRecursive = true;
      var namespace = this._namespaceForType(type);
      var record = Ember.A(namespace.records[id]);

      /**
       * In the case where there are relationships, this method is called again
       * for each relation. Given the relations have references to the main
       * object, we use allowRecursive to avoid going further into infinite
       * recursiveness.
       *
       * Concept from ember-indexdb-adapter
       */
      if (opts && typeof opts.allowRecursive !== 'undefined') {
        allowRecursive = opts.allowRecursive;
      }

      if (!record || !record.hasOwnProperty('id')) {
        return Ember.RSVP.reject(new Error("Couldn't find record of"
                                           + " type '" + type.typeKey
                                           + "' for the id '" + id + "'."));
      }

      if (allowRecursive) {
        return this.loadRelationships(type, record);
      } else {
        return Ember.RSVP.resolve(record);
      }
    },

    findMany: function (store, type, ids, opts) {
      var namespace = this._namespaceForType(type);
      var adapter = this,
          allowRecursive = true,
          results = [], record;

      /**
       * In the case where there are relationships, this method is called again
       * for each relation. Given the relations have references to the main
       * object, we use allowRecursive to avoid going further into infinite
       * recursiveness.
       *
       * Concept from ember-indexdb-adapter
       */
      if (opts && typeof opts.allowRecursive !== 'undefined') {
        allowRecursive = opts.allowRecursive;
      }

      for (var i = 0; i < ids.length; i++) {
        record = namespace.records[ids[i]];
        if (!record || !record.hasOwnProperty('id')) {
          return Ember.RSVP.reject(new Error("Couldn't find record of type '" + type.typeKey
                                             + "' for the id '" + ids[i] + "'."));
        }
        results.push(Ember.copy(record));
      }

      if (results.get('length') && allowRecursive) {
        return this.loadRelationshipsForMany(type, results);
      } else {
        return Ember.RSVP.resolve(results);
      }
    },

    // Supports queries that look like this:
    //
    //   {
    //     <property to query>: <value or regex (for strings) to match>,
    //     ...
    //   }
    //
    // Every property added to the query is an "AND" query, not "OR"
    //
    // Example:
    //
    //  match records with "complete: true" and the name "foo" or "bar"
    //
    //    { complete: true, name: /foo|bar/ }
    findQuery: function (store, type, query, recordArray) {
      var namespace = this._namespaceForType(type);
      var results = this.query(namespace.records, query);

      if (results.get('length')) {
        return this.loadRelationshipsForMany(type, results);
      } else {
        return Ember.RSVP.reject();
      }
    },

    query: function (records, query) {
      var results = [], record;

      function recordMatchesQuery(record) {
        return Ember.keys(query).every(function(property) {
          var test = query[property];
          if (Object.prototype.toString.call(test) === '[object RegExp]') {
            return test.test(record[property]);
          } else {
            return record[property] === test;
          }
        });
      }

      for (var id in records) {
        record = records[id];
        if (recordMatchesQuery(record)) {
          results.push(Ember.copy(record));
        }
      }
      return results;
    },

    findAll: function (store, type) {
      var namespace = this._namespaceForType(type),
          results = [];

      for (var id in namespace.records) {
        results.push(Ember.copy(namespace.records[id]));
      }
      return Ember.RSVP.resolve(results);
    },

    createRecord: function (store, type, record) {
      var namespaceRecords = this._namespaceForType(type);
      var recordHash = record.serialize({includeId: true});

      namespaceRecords.records[recordHash.id] = recordHash;

      this.persistData(type, namespaceRecords);
      return Ember.RSVP.resolve();
    },

    updateRecord: function (store, type, record) {
      var namespaceRecords = this._namespaceForType(type);
      var id = record.get('id');

      namespaceRecords.records[id] = record.serialize({ includeId: true });

      this.persistData(type, namespaceRecords);
      return Ember.RSVP.resolve();
    },

    deleteRecord: function (store, type, record) {
      var namespaceRecords = this._namespaceForType(type);
      var id = record.get('id');

      delete namespaceRecords.records[id];

      this.persistData(type, namespaceRecords);
      return Ember.RSVP.resolve();
    },

    generateIdForRecord: function () {
      return Math.random().toString(32).slice(2).substr(0, 5);
    },

    // private

    adapterNamespace: function () {
      return this.get('namespace') || 'DS.LSAdapter';
    },

    loadData: function () {
      var storage = localStorage.getItem(this.adapterNamespace());
      return storage ? JSON.parse(storage) : {};
    },

    persistData: function(type, data) {
      var modelNamespace = this.modelNamespace(type);
      var localStorageData = this.loadData();

      localStorageData[modelNamespace] = data;

      localStorage.setItem(this.adapterNamespace(), JSON.stringify(localStorageData));
    },

    _namespaceForType: function (type) {
      var namespace = this.modelNamespace(type);
      var storage   = this.loadData();

      return storage[namespace] || {records: {}};
    },

    modelNamespace: function(type) {
      return type.url || type.typeKey;
    },

    /**
     * This takes a record, then analyzes the model relationships and replaces
     * ids with the actual values.
     *
     * Stolen from ember-indexdb-adapter
     *
     * Consider the following JSON is entered:
     *
     * ```js
     * {
     *   "id": 1,
     *   "title": "Rails Rambo",
     *   "comments": [1, 2]
     * }
     *
     * This will return:
     *
     * ```js
     * {
     *   "id": 1,
     *   "title": "Rails Rambo",
     *   "comments": [1, 2]
     *
     *   "_embedded": {
     *     "comment": [{
     *       "_id": 1,
     *       "comment_title": "FIRST"
     *     }, {
     *       "_id": 2,
     *       "comment_title": "Rails is unagi"
     *     }]
     *   }
     * }
     *
     * This way, whenever a resource returned, its relationships will be also
     * returned.
     *
     * @method loadRelationships
     * @private
     * @param {DS.Model} type
     * @param {Object} record
     */
    loadRelationships: function(type, record) {
      var adapter = this,
          resultJSON = {},
          typeKey = type.typeKey,
          relationshipNames, relationships,
          relationshipPromises = [];

      /**
       * Create a chain of promises, so the relationships are
       * loaded sequentially.  Think of the variable
       * `recordPromise` as of the accumulator in a left fold.
       */
      var recordPromise = Ember.RSVP.resolve(record);

      relationshipNames = Ember.get(type, 'relationshipNames');
      relationships = relationshipNames.belongsTo
        .concat(relationshipNames.hasMany);

      relationships.forEach(function(relationName) {
        var relationModel = type.typeForRelationship(relationName);
        var relationEmbeddedId = record[relationName];
        var relationProp  = adapter.relationshipProperties(type, relationName);
        var relationType  = relationProp.kind;
        var foreignAdapter = type.store.adapterFor(relationModel);

        var opts = {allowRecursive: false};

        /**
         * embeddedIds are ids of relations that are included in the main
         * payload, such as:
         *
         * {
         *    cart: {
         *      id: "s85fb",
         *      customer: "rld9u"
         *    }
         * }
         *
         * In this case, cart belongsTo customer and its id is present in the
         * main payload. We find each of these records and add them to _embedded.
         */
        if (relationEmbeddedId && foreignAdapter === adapter)
        {
          recordPromise = recordPromise.then(function(recordPayload) {
            var promise;
            if (relationType === 'belongsTo' || relationType === 'hasOne') {
              promise = adapter.find(null, relationModel, relationEmbeddedId, opts);
            } else if (relationType == 'hasMany') {
              promise = adapter.findMany(null, relationModel, relationEmbeddedId, opts);
            }

            return promise.then(function(relationRecord) {
              return adapter.addEmbeddedPayload(recordPayload, relationName, relationRecord);
            });
          });
        }
      });

      return recordPromise;
    },

    /**
     * Given the following payload,
     *
     *   {
     *      cart: {
     *        id: "1",
     *        customer: "2"
     *      }
     *   }
     *
     * With `relationshipName` being `customer` and `relationshipRecord`
     *
     *   {id: "2", name: "Rambo"}
     *
     * This method returns the following payload:
     *
     *   {
     *      cart: {
     *        id: "1",
     *        customer: "2"
     *      },
     *      _embedded: {
     *        customer: {
     *          id: "2",
     *          name: "Rambo"
     *        }
     *      }
     *   }
     *
     * which is then treated by the serializer later.
     *
     * @method addEmbeddedPayload
     * @private
     * @param {Object} payload
     * @param {String} relationshipName
     * @param {Object} relationshipRecord
     */
    addEmbeddedPayload: function(payload, relationshipName, relationshipRecord) {
      var objectHasId = (relationshipRecord && relationshipRecord.id);
      var arrayHasIds = (relationshipRecord.length && relationshipRecord.everyBy("id"));
      var isValidRelationship = (objectHasId || arrayHasIds);

      if (isValidRelationship) {
        if (!payload['_embedded']) {
          payload['_embedded'] = {};
        }

        payload['_embedded'][relationshipName] = relationshipRecord;
        if (relationshipRecord.length) {
          payload[relationshipName] = relationshipRecord.mapBy('id');
        } else {
          payload[relationshipName] = relationshipRecord.id;
        }
      }

      if (this.isArray(payload[relationshipName])) {
        payload[relationshipName] = payload[relationshipName].filter(function(id) {
          return id;
        });
      }

      return payload;
    },

    isArray: function(value) {
      return Object.prototype.toString.call(value) === '[object Array]';
    },

    /**
     * Same as `loadRelationships`, but for an array of records.
     *
     * @method loadRelationshipsForMany
     * @private
     * @param {DS.Model} type
     * @param {Object} recordsArray
     */
    loadRelationshipsForMany: function(type, recordsArray) {
      var adapter = this,
          promise = Ember.RSVP.resolve([]);

      /**
       * Create a chain of promises, so the records are loaded sequentially.
       * Think of the variable promise as of the accumulator in a left fold.
       */
      recordsArray.forEach(function(record) {
        promise = promise.then(function(records) {
          return adapter.loadRelationships(type, record)
            .then(function(loadedRecord) {
              records.push(loadedRecord);
              return records;
            });
        });
      });

      return promise;
    },

    /**
     *
     * @method relationshipProperties
     * @private
     * @param {DS.Model} type
     * @param {String} relationName
     */
    relationshipProperties: function(type, relationName) {
      var relationships = Ember.get(type, 'relationshipsByName');
      if (relationName) {
        return relationships.get(relationName);
      } else {
        return relationships;
      }
    }
  });
}());
<commit_msg>function determineRelationshipType has moved<commit_after>/*global Ember*/
/*global DS*/
(function () {
  'use strict';

  DS.LSSerializer = DS.JSONSerializer.extend({

    serializeHasMany: function(record, json, relationship) {
      var key = relationship.key;
      var payloadKey = this.keyForRelationship ? this.keyForRelationship(key, "hasMany") : key;
      var relationshipType = record.constructor.determineRelationshipType(relationship);

      if (relationshipType === 'manyToNone' ||
          relationshipType === 'manyToMany' ||
          relationshipType === 'manyToOne') {
        json[payloadKey] = record.get(key).mapBy('id');
        // TODO support for polymorphic manyToNone and manyToMany relationships
      }
    },

    /**
     * Extracts whatever was returned from the adapter.
     *
     * If the adapter returns relationships in an embedded way, such as follows:
     *
     * ```js
     * {
     *   "id": 1,
     *   "title": "Rails Rambo",
     *
     *   "_embedded": {
     *     "comment": [{
     *       "id": 1,
     *       "comment_title": "FIRST"
     *     }, {
     *       "id": 2,
     *       "comment_title": "Rails is unagi"
     *     }]
     *   }
     * }
     *
     * this method will create separated JSON for each resource and then push
     * them individually to the Store.
     *
     * In the end, only the main resource will remain, containing the ids of its
     * relationships. Given the relations are already in the Store, we will
     * return a JSON with the main resource alone. The Store will sort out the
     * associations by itself.
     *
     * @method extractSingle
     * @private
     * @param {DS.Store} store the returned store
     * @param {DS.Model} type the type/model
     * @param {Object} payload returned JSON
     */
    extractSingle: function(store, type, payload) {
      if (payload && payload._embedded) {
        for (var relation in payload._embedded) {
          var relType = type.typeForRelationship(relation);
          var typeName = relType.typeKey,
              embeddedPayload = payload._embedded[relation];

          if (embeddedPayload) {
            if (Ember.isArray(embeddedPayload)) {
              store.pushMany(typeName, embeddedPayload);
            } else {
              store.push(typeName, embeddedPayload);
            }
          }
        }

        delete payload._embedded;
      }

      return this.normalize(type, payload);
    },

    /**
     * This is exactly the same as extractSingle, but used in an array.
     *
     * @method extractSingle
     * @private
     * @param {DS.Store} store the returned store
     * @param {DS.Model} type the type/model
     * @param {Array} payload returned JSONs
     */
    extractArray: function(store, type, payload) {
      return payload.map(function(json) {
        return this.extractSingle(store, type, json);
      }, this);
    }

  });

  DS.LSAdapter = DS.Adapter.extend(Ember.Evented, {
    /**
      This is the main entry point into finding records. The first parameter to
      this method is the model's name as a string.

      @method find
      @param {DS.Model} type
      @param {Object|String|Integer|null} id
      */
    find: function(store, type, id, opts) {
      var allowRecursive = true;
      var namespace = this._namespaceForType(type);
      var record = Ember.A(namespace.records[id]);

      /**
       * In the case where there are relationships, this method is called again
       * for each relation. Given the relations have references to the main
       * object, we use allowRecursive to avoid going further into infinite
       * recursiveness.
       *
       * Concept from ember-indexdb-adapter
       */
      if (opts && typeof opts.allowRecursive !== 'undefined') {
        allowRecursive = opts.allowRecursive;
      }

      if (!record || !record.hasOwnProperty('id')) {
        return Ember.RSVP.reject(new Error("Couldn't find record of"
                                           + " type '" + type.typeKey
                                           + "' for the id '" + id + "'."));
      }

      if (allowRecursive) {
        return this.loadRelationships(type, record);
      } else {
        return Ember.RSVP.resolve(record);
      }
    },

    findMany: function (store, type, ids, opts) {
      var namespace = this._namespaceForType(type);
      var adapter = this,
          allowRecursive = true,
          results = [], record;

      /**
       * In the case where there are relationships, this method is called again
       * for each relation. Given the relations have references to the main
       * object, we use allowRecursive to avoid going further into infinite
       * recursiveness.
       *
       * Concept from ember-indexdb-adapter
       */
      if (opts && typeof opts.allowRecursive !== 'undefined') {
        allowRecursive = opts.allowRecursive;
      }

      for (var i = 0; i < ids.length; i++) {
        record = namespace.records[ids[i]];
        if (!record || !record.hasOwnProperty('id')) {
          return Ember.RSVP.reject(new Error("Couldn't find record of type '" + type.typeKey
                                             + "' for the id '" + ids[i] + "'."));
        }
        results.push(Ember.copy(record));
      }

      if (results.get('length') && allowRecursive) {
        return this.loadRelationshipsForMany(type, results);
      } else {
        return Ember.RSVP.resolve(results);
      }
    },

    // Supports queries that look like this:
    //
    //   {
    //     <property to query>: <value or regex (for strings) to match>,
    //     ...
    //   }
    //
    // Every property added to the query is an "AND" query, not "OR"
    //
    // Example:
    //
    //  match records with "complete: true" and the name "foo" or "bar"
    //
    //    { complete: true, name: /foo|bar/ }
    findQuery: function (store, type, query, recordArray) {
      var namespace = this._namespaceForType(type);
      var results = this.query(namespace.records, query);

      if (results.get('length')) {
        return this.loadRelationshipsForMany(type, results);
      } else {
        return Ember.RSVP.reject();
      }
    },

    query: function (records, query) {
      var results = [], record;

      function recordMatchesQuery(record) {
        return Ember.keys(query).every(function(property) {
          var test = query[property];
          if (Object.prototype.toString.call(test) === '[object RegExp]') {
            return test.test(record[property]);
          } else {
            return record[property] === test;
          }
        });
      }

      for (var id in records) {
        record = records[id];
        if (recordMatchesQuery(record)) {
          results.push(Ember.copy(record));
        }
      }
      return results;
    },

    findAll: function (store, type) {
      var namespace = this._namespaceForType(type),
          results = [];

      for (var id in namespace.records) {
        results.push(Ember.copy(namespace.records[id]));
      }
      return Ember.RSVP.resolve(results);
    },

    createRecord: function (store, type, record) {
      var namespaceRecords = this._namespaceForType(type);
      var recordHash = record.serialize({includeId: true});

      namespaceRecords.records[recordHash.id] = recordHash;

      this.persistData(type, namespaceRecords);
      return Ember.RSVP.resolve();
    },

    updateRecord: function (store, type, record) {
      var namespaceRecords = this._namespaceForType(type);
      var id = record.get('id');

      namespaceRecords.records[id] = record.serialize({ includeId: true });

      this.persistData(type, namespaceRecords);
      return Ember.RSVP.resolve();
    },

    deleteRecord: function (store, type, record) {
      var namespaceRecords = this._namespaceForType(type);
      var id = record.get('id');

      delete namespaceRecords.records[id];

      this.persistData(type, namespaceRecords);
      return Ember.RSVP.resolve();
    },

    generateIdForRecord: function () {
      return Math.random().toString(32).slice(2).substr(0, 5);
    },

    // private

    adapterNamespace: function () {
      return this.get('namespace') || 'DS.LSAdapter';
    },

    loadData: function () {
      var storage = localStorage.getItem(this.adapterNamespace());
      return storage ? JSON.parse(storage) : {};
    },

    persistData: function(type, data) {
      var modelNamespace = this.modelNamespace(type);
      var localStorageData = this.loadData();

      localStorageData[modelNamespace] = data;

      localStorage.setItem(this.adapterNamespace(), JSON.stringify(localStorageData));
    },

    _namespaceForType: function (type) {
      var namespace = this.modelNamespace(type);
      var storage   = this.loadData();

      return storage[namespace] || {records: {}};
    },

    modelNamespace: function(type) {
      return type.url || type.typeKey;
    },

    /**
     * This takes a record, then analyzes the model relationships and replaces
     * ids with the actual values.
     *
     * Stolen from ember-indexdb-adapter
     *
     * Consider the following JSON is entered:
     *
     * ```js
     * {
     *   "id": 1,
     *   "title": "Rails Rambo",
     *   "comments": [1, 2]
     * }
     *
     * This will return:
     *
     * ```js
     * {
     *   "id": 1,
     *   "title": "Rails Rambo",
     *   "comments": [1, 2]
     *
     *   "_embedded": {
     *     "comment": [{
     *       "_id": 1,
     *       "comment_title": "FIRST"
     *     }, {
     *       "_id": 2,
     *       "comment_title": "Rails is unagi"
     *     }]
     *   }
     * }
     *
     * This way, whenever a resource returned, its relationships will be also
     * returned.
     *
     * @method loadRelationships
     * @private
     * @param {DS.Model} type
     * @param {Object} record
     */
    loadRelationships: function(type, record) {
      var adapter = this,
          resultJSON = {},
          typeKey = type.typeKey,
          relationshipNames, relationships,
          relationshipPromises = [];

      /**
       * Create a chain of promises, so the relationships are
       * loaded sequentially.  Think of the variable
       * `recordPromise` as of the accumulator in a left fold.
       */
      var recordPromise = Ember.RSVP.resolve(record);

      relationshipNames = Ember.get(type, 'relationshipNames');
      relationships = relationshipNames.belongsTo
        .concat(relationshipNames.hasMany);

      relationships.forEach(function(relationName) {
        var relationModel = type.typeForRelationship(relationName);
        var relationEmbeddedId = record[relationName];
        var relationProp  = adapter.relationshipProperties(type, relationName);
        var relationType  = relationProp.kind;
        var foreignAdapter = type.store.adapterFor(relationModel);

        var opts = {allowRecursive: false};

        /**
         * embeddedIds are ids of relations that are included in the main
         * payload, such as:
         *
         * {
         *    cart: {
         *      id: "s85fb",
         *      customer: "rld9u"
         *    }
         * }
         *
         * In this case, cart belongsTo customer and its id is present in the
         * main payload. We find each of these records and add them to _embedded.
         */
        if (relationEmbeddedId && foreignAdapter === adapter)
        {
          recordPromise = recordPromise.then(function(recordPayload) {
            var promise;
            if (relationType === 'belongsTo' || relationType === 'hasOne') {
              promise = adapter.find(null, relationModel, relationEmbeddedId, opts);
            } else if (relationType == 'hasMany') {
              promise = adapter.findMany(null, relationModel, relationEmbeddedId, opts);
            }

            return promise.then(function(relationRecord) {
              return adapter.addEmbeddedPayload(recordPayload, relationName, relationRecord);
            });
          });
        }
      });

      return recordPromise;
    },

    /**
     * Given the following payload,
     *
     *   {
     *      cart: {
     *        id: "1",
     *        customer: "2"
     *      }
     *   }
     *
     * With `relationshipName` being `customer` and `relationshipRecord`
     *
     *   {id: "2", name: "Rambo"}
     *
     * This method returns the following payload:
     *
     *   {
     *      cart: {
     *        id: "1",
     *        customer: "2"
     *      },
     *      _embedded: {
     *        customer: {
     *          id: "2",
     *          name: "Rambo"
     *        }
     *      }
     *   }
     *
     * which is then treated by the serializer later.
     *
     * @method addEmbeddedPayload
     * @private
     * @param {Object} payload
     * @param {String} relationshipName
     * @param {Object} relationshipRecord
     */
    addEmbeddedPayload: function(payload, relationshipName, relationshipRecord) {
      var objectHasId = (relationshipRecord && relationshipRecord.id);
      var arrayHasIds = (relationshipRecord.length && relationshipRecord.everyBy("id"));
      var isValidRelationship = (objectHasId || arrayHasIds);

      if (isValidRelationship) {
        if (!payload['_embedded']) {
          payload['_embedded'] = {};
        }

        payload['_embedded'][relationshipName] = relationshipRecord;
        if (relationshipRecord.length) {
          payload[relationshipName] = relationshipRecord.mapBy('id');
        } else {
          payload[relationshipName] = relationshipRecord.id;
        }
      }

      if (this.isArray(payload[relationshipName])) {
        payload[relationshipName] = payload[relationshipName].filter(function(id) {
          return id;
        });
      }

      return payload;
    },

    isArray: function(value) {
      return Object.prototype.toString.call(value) === '[object Array]';
    },

    /**
     * Same as `loadRelationships`, but for an array of records.
     *
     * @method loadRelationshipsForMany
     * @private
     * @param {DS.Model} type
     * @param {Object} recordsArray
     */
    loadRelationshipsForMany: function(type, recordsArray) {
      var adapter = this,
          promise = Ember.RSVP.resolve([]);

      /**
       * Create a chain of promises, so the records are loaded sequentially.
       * Think of the variable promise as of the accumulator in a left fold.
       */
      recordsArray.forEach(function(record) {
        promise = promise.then(function(records) {
          return adapter.loadRelationships(type, record)
            .then(function(loadedRecord) {
              records.push(loadedRecord);
              return records;
            });
        });
      });

      return promise;
    },

    /**
     *
     * @method relationshipProperties
     * @private
     * @param {DS.Model} type
     * @param {String} relationName
     */
    relationshipProperties: function(type, relationName) {
      var relationships = Ember.get(type, 'relationshipsByName');
      if (relationName) {
        return relationships.get(relationName);
      } else {
        return relationships;
      }
    }
  });
}());
<|endoftext|>