diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..b5d284f
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,8 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Datasource local storage ignored files
+/../../../../../../:\Users\gomas\src\system-builder\.idea/dataSources/
+/dataSources.local.xml
+# Editor-based HTTP Client requests
+/httpRequests/
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..fb8cd6d
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/sqldialects.xml b/.idea/sqldialects.xml
new file mode 100644
index 0000000..63772a3
--- /dev/null
+++ b/.idea/sqldialects.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/system-builder.iml b/.idea/system-builder.iml
new file mode 100644
index 0000000..5e764c4
--- /dev/null
+++ b/.idea/system-builder.iml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..94a25f7
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/dist/example-def.js b/dist/example-def.js
deleted file mode 100644
index 117c302..0000000
--- a/dist/example-def.js
+++ /dev/null
@@ -1,142 +0,0 @@
-"use strict";
-Object.defineProperty(exports, "__esModule", { value: true });
-var def = {
- name: 'Todo',
- storage: {
- tables: [
- {
- name: "task",
- relations: [
- {
- type: 'belongs-to',
- table: 'list',
- }
- ],
- columns: [
- {
- name: "name",
- type: "string",
- nullable: false,
- },
- {
- name: "description",
- type: "string",
- nullable: true,
- },
- {
- name: "completed",
- type: "boolean",
- nullable: false,
- },
- {
- name: "completed_date",
- type: "date",
- nullable: true,
- },
- {
- name: "metadata",
- type: "blob",
- nullable: true,
- }
- ]
- },
- {
- name: "list",
- relations: [],
- columns: [
- {
- name: "name",
- type: "string",
- unique: false,
- nullable: false,
- },
- ]
- },
- {
- name: "user",
- relations: [],
- columns: [
- {
- name: "name",
- type: "string",
- nullable: false,
- },
- {
- name: "password",
- type: "string",
- nullable: false,
- }
- ]
- }
- ],
- relations: [
- {
- left: 'list',
- relation: 'many-to-many',
- right: 'user',
- columns: [
- {
- name: 'access',
- type: 'string',
- unique: false,
- nullable: false,
- }
- ]
- }
- ]
- },
- views: [
- {
- component: 'task',
- type: ['list', 'count'],
- columns: [
- 'name',
- 'description',
- 'completed',
- 'completed_date',
- ],
- orderBy: [
- {
- column: 'modified',
- direction: 'desc'
- }
- ],
- filters: [
- {
- param: 'list',
- column: 'list_id',
- comparison: '=',
- required: true
- },
- {
- param: 'completed',
- column: 'completed',
- comparison: '=',
- required: false
- },
- ]
- },
- {
- component: 'list',
- type: ['list', 'count'],
- columns: [
- 'name',
- ],
- orderBy: [
- {
- column: 'modified',
- direction: 'desc'
- }
- ],
- filters: [
- {
- param: 'user',
- column: 'user_id',
- comparison: '=',
- required: true
- },
- ]
- },
- ]
-};
-exports.default = def;
diff --git a/package.json b/package.json
index 4b2623a..ec0e0d4 100644
--- a/package.json
+++ b/package.json
@@ -1,8 +1,10 @@
{
"dependencies": {
"@types/node": "^14.0.13",
+ "@types/pluralize": "^0.0.29",
"ncp": "^2.0.0",
- "path": "^0.12.7"
+ "path": "^0.12.7",
+ "pluralize": "^8.0.0"
},
"scripts": {
"start": "rm -rf dist && tsc && cp -r frame dist/ && cd dist && node index.js"
diff --git a/src/database/database-creator.ts b/src/database/database-creator.ts
index 43c4a00..33ef7c1 100644
--- a/src/database/database-creator.ts
+++ b/src/database/database-creator.ts
@@ -3,7 +3,7 @@ import * as fs from 'fs';
import {
SystemDef,
StorageDef,
- ViewDef,
+ EndpointDef,
Order,
Filter,
TableDef,
diff --git a/src/example-def.ts b/src/example-def.ts
index 542e53e..f663d36 100644
--- a/src/example-def.ts
+++ b/src/example-def.ts
@@ -27,6 +27,7 @@ let def: SystemDef = {
name: "completed",
type: "boolean",
nullable: false,
+ default: 'false'
},
{
name: "completed_date",
@@ -85,59 +86,197 @@ let def: SystemDef = {
}
]
},
- views: [
+ components: [
{
component: 'task',
- type: ['list', 'count'],
- columns: [
- 'name',
- 'description',
- 'completed',
- 'completed_date',
- ],
- orderBy: [
+ endpoints: [
{
- column: 'modified',
- direction: 'desc'
+ component: 'task',
+ table: 'task',
+ type: 'list',
+ columns: [
+ {
+ name: 'name',
+ table: 'task'
+ },
+ {
+ name: 'description',
+ table: 'task'
+ },
+ {
+ name: 'completed',
+ table: 'task'
+ },
+ {
+ name: 'completed_date',
+ table: 'task'
+ },
+ ],
+ orderBy: [
+ {
+ column: {
+ name: 'modified',
+ table: 'task'
+ },
+ direction: 'desc'
+ }
+ ],
+ filters: [
+ {
+ param: 'list',
+ column: {
+ name: 'list_id',
+ table: 'task'
+ },
+ comparison: '=',
+ required: true
+ },
+ {
+ param: 'completed',
+ column: {
+ name: 'completed',
+ table: 'task'
+ },
+ comparison: '=',
+ required: false
+ },
+ ]
+ },
+ {
+ component: 'task',
+ table: 'task',
+ type: 'count',
+ columns: [
+ {
+ name: 'name',
+ table: 'task'
+ },
+ {
+ name: 'description',
+ table: 'task'
+ },
+ {
+ name: 'completed',
+ table: 'task'
+ },
+ {
+ name: 'completed_date',
+ table: 'task'
+ },
+ ],
+ orderBy: [
+ {
+ column: {
+ name: 'modified',
+ table: 'task'
+ },
+ direction: 'desc'
+ }
+ ],
+ filters: [
+ {
+ param: 'list',
+ column: {
+ name: 'list_id',
+ table: 'task'
+ },
+ comparison: '=',
+ required: true
+ },
+ {
+ param: 'completed',
+ column: {
+ name: 'completed',
+ table: 'task'
+ },
+ comparison: '=',
+ required: false
+ },
+ ]
+ },
+ {
+ component: 'task',
+ table: 'task',
+ type: 'create',
+ columns: [
+ {
+ name: 'name',
+ table: 'task',
+ param: 'name',
+ required: true
+ },
+ {
+ name: 'list_id',
+ table: 'task',
+ param: 'list',
+ required: true
+ },
+ {
+ name: 'description',
+ table: 'task',
+ param: 'description'
+ },
+ {
+ name: 'completed',
+ table: 'task',
+ param: 'completed'
+ }
+ ],
}
- ],
- filters: [
- {
- param: 'list',
- column: 'list_id',
- comparison: '=',
- required: true
- },
- {
- param: 'completed',
- column: 'completed',
- comparison: '=',
- required: false
- },
]
},
{
component: 'list',
- type: ['list', 'count'],
- columns: [
- 'name',
- ],
- orderBy: [
+ endpoints: [
{
- column: 'modified',
- direction: 'desc'
- }
- ],
- filters: [
- {
- param: 'user',
- column: 'user_id',
- comparison: '=',
- required: true
+ component: 'list',
+ table: 'list',
+ type: 'list',
+ columns: [
+ {
+ name: 'name',
+ table: 'list'
+ },
+ ],
+ join: [
+ {
+ table: 'list_user',
+ on: {
+ left: {
+ name: 'list_id',
+ table: 'list'
+ },
+ right: {
+ name: 'list_id',
+ table: 'list_user'
+ }
+ }
+ }
+ ],
+ orderBy: [
+ {
+ column: {
+ name: 'modified',
+ table: 'list'
+ },
+ direction: 'desc'
+ }
+ ],
+ filters: [
+ {
+ param: 'user',
+ column: {
+ name: 'user_id',
+ table: 'list_user'
+ },
+ comparison: '=',
+ required: true
+ },
+ ]
},
]
- },
- ]
-}
+ }
+ ],
+};
export default def;
diff --git a/src/systemGenService.ts b/src/systemGenService.ts
index a54ae17..4a8d3f0 100644
--- a/src/systemGenService.ts
+++ b/src/systemGenService.ts
@@ -33,7 +33,7 @@ const systemGenService = new SystemGenService();
interface SystemDef {
name: string;
storage: StorageDef;
- views: ViewDef[];
+ components: ComponentDef[];
// TODO: add Views, ACLs, Behaviors, UX
}
@@ -42,25 +42,32 @@ interface StorageDef {
relations: ManyToManyDef[];
}
-interface ViewDef {
+interface ComponentDef {
component: string;
- type: ('list' | 'count' | 'item' | 'distinct')[];
- columns: string[];
+ endpoints: EndpointDef[];
+}
+
+interface EndpointDef {
+ component: string;
+ table: string;
+ type: ('list' | 'count' | 'item' | 'distinct' | 'update' | 'create' | 'delete');
+ columns: ColumnRef[];
+ values?: ValueDef[];
+ join?: JoinDef[];
orderBy?: Order[];
filters?: Filter[];
// if type is 'list' it will always include skip and limit for pagination
}
interface Order {
- column: string;
+ column: ColumnRef;
direction: 'asc' | 'desc';
}
interface Filter {
param: string; // the query param used to get the value
- column: string;
+ column: ColumnRef;
comparison: '=' | '!=' | '>' | '<' | 'contains';
- value?: string;
required?: boolean;
}
@@ -79,6 +86,26 @@ interface ColumnDef {
default?: string;
}
+interface ColumnRef {
+ name: string;
+ table: string;
+ param?: string;
+ required?: boolean;
+}
+
+interface ValueDef {
+ column: string;
+ required: boolean;
+}
+
+interface JoinDef {
+ table: string;
+ on: {
+ left: ColumnRef;
+ right: ColumnRef;
+ }
+}
+
interface BelongsToDef {
type: 'belongs-to';
table: string;
@@ -92,14 +119,17 @@ interface ManyToManyDef {
}
export {
- SystemDef,
- StorageDef,
- ViewDef,
- Order,
- Filter,
- TableDef,
- ColumnDef,
BelongsToDef,
+ ColumnDef,
+ ColumnRef,
+ ComponentDef,
+ EndpointDef,
+ Filter,
+ JoinDef,
ManyToManyDef,
+ Order,
+ StorageDef,
+ SystemDef,
+ TableDef,
systemGenService
}
diff --git a/src/views/views-creator.ts b/src/views/views-creator.ts
index 2fa26e2..c59e8a7 100644
--- a/src/views/views-creator.ts
+++ b/src/views/views-creator.ts
@@ -1,35 +1,44 @@
import {
- SystemDef,
- ViewDef
+ BelongsToDef,
+ ColumnDef,
+ Filter, JoinDef, Order,
+ SystemDef, TableDef,
+ EndpointDef, ComponentDef, ColumnRef
} from '../systemGenService';
import * as path from 'path';
import * as fs from 'fs';
+import pluralize from 'pluralize';
const ncp = require('ncp').ncp;
export function createViews(systemDef: SystemDef) {
let viewsPromises = [];
- for (let i in systemDef.views) {
- viewsPromises.push(createComponent(systemDef.views[i], systemDef.name));
+ for (let i in systemDef.components) {
+ viewsPromises.push(createComponent(systemDef.components[i], systemDef));
}
return Promise.all(viewsPromises);
}
-function createComponent(view: ViewDef, outDir: string) {
- let componentPromise = new Promise((componentResolve, componentReject) => {
- ncp(path.join(process.cwd(), 'frame', 'src', 'components', '{{component}}'), path.join(process.cwd(), outDir, 'src', 'components', view.component), (err: any) => {
+function createComponent(component: ComponentDef, systemDef: SystemDef) {
+ let componentPromise = new Promise((componentResolve, componentReject) => {
+ ncp(path.join(process.cwd(), 'frame', 'src', 'components', '{{component}}'), path.join(process.cwd(), systemDef.name, 'src', 'components', component.component), (err: any) => {
if (err) {
console.log(err);
} else {
- let mapperPromise = initializeComponentFile(path.join(process.cwd(), outDir, 'src', 'components', view.component, '{{component}}Mapper.ets'), path.join(process.cwd(), outDir, 'src', 'components', view.component, `${view.component}Mapper.ts`), view.component, outDir);
- let routesPromise = initializeComponentFile(path.join(process.cwd(), outDir, 'src', 'components', view.component, '{{component}}Routes.ets'), path.join(process.cwd(), outDir, 'src', 'components', view.component, `${view.component}Routes.ts`), view.component, outDir);
- addInitializeRoutesCode(view.component, outDir);
- let serviceFileLocation: string = path.join(process.cwd(), outDir, 'src', 'components', view.component, `${view.component}Service.ts`);
- let servicePromise = initializeComponentFile(path.join(process.cwd(), outDir, 'src', 'components', view.component, '{{component}}Service.ets'), serviceFileLocation, view.component, outDir);
- addInitializeServiceCode(view.component, outDir);
+ let mapperPromise = initializeComponentFile(path.join(process.cwd(), systemDef.name, 'src', 'components', component.component, '{{component}}Mapper.ets'), path.join(process.cwd(), systemDef.name, 'src', 'components', component.component, `${component.component}Mapper.ts`), component.component, systemDef.name);
+ for (let i in component.endpoints) {
+ mapperPromise = mapperPromise.then(() => {
+ return insertMapperCode(component.endpoints[i], systemDef.name);
+ });
+ }
+ let routesPromise = initializeComponentFile(path.join(process.cwd(), systemDef.name, 'src', 'components', component.component, '{{component}}Routes.ets'), path.join(process.cwd(), systemDef.name, 'src', 'components', component.component, `${component.component}Routes.ts`), component.component, systemDef.name);
+ addInitializeRoutesCode(component.component, systemDef.name);
+ let serviceFileLocation: string = path.join(process.cwd(), systemDef.name, 'src', 'components', component.component, `${component.component}Service.ts`);
+ let servicePromise = initializeComponentFile(path.join(process.cwd(), systemDef.name, 'src', 'components', component.component, '{{component}}Service.ets'), serviceFileLocation, component.component, systemDef.name);
+ addInitializeServiceCode(component.component, systemDef.name);
Promise.all([mapperPromise, routesPromise, servicePromise]).then(() => {
- console.log(`success creating component ${view.component}`);
+ console.log(`success creating component ${component.component}`);
componentResolve();
});
}
@@ -39,7 +48,7 @@ function createComponent(view: ViewDef, outDir: string) {
}
function initializeComponentFile(sourceFile: string, destinationFile: string, component: string, outDir: string) {
- let filePromise = new Promise((resolve, reject) => {
+ let filePromise = new Promise((resolve, reject) => {
fs.rename(sourceFile, destinationFile, (err: any) => {
let fileContents = fs.readFileSync(destinationFile, 'utf8');
var uppercaseFirstLetterComponentName = uppercaseFirstLetter(component);
@@ -81,6 +90,207 @@ function addInitializeRoutesCode(component: string, outDir: string) {
fs.writeFileSync(fileLocation, newRoutesFile, 'utf8');
}
+function insertMapperCode(view: EndpointDef, outDir: string): Promise {
+ const filePromise: Promise = new Promise((resolve) => {
+ const separator: string = `// SYSTEM-BUILDER-${view.component}-mapper`;
+ let fileLocation = path.join(process.cwd(), outDir, 'src', 'components', view.component, `${view.component}Mapper.ts`);
+ let initMapperFile: string = fs.readFileSync(fileLocation, 'utf8');
+ let parts = initMapperFile.split(separator);
+ // TODO: add mapper code here
+ parts[0] = parts[0] + createMapperFunc(view);
+ let newMapperFile = parts.join(separator);
+ fs.writeFileSync(fileLocation, newMapperFile, 'utf8');
+ resolve();
+ });
+ return filePromise;
+}
+
+function createMapperFunc(view: EndpointDef): string {
+ let func: string = '';
+ let funcName: string = '';
+ if (view.type.indexOf('list') !== -1 || view.type.indexOf('count') !== -1) {
+ func = buildGetList(view);
+ } else if (view.type.indexOf('item') !== -1) {
+ funcName = `get${uppercaseFirstLetter(view.component)}`;
+ } else if (view.type.indexOf('update') !== -1) {
+ funcName = `update${uppercaseFirstLetter(view.component)}`;
+ } else if (view.type.indexOf('create') !== -1) {
+ func = buildCreateFunc(view);
+ }
+ return func;
+}
+
+function buildCreateFunc(view: EndpointDef): string {
+ let func: string = '';
+ let funcName = `create${uppercaseFirstLetter(view.component)}`;
+ let query: string = `INSERT INTO ${view.table} SET `;
+ let requiredQueryValues: string = '';
+ let optionalValuesCode: string = '';
+ for (let i in view.columns) {
+ let c: ColumnRef = view.columns[i];
+ if (c.required) {
+ if (!query.endsWith('SET ')) {
+ query = query + `, `;
+ }
+ query = query + `${c.table}.${c.name} = ?`;
+ if (requiredQueryValues.length) {
+ requiredQueryValues = requiredQueryValues + `, `;
+ }
+ requiredQueryValues = requiredQueryValues + `params.${c.param}`;
+ } else {
+ optionalValuesCode = optionalValuesCode + `
+ if (params.${c.param}) {
+ query = query + ', ${c.table}.${c.name} = ?';
+ queryValues.push(params.${c.param});
+ }
+`;
+ }
+ }
+
+ func = `
+ public ${funcName}(params: {[key: string]: string}) {
+
+ let query: string = '${query}';
+ let queryValues: string[] = [${requiredQueryValues}];
+
+ // optional params?
+ ${optionalValuesCode}
+ return super.runQuery(query, [...queryValues]);
+ }
+`;
+
+ return func;
+}
+
+function buildGetList(view: EndpointDef): string {
+ // TODO: optionally allow a raw query definition with predefined params
+ // This would be so much simpler and easy if it were just the queries wanted
+ let func: string = '';
+ let funcName: string = '';
+ let columns: string = view.type === 'count' ? `count(*)` : `${view.component}.${view.component}_id, ${view.columns.map(c => `${c.table}.${c.name}`).join(', ')}`;
+ let queryValues: string = '';
+ let query: string = '';
+ let tables: string = view.join?.length ? buildJoin(view) : `${view.table}`;
+ let filters: string = '';
+ funcName = `get${pluralize(uppercaseFirstLetter(view.component))}` + (view.type === 'count' ? 'Count' : '');
+
+ let requiredFilterStrings = [];
+ let requiredQueryValues = '';
+ let optionalFilterParts = '';
+ if (view.filters) {
+ for (let i in view.filters) {
+ let f: Filter = view.filters[i];
+ if (f.required) {
+ requiredFilterStrings.push(`${f.column.table}.${f.column.name} ${f.comparison} ?`); // e.g. 'table.id = ?'
+ if (requiredQueryValues.length) {
+ requiredQueryValues = requiredQueryValues + `, `;
+ }
+ requiredQueryValues = requiredQueryValues + `params.${f.param}`;
+ } else {
+ /* e.g.
+ if (eventLog.app) {
+ query = query + ', app = ?';
+ queryValues.push(eventLog.app);
+ }
+ */
+ optionalFilterParts = optionalFilterParts + `
+ if (params.${f.param}) {
+ query = query + ' AND ${f.column.table}.${f.column.name} ${f.comparison} ?';
+ queryValues.push(params.${f.param});
+ }`;
+ }
+ }
+ filters = ` WHERE ${requiredFilterStrings.join(' AND ')}`;
+ }
+
+ query = `SELECT ${columns} FROM ${tables}${filters}`;
+ let orderByCode = '';
+ if (view.orderBy) {
+ orderByCode = `
+ query = query + '${buildOrderBy(view)}';
+`;
+ }
+ let funcParams = 'params: {[key: string]: string}, offset: number, limit: number';
+ let limitOrderCode = `query = query + ' LIMIT ?, ?';
+ queryValues.push(offset.toString(10));
+ queryValues.push(limit.toString(10));
+ ${orderByCode}`;
+ if (view.type === 'count') {
+ funcParams = 'params: {[key: string]: string}';
+ limitOrderCode = ``;
+ }
+
+ func = `
+ public ${funcName}(${funcParams}) {
+
+ let query: string = '${query}';
+ let queryValues: string[] = [${requiredQueryValues}];
+
+ // optional params?
+ ${optionalFilterParts}
+ ${limitOrderCode}
+ return super.runQuery(query, [...queryValues]);
+ }
+`;
+ return func;
+}
+
+function buildJoin(view: EndpointDef): string {
+ if (view.join?.length) {
+ let join = `${view.table}`;
+ for (let i in view.join) {
+ let j: JoinDef = view.join[i];
+ join = join + ` JOIN ${j.table} ON ${j.on.left.table}.${j.on.left.name} = ${j.on.right.table}.${j.on.right.name}`;
+ }
+ return join;
+ }
+ return '';
+}
+
+function buildOrderBy(view: EndpointDef): string {
+ let orderBy: string = '';
+ if (view.orderBy?.length) {
+ orderBy = orderBy + ' ORDER BY ';
+ for (let i in view.orderBy) {
+ let order: Order = view.orderBy[i];
+ if (parseInt(i) !== 0) {
+ orderBy = orderBy + ', ';
+ }
+ orderBy = orderBy + `${order.column.table}.${order.column.name} ${order.direction}`;
+ }
+ }
+ return orderBy;
+}
+
+function tableHasColumn(tableName: string, column: string, sysDef: SystemDef): boolean {
+ let answer: boolean = false;
+ for (let i in sysDef.storage.tables) {
+ let table: TableDef = sysDef.storage.tables[i];
+ if (table.name === tableName) {
+ for (let j in table.columns) {
+ let c: ColumnDef = table.columns[j];
+ if (column === c.name) {
+ answer = true;
+ break;
+ }
+ }
+ if (!answer && table.relations.length) {
+ for (let k in table.columns) {
+ let r: BelongsToDef = table.relations[k];
+ if (column === `${r.table}_id`) {
+ answer = true;
+ break;
+ }
+ }
+ }
+ }
+ if (answer) {
+ break;
+ }
+ }
+ return answer;
+}
+
function uppercaseFirstLetter(input: string)
{
return input.charAt(0).toUpperCase() + input.slice(1);
diff --git a/ui-component-plan.md b/ui-component-plan.md
new file mode 100644
index 0000000..02e92a4
--- /dev/null
+++ b/ui-component-plan.md
@@ -0,0 +1,37 @@
+# System Builder
+
+System builder is a full stack builder for apps that allows you to define your data backend, APIs and views of the data and design your own apps or webpages
+
+
+### UI Components
+
+All UI components need to be able to link into any data/api that has been fetched.
+
+* Text
+* Image
+* Input (all types) plus Date Picker and Dropdown, checkbox, radio, text, textarea, email, password
+* Markdown editor: https://simplemde.com/ plus https://github.com/showdownjs/showdown
+* Table (with actions per item, maybe buttons on the list item)
+* List (with actions per item, maybe buttons on the list item)
+* Button (with text and actions)
+* charts?
+* div? <- probably just a 'section' that can contain other components
+* Loading style/animation?
+
+#### Things to keep track of
+
+There will have to be stuff we keep track of in order to reproduce the page as the user created it.
+
+* All styles -> possibly apply css dynamically this way: https://stackoverflow.com/questions/1720320/how-to-dynamically-create-css-class-in-javascript-and-apply
+* Location in the document (could be hard coded but makes it hard for responsive design)
+* class and id names for reference in styles and events
+* accessibility text and other accessibility info
+
+#### Other things
+
+* Upload images to use in styles for backgrounds or elements
+* Save text and such in the database separate from the data source
+
+#### How data sources will work
+
+Many data sources can be defined or connected, but the default will be the app that is created in the system builder UI. Each view gets created with a unique name. All components can pull data from a view by referencing the view's name plus property or index. Data sources will all be pulled using the RXJS pattern used in bx-console and referenced by their unique names. Update calls will allow you to define a list of get requests to refresh after the update calls are made. Each view will have some state stored on it in addition to the data, including loading, and error state/messages.
diff --git a/yarn.lock b/yarn.lock
index 0f70e19..9bad5cf 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -7,6 +7,11 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.0.13.tgz#ee1128e881b874c371374c1f72201893616417c9"
integrity sha512-rouEWBImiRaSJsVA+ITTFM6ZxibuAlTuNOCyxVbwreu6k6+ujs7DfnU9o+PShFhET78pMBl3eH+AGSI5eOTkPA==
+"@types/pluralize@^0.0.29":
+ version "0.0.29"
+ resolved "https://registry.yarnpkg.com/@types/pluralize/-/pluralize-0.0.29.tgz#6ffa33ed1fc8813c469b859681d09707eb40d03c"
+ integrity sha512-BYOID+l2Aco2nBik+iYS4SZX0Lf20KPILP5RGmM1IgzdwNdTs0eebiFriOPcej1sX9mLnSoiNte5zcFxssgpGA==
+
inherits@2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
@@ -25,6 +30,11 @@ path@^0.12.7:
process "^0.11.1"
util "^0.10.3"
+pluralize@^8.0.0:
+ version "8.0.0"
+ resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-8.0.0.tgz#1a6fa16a38d12a1901e0320fa017051c539ce3b1"
+ integrity sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==
+
process@^0.11.1:
version "0.11.10"
resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"