budibase/packages/core/test/specHelpers.js

507 lines
22 KiB
JavaScript
Raw Normal View History

2019-07-15 08:12:52 +02:00
import path from "path";
import {getAppApis, getRecordApi,
getCollectionApi, getIndexApi, getActionsApi} from "../src";
import memory from "./memory";
import {setupDatastore} from "../src/appInitialise";
import {configFolder, fieldDefinitions,
templateDefinitions, isNothing,
joinKey,
isSomething} from "../src/common";
import { getNewIndexTemplate } from "../src/templateApi/createNodes";
import {indexTypes} from "../src/templateApi/indexes";
import getTemplateApi from "../src/templateApi";
import {getApplicationDefinition} from "../src/templateApi/getApplicationDefinition";
import getAuthApi from "../src/authApi";
import {createEventAggregator} from "../src/appInitialise/eventAggregator";
import {filter, find} from "lodash/fp";
import {createBehaviourSources} from "../src/actionsApi/buildBehaviourSource";
import {createAction, createTrigger} from "../src/templateApi/createActions";
import {initialiseActions} from "../src/actionsApi/initialise";
import {cleanup} from "../src/transactions/cleanup";
import nodeCrypto from "./nodeCrypto";
import {permission} from "../src/authApi/permissions";
import {generateFullPermissions} from "../src/authApi/generateFullPermissions"
import {initialiseData} from "../src/appInitialise/initialiseData";
const exp = module.exports;
export const testFileArea = (testNameArea) => path.join("test", "fs_test_area", testNameArea);
export const testConfigFolder = (testAreaName) => path.join(exp.testFileArea(testAreaName), configFolder);
export const testFieldDefinitionsPath = (testAreaName) => path.join(exp.testFileArea(testAreaName), fieldDefinitions);
export const testTemplatesPath = (testAreaName) => path.join(exp.testFileArea(testAreaName), templateDefinitions);
export const getMemoryStore = () => setupDatastore(memory({}));
export const getMemoryTemplateApi = () => {
const app = {
datastore:getMemoryStore(),
publish: () => {},
getEpochTime : async () => (new Date()).getTime(),
user:{name:"", permissions:[permission.writeTemplates.get()]}
};
app.removePermission = removePermission(app);
app.withOnlyThisPermission = withOnlyThisPermission(app);
app.withNoPermissions = withNoPermissions(app);
const templateApi = getTemplateApi(app);
templateApi._eventAggregator = createEventAggregator();
templateApi._storeHandle = app.datastore;
return {templateApi, app};
}
// TODO: subscribe actions
export const appFromTempalteApi = async (templateApi, disableCleanupTransactions=false) => {
const appDef = await templateApi.getApplicationDefinition();
const app = {
hierarchy:appDef.hierarchy,
datastore:templateApi._storeHandle,
publish:templateApi._eventAggregator.publish,
_eventAggregator: templateApi._eventAggregator,
getEpochTime : async () => (new Date()).getTime(),
crypto:nodeCrypto,
user:{name:"bob", permissions: []},
actions:{}
};
app.removePermission = removePermission(app);
app.withOnlyThisPermission = withOnlyThisPermission(app);
app.withNoPermissions = withNoPermissions(app);
const fullPermissions = generateFullPermissions(app);
app.user.permissions = fullPermissions;
if(disableCleanupTransactions)
app.cleanupTransactions = async () => {};
else
app.cleanupTransactions = async () => await cleanup(app);
return app;
};
const removePermission = app => perm => {
app.user.permissions = filter(p => p.type !== perm.type
|| (isSomething(perm.nodeKey)
&& perm.nodeKey !== p.nodeKey))
(app.user.permissions);
}
const withOnlyThisPermission = app => perm =>
app.user.permissions = [perm];
const withNoPermissions = app => () =>
app.user.permissions = [];
export const getRecordApiFromTemplateApi = async (templateApi, disableCleanupTransactions=false) => {
const app = await appFromTempalteApi(templateApi, disableCleanupTransactions);
const recordapi = getRecordApi();
recordapi._storeHandle = app.datastore;
}
export const getCollectionApiFromTemplateApi = async (templateApi, disableCleanupTransactions=false) =>
getCollectionApi(await appFromTempalteApi(templateApi, disableCleanupTransactions));
export const getIndexApiFromTemplateApi = async (templateApi, disableCleanupTransactions=false) =>
getIndexApi(await appFromTempalteApi(templateApi, disableCleanupTransactions));
export const getAuthApiFromTemplateApi = async (templateApi, disableCleanupTransactions=false) =>
getAuthApi(await appFromTempalteApi(templateApi, disableCleanupTransactions));
export const findIndex = (parentNode, name) =>
find(i => i.name === name)(parentNode.indexes);
export const findCollectionDefaultIndex = (recordCollectionNode) =>
findIndex(recordCollectionNode.parent(), recordCollectionNode.name + "_index");
export const hierarchyFactory = (...additionalFeatures) => templateApi => {
const root = templateApi.getNewRootLevel();
const settingsRecord = templateApi.getNewSingleRecordTemplate(root);
settingsRecord.name = "settings";
const customerRecord = templateApi.getNewRecordTemplate(root, "customer");
customerRecord.collectionName = "customers";
findCollectionDefaultIndex(customerRecord).map = "return {surname:record.surname, isalive:record.isalive};";
const partnerRecord = templateApi.getNewRecordTemplate(root, "partner");
partnerRecord.collectionName = "partners";
const partnerInvoiceRecord = templateApi.getNewRecordTemplate(partnerRecord, "invoice");
partnerInvoiceRecord.collectionName = "invoices";
findCollectionDefaultIndex(partnerInvoiceRecord).name = "partnerInvoices_index";
const invoiceRecord = templateApi.getNewRecordTemplate(customerRecord, "invoice");
invoiceRecord.collectionName = "invoices";
findCollectionDefaultIndex(invoiceRecord).map = "return {createdDate: record.createdDate, totalIncVat: record.totalIncVat};";
const chargeRecord = templateApi.getNewRecordTemplate(invoiceRecord, "charge");
chargeRecord.collectionName = "charges";
const hierarchy = ({root, customerRecord,
invoiceRecord, partnerRecord,
partnerInvoiceRecord, chargeRecord, settingsRecord});
for(let feature of additionalFeatures) {
feature(hierarchy, templateApi);
}
return hierarchy;
};
export const basicAppHierarchyCreator = templateApis =>
hierarchyFactory()(templateApis);
export const withFields = (hierarchy, templateApi) => {
const {customerRecord, invoiceRecord,
partnerInvoiceRecord, chargeRecord,
partnerRecord, settingsRecord, root} = hierarchy;
getNewFieldAndAdd(templateApi, settingsRecord)("appName", "string", "");
const newCustomerField = getNewFieldAndAdd(templateApi, customerRecord);
const partnersReferenceIndex = templateApi.getNewIndexTemplate(root);
partnersReferenceIndex.name = "partnersReference";
partnersReferenceIndex.map = "return {name:record.businessName};";
partnersReferenceIndex.allowedRecordNodeIds = [partnerRecord.nodeId];
const partnerCustomersReverseIndex = templateApi.getNewIndexTemplate(partnerRecord, indexTypes.reference);
partnerCustomersReverseIndex.name = "partnerCustomers";
partnerCustomersReverseIndex.map = "return {...record};";
partnerCustomersReverseIndex.filter = "record.isalive === true"
partnerCustomersReverseIndex.allowedRecordNodeIds = [customerRecord.nodeId];
hierarchy.partnerCustomersReverseIndex = partnerCustomersReverseIndex;
newCustomerField("surname", "string");
newCustomerField("isalive", "bool", "true");
newCustomerField("createddate", "datetime");
newCustomerField("age", "number");
newCustomerField("profilepic", "file");
const customerPartnerField = newCustomerField("partner", "reference", undefined, {
indexNodeKey : "/partnersReference",
displayValue : "name",
reverseIndexNodeKeys : [joinKey(
partnerRecord.nodeKey(), "partnerCustomers" )]
});
const referredToCustomersReverseIndex = templateApi.getNewIndexTemplate(customerRecord, indexTypes.reference);
referredToCustomersReverseIndex.name = "referredToCustomers";
referredToCustomersReverseIndex.map = "return {...record};";
referredToCustomersReverseIndex.getShardName = "return !record.surname ? 'null' : record.surname.substring(0,1);"
referredToCustomersReverseIndex.allowedRecordNodeIds = [customerRecord.nodeId];
hierarchy.referredToCustomersReverseIndex = referredToCustomersReverseIndex;
const customerReferredByField = newCustomerField("referredBy", "reference", undefined, {
indexNodeKey : "/customer_index",
displayValue : "surname",
reverseIndexNodeKeys : [joinKey(
customerRecord.nodeKey(), "referredToCustomers")]
});
hierarchy.customerReferredByField = customerReferredByField;
const newInvoiceField = getNewFieldAndAdd(templateApi, invoiceRecord);
const invoiceTotalIncVatField = newInvoiceField("totalIncVat", "number");
invoiceTotalIncVatField.typeOptions.decimalPlaces = 2;
newInvoiceField("createdDate", "datetime");
newInvoiceField("paidAmount", "number");
newInvoiceField("invoiceType", "string");
newInvoiceField("isWrittenOff", "bool");
const newPartnerField = getNewFieldAndAdd(templateApi, partnerRecord);
newPartnerField("businessName", "string");
const newPartnerInvoiceField = getNewFieldAndAdd(templateApi, partnerInvoiceRecord);
const partnerInvoiceTotalIncVatVield = newPartnerInvoiceField("totalIncVat", "number");
partnerInvoiceTotalIncVatVield.typeOptions.decimalPlaces = 2;
newPartnerInvoiceField("createdDate", "datetime");
newPartnerInvoiceField("paidAmount", "number");
const newChargeField = getNewFieldAndAdd(templateApi, chargeRecord);
newChargeField("amount", "number");
const chargePartnerInvoiceField = newChargeField("partnerInvoice", "reference", undefined, {
reverseIndexNodeKeys : [joinKey(
partnerInvoiceRecord.nodeKey(), "partnerCharges"
)],
displayValue : "createdDate",
indexNodeKey : joinKey(
partnerRecord.nodeKey(), "partnerInvoices_index")
});
const partnerChargesReverseIndex = templateApi.getNewIndexTemplate(partnerInvoiceRecord, indexTypes.reference);
partnerChargesReverseIndex.name = "partnerCharges";
partnerChargesReverseIndex.map = "return {...record};";
partnerChargesReverseIndex.allowedRecordNodeIds = [chargeRecord];
hierarchy.partnerChargesReverseIndex = partnerChargesReverseIndex;
const customersReferenceIndex = templateApi.getNewIndexTemplate(hierarchy.root);
customersReferenceIndex.name = "customersReference";
customersReferenceIndex.map = "return {name:record.surname}";
customersReferenceIndex.filter = "record.isalive === true";
customersReferenceIndex.allowedRecordNodeIds = [customerRecord.nodeId];
const invoiceCustomerField = newInvoiceField("customer", "reference", undefined, {
indexNodeKey : "/customersReference",
reverseIndexNodeKeys : [findCollectionDefaultIndex(invoiceRecord).nodeKey()],
displayValue : "name"
});
}
export const withIndexes = (hierarchy, templateApi) => {
const {root, customerRecord,
partnerInvoiceRecord, invoiceRecord,
partnerRecord, chargeRecord } = hierarchy;
const deceasedCustomersIndex = getNewIndexTemplate(root);
deceasedCustomersIndex.name = "deceased";
deceasedCustomersIndex.map = "return {surname: record.surname, age:record.age};";
deceasedCustomersIndex.filter = "record.isalive === false";
findCollectionDefaultIndex(customerRecord).map = "return record;"
deceasedCustomersIndex.allowedRecordNodeIds = [customerRecord.nodeId];
findCollectionDefaultIndex(invoiceRecord).allowedRecordNodeIds = [invoiceRecord.nodeId];
findCollectionDefaultIndex(customerRecord).allowedRecordNodeIds = [customerRecord.nodeId];
findCollectionDefaultIndex(partnerRecord).allowedRecordNodeIds = [partnerRecord.nodeId];
findIndex(partnerRecord, "partnerInvoices_index").allowedRecordNodeIds = [partnerInvoiceRecord.nodeId];
findCollectionDefaultIndex(chargeRecord).allowedRecordNodeIds = [chargeRecord.nodeId];
const customerInvoicesIndex = getNewIndexTemplate(root);
customerInvoicesIndex.name = "customer_invoices";
customerInvoicesIndex.map = "return record;";
customerInvoicesIndex.filter = "record.type === 'invoice'";
customerInvoicesIndex.allowedRecordNodeIds = [invoiceRecord.nodeId];
const outstandingInvoicesIndex = getNewIndexTemplate(root);
outstandingInvoicesIndex.name = "Outstanding Invoices";
outstandingInvoicesIndex.filter = "record.type === 'invoice' && record.paidAmount < record.totalIncVat";
outstandingInvoicesIndex.map = "return {...record};";
outstandingInvoicesIndex.allowedRecordNodeIds = [
invoiceRecord.nodeId, partnerInvoiceRecord.nodeId
];
const allInvoicesAggregateGroup = templateApi.getNewAggregateGroupTemplate(outstandingInvoicesIndex);
allInvoicesAggregateGroup.name = "all_invoices";
const allInvoicesByType = templateApi.getNewAggregateGroupTemplate(outstandingInvoicesIndex);
allInvoicesByType.groupBy = "return record.invoiceType";
allInvoicesByType.name = "all_invoices_by_type";
const allInvoicesTotalAmountAggregate = templateApi.getNewAggregateTemplate(allInvoicesByType);
allInvoicesTotalAmountAggregate.name = "totalIncVat";
allInvoicesTotalAmountAggregate.aggregatedValue = "return record.totalIncVat";
const allInvoicesPaidAmountAggregate = templateApi.getNewAggregateTemplate(allInvoicesByType);
allInvoicesPaidAmountAggregate.name = "paidAmount";
allInvoicesPaidAmountAggregate.aggregatedValue = "return record.paidAmount";
const writtenOffInvoicesByType = templateApi.getNewAggregateGroupTemplate(outstandingInvoicesIndex);
writtenOffInvoicesByType.groupBy = "return record.invoiceType";
writtenOffInvoicesByType.name = "written_off";
writtenOffInvoicesByType.condition = "record.isWrittenOff === true";
const writtenOffInvoicesTotalAmountAggregate = templateApi.getNewAggregateTemplate(writtenOffInvoicesByType);
writtenOffInvoicesTotalAmountAggregate.name = "totalIncVat";
writtenOffInvoicesTotalAmountAggregate.aggregatedValue = "return record.totalIncVat";
const customersBySurnameIndex = templateApi.getNewIndexTemplate(root);
customersBySurnameIndex.name = "customersBySurname";
customersBySurnameIndex.map = "return {...record};"
customersBySurnameIndex.filter = "";
customersBySurnameIndex.allowedRecordNodeIds = [customerRecord.nodeId];
customersBySurnameIndex.getShardName = "return !record.surname ? 'null' : record.surname.substring(0,1);"
const customersDefaultIndex = findCollectionDefaultIndex(customerRecord);
const customersNoGroupaggregateGroup = templateApi.getNewAggregateGroupTemplate(customersDefaultIndex);
customersNoGroupaggregateGroup.name = "Customers Summary";
const allCustomersAgeFunctions = templateApi.getNewAggregateTemplate(customersNoGroupaggregateGroup);
allCustomersAgeFunctions.aggregatedValue = "return record.age";
allCustomersAgeFunctions.name = "all customers - age breakdown";
const invoicesByOutstandingIndex = templateApi.getNewIndexTemplate(customerRecord);
invoicesByOutstandingIndex.name = "invoicesByOutstanding";
invoicesByOutstandingIndex.map = "return {...record};"
invoicesByOutstandingIndex.filter = "";
invoicesByOutstandingIndex.getShardName = "return (record.totalIncVat > record.paidAmount ? 'outstanding' : 'paid');"
invoicesByOutstandingIndex.allowedRecordNodeIds = [
partnerInvoiceRecord.nodeId, invoiceRecord.nodeId
];
const allInvoicesByType_Sharded = templateApi.getNewAggregateGroupTemplate(invoicesByOutstandingIndex);
allInvoicesByType_Sharded.groupBy = "return record.invoiceType";
allInvoicesByType_Sharded.name = "all_invoices_by_type";
const allInvoicesTotalAmountAggregate_Sharded = templateApi.getNewAggregateTemplate(allInvoicesByType_Sharded);
allInvoicesTotalAmountAggregate_Sharded.name = "totalIncVat";
allInvoicesTotalAmountAggregate_Sharded.aggregatedValue = "return record.totalIncVat";
hierarchy.allInvoicesByType = allInvoicesByType;
hierarchy.allInvoicesTotalAmountAggregate = allInvoicesTotalAmountAggregate;
hierarchy.allInvoicesPaidAmountAggregate = allInvoicesPaidAmountAggregate;
hierarchy.customersDefaultIndex = customersDefaultIndex;
hierarchy.allCustomersAgeFunctions = allCustomersAgeFunctions;
hierarchy.customersNoGroupaggregateGroup = customersNoGroupaggregateGroup;
hierarchy.invoicesByOutstandingIndex = invoicesByOutstandingIndex;
hierarchy.customersBySurnameIndex = customersBySurnameIndex;
hierarchy.outstandingInvoicesIndex = outstandingInvoicesIndex;
hierarchy.deceasedCustomersIndex = deceasedCustomersIndex;
hierarchy.customerInvoicesIndex = customerInvoicesIndex;
};
export const basicAppHierarchyCreator_WithFields = templateApi =>
hierarchyFactory(withFields)(templateApi);
export const basicAppHierarchyCreator_WithFields_AndIndexes = templateApi =>
hierarchyFactory(withFields, withIndexes)(templateApi);
export const setupApphierarchy = async (creator, disableCleanupTransactions=false) => {
const {templateApi} = getMemoryTemplateApi();
const hierarchy = creator(templateApi);
await initialiseData(templateApi._storeHandle, {hierarchy:hierarchy.root, actions:[], triggers:[]});
await templateApi.saveApplicationHierarchy(hierarchy.root);
const app = await appFromTempalteApi(templateApi, disableCleanupTransactions);
const collectionApi = getCollectionApi(app);
const indexApi = getIndexApi(app);
const authApi = getAuthApi(app);
const actionsApi = getActionsApi(app);
const recordApi = await getRecordApi(app);
recordApi._storeHandle = app.datastore;
actionsApi._app = app;
const apis = {
recordApi,
collectionApi,
templateApi,
indexApi,
authApi,
actionsApi,
appHierarchy:hierarchy,
subscribe:templateApi._eventAggregator.subscribe,
app
};
return apis;
};
const disableCleanupTransactions = app => {
}
export const getNewFieldAndAdd = (templateApi, record) => (name, type, initial, typeOptions) => {
const field = templateApi.getNewField(type);
field.name = name;
field.getInitialValue = !initial ? "default" : initial;
if(!!typeOptions)
field.typeOptions = typeOptions;
templateApi.addField(record, field);
return field;
};
export const stubEventHandler = () => {
const events = [];
return {
handle: (name, context) => {
events.push({name, context});
},
events,
getEvents: n => filter(e => e.name === n)
(events)
};
};
export const createValidActionsAndTriggers = () => {
const logMessage = createAction();
logMessage.name = "logMessage";
logMessage.behaviourName = "log";
logMessage.behaviourSource = "budibase-behaviours";
const measureCallTime = createAction();
measureCallTime.name = "measureCallTime";
measureCallTime.behaviourName = "call_timer";
measureCallTime.behaviourSource = "budibase-behaviours";
const sendEmail = createAction();
sendEmail.name = "sendEmail";
sendEmail.behaviourName = "send_email";
sendEmail.behaviourSource = "my-custom-lib";
const logOnErrorTrigger = createTrigger();
logOnErrorTrigger.actionName = "logMessage";
logOnErrorTrigger.eventName = "recordApi:save:onError";
logOnErrorTrigger.optionsCreator = "return context.error.message;";
const timeCustomerSaveTrigger = createTrigger();
timeCustomerSaveTrigger.actionName = "measureCallTime";
timeCustomerSaveTrigger.eventName = "recordApi:save:onComplete";
timeCustomerSaveTrigger.optionsCreator = "return 999;";
timeCustomerSaveTrigger.condition = "context.record.type === 'customer'";
const allActions = [logMessage, measureCallTime, sendEmail];
const allTriggers = [logOnErrorTrigger, timeCustomerSaveTrigger];
const behaviourSources = createBehaviourSources();
const logs = [];
const call_timers = [];
const emails = [];
behaviourSources.register("budibase-behaviours", {
log: message => logs.push(message),
call_timer: opts => call_timers.push(opts)
});
behaviourSources.register("my-custom-lib", {
send_email: em => emails.push(em)
});
return {
logMessage, measureCallTime, sendEmail,
logOnErrorTrigger, timeCustomerSaveTrigger,
allActions, allTriggers, behaviourSources,
logs, call_timers, emails
};
};
export const createAppDefinitionWithActionsAndTriggers = async () => {
const {appHierarchy, templateApi, app, actionsApi} = await setupApphierarchy(
basicAppHierarchyCreator_WithFields
);
// adding validation rule so it can fail when we save it
templateApi.addRecordValidationRule(appHierarchy.customerRecord)(
templateApi.commonRecordValidationRules.fieldNotEmpty("surname")
);
await templateApi.saveApplicationHierarchy(appHierarchy.root);
const actionsAndTriggers = createValidActionsAndTriggers();
const {allActions, allTriggers, behaviourSources} = actionsAndTriggers;
await templateApi.saveActionsAndTriggers(allActions, allTriggers);
app.actions = initialiseActions(
templateApi._eventAggregator.subscribe,
behaviourSources,
allActions,
allTriggers);
app.user.permissions = generateFullPermissions(app);
app.behaviourSources = behaviourSources;
const appDefinition = await templateApi.getApplicationDefinition();
return {templateApi, appDefinition, ...actionsAndTriggers, ...appHierarchy, app, actionsApi};
};
export const validUser = async (app, authApi, password, enabled=true, accessLevels=null) => {
const access = await authApi.getNewAccessLevel(app);
access.name = "admin";
permission.setPassword.add(access);
const access2 = await authApi.getNewAccessLevel(app);
access2.name = "admin2";
permission.setPassword.add(access);
await authApi.saveAccessLevels({version:0, levels:[access, access2]});
const u = authApi.getNewUser(app);
u.name = "bob";
if(accessLevels === null)
u.accessLevels = ["admin"];
else
u.accessLevels = accessLevels;
u.enabled = enabled;
await authApi.createUser(u, password);
return u;
};