Added CSV export of subscribers
Fixed some bugs in subscriptions Updated some packages to avoid warnings about vulnerabilities Completed RSS feed campaigns
This commit is contained in:
parent
8683f8c91e
commit
bf69e633c4
47 changed files with 5255 additions and 9651 deletions
|
@ -22,6 +22,7 @@ const allowedKeysCommon = ['name', 'description', 'segment', 'namespace',
|
|||
'send_configuration', 'from_name_override', 'from_email_override', 'reply_to_override', 'subject_override', 'data', 'click_tracking_disabled', 'open_tracking_disabled', 'unsubscribe_url'];
|
||||
|
||||
const allowedKeysCreate = new Set(['type', 'source', ...allowedKeysCommon]);
|
||||
const allowedKeysCreateRssEntry = new Set(['type', 'source', 'parent', ...allowedKeysCommon]);
|
||||
const allowedKeysUpdate = new Set([...allowedKeysCommon]);
|
||||
|
||||
const Content = {
|
||||
|
@ -32,7 +33,6 @@ const Content = {
|
|||
SETTINGS_WITH_STATS: 4
|
||||
};
|
||||
|
||||
|
||||
function hash(entity, content) {
|
||||
let filteredEntity;
|
||||
|
||||
|
@ -63,11 +63,25 @@ async function listDTAjax(context, params) {
|
|||
[{ entityTypeId: 'campaign', requiredOperations: ['view'] }],
|
||||
params,
|
||||
builder => builder.from('campaigns')
|
||||
.innerJoin('namespaces', 'namespaces.id', 'campaigns.namespace'),
|
||||
.innerJoin('namespaces', 'namespaces.id', 'campaigns.namespace')
|
||||
.whereNull('campaigns.parent'),
|
||||
['campaigns.id', 'campaigns.name', 'campaigns.cid', 'campaigns.description', 'campaigns.type', 'campaigns.status', 'campaigns.scheduled', 'campaigns.source', 'campaigns.created', 'namespaces.name']
|
||||
);
|
||||
}
|
||||
|
||||
async function listChildrenDTAjax(context, campaignId, params) {
|
||||
return await dtHelpers.ajaxListWithPermissions(
|
||||
context,
|
||||
[{ entityTypeId: 'campaign', requiredOperations: ['view'] }],
|
||||
params,
|
||||
builder => builder.from('campaigns')
|
||||
.innerJoin('namespaces', 'namespaces.id', 'campaigns.namespace')
|
||||
.where('campaigns.parent', campaignId),
|
||||
['campaigns.id', 'campaigns.name', 'campaigns.cid', 'campaigns.description', 'campaigns.type', 'campaigns.status', 'campaigns.scheduled', 'campaigns.source', 'campaigns.created', 'namespaces.name']
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
async function listWithContentDTAjax(context, params) {
|
||||
return await dtHelpers.ajaxListWithPermissions(
|
||||
context,
|
||||
|
@ -368,12 +382,21 @@ async function _createTx(tx, context, entity, content) {
|
|||
|
||||
await _validateAndPreprocess(tx, context, entity, true, content);
|
||||
|
||||
const filteredEntity = filterObject(entity, allowedKeysCreate);
|
||||
const filteredEntity = filterObject(entity, entity.type === CampaignType.RSS_ENTRY ? allowedKeysCreateRssEntry : allowedKeysCreate);
|
||||
filteredEntity.cid = shortid.generate();
|
||||
|
||||
const data = filteredEntity.data;
|
||||
|
||||
filteredEntity.data = JSON.stringify(filteredEntity.data);
|
||||
|
||||
if (filteredEntity.type === CampaignType.RSS || filteredEntity.type === CampaignType.TRIGGERED) {
|
||||
filteredEntity.status = CampaignStatus.ACTIVE;
|
||||
} else if (filteredEntity.type === CampaignType.RSS_ENTRY) {
|
||||
filteredEntity.status = CampaignStatus.SCHEDULED;
|
||||
} else {
|
||||
filteredEntity.status = CampaignStatus.IDLE;
|
||||
}
|
||||
|
||||
const ids = await tx('campaigns').insert(filteredEntity);
|
||||
const id = ids[0];
|
||||
|
||||
|
@ -386,7 +409,11 @@ async function _createTx(tx, context, entity, content) {
|
|||
});
|
||||
}
|
||||
|
||||
await shares.rebuildPermissionsTx(tx, { entityTypeId: 'campaign', entityId: id });
|
||||
if (filteredEntity.parent) {
|
||||
await shares.rebuildPermissionsTx(tx, { entityTypeId: 'campaign', entityId: id, parentId: filteredEntity.parent });
|
||||
} else {
|
||||
await shares.rebuildPermissionsTx(tx, { entityTypeId: 'campaign', entityId: id });
|
||||
}
|
||||
|
||||
if (copyFilesFrom) {
|
||||
await files.copyAllTx(tx, context, copyFilesFrom.entityType, 'file', copyFilesFrom.entityId, 'campaign', 'file', id);
|
||||
|
@ -459,29 +486,44 @@ async function updateWithConsistencyCheck(context, entity, content) {
|
|||
});
|
||||
}
|
||||
|
||||
async function _removeTx(tx, context, id, existing = null) {
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'campaign', id, 'delete');
|
||||
|
||||
if (!existing) {
|
||||
existing = await tx('campaigns').where('id', id).select(['id', 'status', 'type']).first();
|
||||
}
|
||||
|
||||
if (existing.status === CampaignStatus.SENDING) {
|
||||
return new interoperableErrors.InvalidStateError;
|
||||
}
|
||||
|
||||
enforce(existing.type === CampaignType.REGULAR || existing.type === CampaignType.RSS || existing.type === CampaignType.TRIGGERED, 'This campaign cannot be removed by user.');
|
||||
|
||||
const childCampaigns = await tx('campaigns').where('parent', id).select(['id', 'status', 'type']);
|
||||
for (const childCampaign of childCampaigns) {
|
||||
await _removeTx(tx, contect, childCampaign.id, childCampaign);
|
||||
}
|
||||
|
||||
await files.removeAllTx(tx, context, 'campaign', 'file', id);
|
||||
await files.removeAllTx(tx, context, 'campaign', 'attachment', id);
|
||||
|
||||
await tx('campaign_lists').where('campaign', id).del();
|
||||
await tx('campaign_messages').where('campaign', id).del();
|
||||
await tx('campaign_links').where('campaign', id).del();
|
||||
|
||||
await triggers.removeAllByCampaignIdTx(tx, context, id);
|
||||
|
||||
await tx('template_dep_campaigns')
|
||||
.where('campaign', id)
|
||||
.del();
|
||||
|
||||
await tx('campaigns').where('id', id).del();
|
||||
}
|
||||
|
||||
|
||||
async function remove(context, id) {
|
||||
await knex.transaction(async tx => {
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'campaign', id, 'delete');
|
||||
|
||||
const existing = tx('campaigns').where('id', id);
|
||||
if (existing.status === CampaignStatus.SENDING) {
|
||||
return new interoperableErrors.InvalidStateError;
|
||||
}
|
||||
|
||||
await files.removeAllTx(tx, context, 'campaign', 'file', id);
|
||||
await files.removeAllTx(tx, context, 'campaign', 'attachment', id);
|
||||
|
||||
await tx('campaign_lists').where('campaign', id).del();
|
||||
await tx('campaign_messages').where('campaign', id).del();
|
||||
await tx('campaign_links').where('campaign', id).del();
|
||||
|
||||
await triggers.removeAllByCampaignIdTx(tx, context, id);
|
||||
|
||||
await tx('template_dep_campaigns')
|
||||
.where('campaign', id)
|
||||
.del();
|
||||
|
||||
await tx('campaigns').where('id', id).del();
|
||||
await _removeTx(tx, context, id);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -705,8 +747,6 @@ async function _changeStatus(context, campaignId, permittedCurrentStates, newSta
|
|||
throw new interoperableErrors.InvalidStateError(invalidStateMessage);
|
||||
}
|
||||
|
||||
console.log(scheduled);
|
||||
|
||||
await tx('campaigns').where('id', campaignId).update({
|
||||
status: newState,
|
||||
scheduled
|
||||
|
@ -747,9 +787,20 @@ async function reset(context, campaignId) {
|
|||
});
|
||||
}
|
||||
|
||||
async function enable(context, campaignId) {
|
||||
await _changeStatus(context, campaignId, [CampaignStatus.INACTIVE], CampaignStatus.ACTIVE, 'Cannot enable campaign unless it is in INACTIVE state');
|
||||
}
|
||||
|
||||
async function disable(context, campaignId) {
|
||||
await _changeStatus(context, campaignId, [CampaignStatus.ACTIVE], CampaignStatus.INACTIVE, 'Cannot disable campaign unless it is in ACTIVE state');
|
||||
}
|
||||
|
||||
|
||||
|
||||
module.exports.Content = Content;
|
||||
module.exports.hash = hash;
|
||||
module.exports.listDTAjax = listDTAjax;
|
||||
module.exports.listChildrenDTAjax = listChildrenDTAjax;
|
||||
module.exports.listWithContentDTAjax = listWithContentDTAjax;
|
||||
module.exports.listOthersWhoseListsAreIncludedDTAjax = listOthersWhoseListsAreIncludedDTAjax;
|
||||
module.exports.listTestUsersDTAjax = listTestUsersDTAjax;
|
||||
|
@ -774,6 +825,8 @@ module.exports.getSubscribersQueryGeneratorTx = getSubscribersQueryGeneratorTx;
|
|||
module.exports.start = start;
|
||||
module.exports.stop = stop;
|
||||
module.exports.reset = reset;
|
||||
module.exports.enable = enable;
|
||||
module.exports.disable = disable;
|
||||
|
||||
module.exports.rawGetByTx = rawGetByTx;
|
||||
module.exports.getTrackingSettingsByCidTx = getTrackingSettingsByCidTx;
|
|
@ -694,10 +694,11 @@ async function forHbs(context, listId, subscription) { // assumes grouped subscr
|
|||
return forHbsWithFieldsGrouped(flds, subscription);
|
||||
}
|
||||
|
||||
function getMergeTags(fieldsGrouped, subscription) { // assumes grouped subscription
|
||||
function getMergeTags(fieldsGrouped, subscription, extraTags = {}) { // assumes grouped subscription
|
||||
const mergeTags = {
|
||||
'EMAIL': subscription.email,
|
||||
...getMergeTagsForBases(getTrustedUrl(), getSandboxUrl(), getPublicUrl())
|
||||
...getMergeTagsForBases(getTrustedUrl(), getSandboxUrl(), getPublicUrl()),
|
||||
...extraTags
|
||||
};
|
||||
|
||||
for (const fld of fieldsGrouped) {
|
||||
|
|
|
@ -195,24 +195,6 @@ async function remove(context, id) {
|
|||
});
|
||||
}
|
||||
|
||||
async function getMergeTags(context, id) {
|
||||
return await knex.transaction(async tx => {
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'list', id, ['view']);
|
||||
const groupedFields = await fields.listGroupedTx(tx, id);
|
||||
|
||||
const mergeTags = [];
|
||||
for (const field of groupedFields) {
|
||||
mergeTags.push({
|
||||
key: field.key,
|
||||
value: field.name
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
return mergeTags;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
module.exports.UnsubscriptionMode = UnsubscriptionMode;
|
||||
module.exports.hash = hash;
|
||||
|
@ -226,4 +208,3 @@ module.exports.getByCid = getByCid;
|
|||
module.exports.create = create;
|
||||
module.exports.updateWithConsistencyCheck = updateWithConsistencyCheck;
|
||||
module.exports.remove = remove;
|
||||
module.exports.getMergeTags = getMergeTags;
|
||||
|
|
|
@ -20,7 +20,7 @@ function hash(entity) {
|
|||
return hasher.hash(filterObject(entity, allowedKeys));
|
||||
}
|
||||
|
||||
async function getByIdWithTemplate(context, id) {
|
||||
async function getByIdWithTemplate(context, id, withPermissions = true) {
|
||||
return await knex.transaction(async tx => {
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'report', id, 'view');
|
||||
|
||||
|
@ -33,7 +33,9 @@ async function getByIdWithTemplate(context, id) {
|
|||
entity.user_fields = JSON.parse(entity.user_fields);
|
||||
entity.params = JSON.parse(entity.params);
|
||||
|
||||
entity.permissions = await shares.getPermissionsTx(tx, context, 'report', id);
|
||||
if (withPermissions) {
|
||||
entity.permissions = await shares.getPermissionsTx(tx, context, 'report', id);
|
||||
}
|
||||
|
||||
return entity;
|
||||
});
|
||||
|
|
|
@ -100,7 +100,14 @@ async function assign(context, entityTypeId, entityId, userId, role) {
|
|||
await enforceEntityPermissionTx(tx, context, entityTypeId, entityId, 'share');
|
||||
|
||||
enforce(await tx('users').where('id', userId).select('id').first(), 'Invalid user id');
|
||||
enforce(await tx(entityType.entitiesTable).where('id', entityId).select('id').first(), 'Invalid entity id');
|
||||
|
||||
const extraColumns = entityType.dependentPermissions ? entityType.dependentPermissions.extraColumns : [];
|
||||
const entity = await tx(entityType.entitiesTable).where('id', entityId).select(['id', ...extraColumns]).first();
|
||||
enforce(entity, 'Invalid entity id');
|
||||
|
||||
if (entityType.dependentPermissions) {
|
||||
enforce(!entityType.dependentPermissions.getParent(entity), 'Cannot share/unshare a dependent entity');
|
||||
}
|
||||
|
||||
const entry = await tx(entityType.sharesTable).where({user: userId, entity: entityId}).select('role').first();
|
||||
|
||||
|
@ -310,13 +317,51 @@ async function rebuildPermissionsTx(tx, restriction) {
|
|||
}
|
||||
await expungeQuery;
|
||||
|
||||
const entitiesQuery = tx(entityType.entitiesTable).select(['id', 'namespace']);
|
||||
const extraColumns = entityType.dependentPermissions ? entityType.dependentPermissions.extraColumns : [];
|
||||
const entitiesQuery = tx(entityType.entitiesTable).select(['id', 'namespace', ...extraColumns]);
|
||||
|
||||
|
||||
const notToBeInserted = new Set();
|
||||
if (restriction.entityId) {
|
||||
entitiesQuery.where('id', restriction.entityId);
|
||||
if (restriction.parentId) {
|
||||
notToBeInserted.add(restriction.parentId);
|
||||
entitiesQuery.whereIn('id', [restriction.entityId, restriction.parentId]);
|
||||
} else {
|
||||
entitiesQuery.where('id', restriction.entityId);
|
||||
}
|
||||
}
|
||||
const entities = await entitiesQuery;
|
||||
|
||||
for (const entity of entities) {
|
||||
// TODO - process restriction.parentId
|
||||
|
||||
const parentEntities = new Map();
|
||||
let nonChildEntities;
|
||||
if (entityType.dependentPermissions) {
|
||||
nonChildEntities = [];
|
||||
|
||||
for (const entity of entities) {
|
||||
const parent = entityType.dependentPermissions.getParent(entity);
|
||||
|
||||
if (parent) {
|
||||
let childEntities;
|
||||
if (parentEntities.has(parent)) {
|
||||
childEntities = parentEntities.get(parent);
|
||||
} else {
|
||||
childEntities = [];
|
||||
parentEntities.set(parent, childEntities);
|
||||
}
|
||||
|
||||
childEntities.push(entity.id);
|
||||
} else {
|
||||
nonChildEntities.push(entity);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
nonChildEntities = entities;
|
||||
}
|
||||
|
||||
|
||||
for (const entity of nonChildEntities) {
|
||||
const permsPerUser = new Map();
|
||||
|
||||
if (entity.namespace) { // The root namespace has not parent namespace, thus the test
|
||||
|
@ -350,15 +395,37 @@ async function rebuildPermissionsTx(tx, restriction) {
|
|||
}
|
||||
}
|
||||
|
||||
for (const userPermsPair of permsPerUser.entries()) {
|
||||
const data = [];
|
||||
if (!notToBeInserted.has(entity.id)) {
|
||||
for (const userPermsPair of permsPerUser.entries()) {
|
||||
const data = [];
|
||||
|
||||
for (const operation of userPermsPair[1]) {
|
||||
data.push({user: userPermsPair[0], entity: entity.id, operation});
|
||||
for (const operation of userPermsPair[1]) {
|
||||
data.push({user: userPermsPair[0], entity: entity.id, operation});
|
||||
}
|
||||
|
||||
if (data.length > 0) {
|
||||
await tx(entityType.permissionsTable).insert(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (data.length > 0) {
|
||||
await tx(entityType.permissionsTable).insert(data);
|
||||
if (parentEntities.has(entity.id)) {
|
||||
const childEntities = parentEntities.get(entity.id);
|
||||
|
||||
for (const childId of childEntities) {
|
||||
for (const userPermsPair of permsPerUser.entries()) {
|
||||
const data = [];
|
||||
|
||||
for (const operation of userPermsPair[1]) {
|
||||
if (operation !== 'share') {
|
||||
data.push({user: userPermsPair[0], entity: childId, operation});
|
||||
}
|
||||
}
|
||||
|
||||
if (data.length > 0) {
|
||||
await tx(entityType.permissionsTable).insert(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -81,7 +81,7 @@ function getSubscriptionTableName(listId) {
|
|||
return `subscription__${listId}`;
|
||||
}
|
||||
|
||||
async function getGroupedFieldsMap(tx, listId) {
|
||||
async function getGroupedFieldsMapTx(tx, listId) {
|
||||
const groupedFields = await fields.listGroupedTx(tx, listId);
|
||||
const result = {};
|
||||
for (const fld of groupedFields) {
|
||||
|
@ -189,7 +189,7 @@ function hashByAllowedKeys(allowedKeys, entity) {
|
|||
|
||||
async function hashByList(listId, entity) {
|
||||
return await knex.transaction(async tx => {
|
||||
const groupedFieldsMap = await getGroupedFieldsMap(tx, listId);
|
||||
const groupedFieldsMap = await getGroupedFieldsMapTx(tx, listId);
|
||||
const allowedKeys = getAllowedKeys(groupedFieldsMap);
|
||||
return hashByAllowedKeys(allowedKeys, entity);
|
||||
});
|
||||
|
@ -204,7 +204,7 @@ async function _getByTx(tx, context, listId, key, value, grouped) {
|
|||
throw new interoperableErrors.NotFoundError('Subscription not found in this list');
|
||||
}
|
||||
|
||||
const groupedFieldsMap = await getGroupedFieldsMap(tx, listId);
|
||||
const groupedFieldsMap = await getGroupedFieldsMapTx(tx, listId);
|
||||
|
||||
if (grouped) {
|
||||
groupSubscription(groupedFieldsMap, entity);
|
||||
|
@ -248,7 +248,7 @@ async function listDTAjax(context, listId, segmentId, params) {
|
|||
// to group the fields. Then we copy relevant values form grouped subscription to ajaxList's data which then get
|
||||
// returned to the client. During the copy, we also render the values.
|
||||
|
||||
const groupedFieldsMap = await getGroupedFieldsMap(tx, listId);
|
||||
const groupedFieldsMap = await getGroupedFieldsMapTx(tx, listId);
|
||||
const listFlds = await fields.listByOrderListTx(tx, listId, ['column', 'id']);
|
||||
|
||||
const columns = [
|
||||
|
@ -387,7 +387,7 @@ async function list(context, listId, grouped = true, offset, limit) {
|
|||
const entities = await entitiesQry;
|
||||
|
||||
if (grouped) {
|
||||
const groupedFieldsMap = await getGroupedFieldsMap(tx, listId);
|
||||
const groupedFieldsMap = await getGroupedFieldsMapTx(tx, listId);
|
||||
|
||||
for (const entity of entities) {
|
||||
groupSubscription(groupedFieldsMap, entity);
|
||||
|
@ -401,6 +401,48 @@ async function list(context, listId, grouped = true, offset, limit) {
|
|||
});
|
||||
}
|
||||
|
||||
// Note that this does not do all the work in the transaction. Thus it is prone to fail if the list is deleted in during the run of the function
|
||||
async function* listIterator(context, listId, segmentId, grouped = true) {
|
||||
let groupedFieldsMap;
|
||||
let addSegmentQuery;
|
||||
|
||||
await knex.transaction(async tx => {
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'viewSubscriptions');
|
||||
|
||||
if (grouped) {
|
||||
groupedFieldsMap = await getGroupedFieldsMapTx(tx, listId);
|
||||
}
|
||||
|
||||
addSegmentQuery = segmentId ? await segments.getQueryGeneratorTx(tx, listId, segmentId) : () => {};
|
||||
});
|
||||
|
||||
let lastId = 0;
|
||||
|
||||
while (true) {
|
||||
const entities = await knex(getSubscriptionTableName(listId))
|
||||
.orderBy('id', 'asc')
|
||||
.where('id', '>', lastId)
|
||||
.where(function() {
|
||||
addSegmentQuery(this);
|
||||
})
|
||||
.limit(500);
|
||||
|
||||
if (entities.length > 0) {
|
||||
for (const entity of entities) {
|
||||
if (grouped) {
|
||||
groupSubscription(groupedFieldsMap, entity);
|
||||
}
|
||||
|
||||
yield entity;
|
||||
}
|
||||
|
||||
lastId = entities[entities.length - 1].id;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function serverValidate(context, listId, data) {
|
||||
return await knex.transaction(async tx => {
|
||||
const result = {};
|
||||
|
@ -563,7 +605,7 @@ async function createTxWithGroupedFieldsMap(tx, context, listId, groupedFieldsMa
|
|||
|
||||
async function create(context, listId, entity, source, meta) {
|
||||
return await knex.transaction(async tx => {
|
||||
const groupedFieldsMap = await getGroupedFieldsMap(tx, listId);
|
||||
const groupedFieldsMap = await getGroupedFieldsMapTx(tx, listId);
|
||||
return await createTxWithGroupedFieldsMap(tx, context, listId, groupedFieldsMap, entity, source, meta);
|
||||
});
|
||||
}
|
||||
|
@ -577,7 +619,7 @@ async function updateWithConsistencyCheck(context, listId, entity, source) {
|
|||
throw new interoperableErrors.NotFoundError();
|
||||
}
|
||||
|
||||
const groupedFieldsMap = await getGroupedFieldsMap(tx, listId);
|
||||
const groupedFieldsMap = await getGroupedFieldsMapTx(tx, listId);
|
||||
const allowedKeys = getAllowedKeys(groupedFieldsMap);
|
||||
|
||||
groupSubscription(groupedFieldsMap, existing);
|
||||
|
@ -718,7 +760,7 @@ async function updateManaged(context, listId, cid, entity) {
|
|||
await knex.transaction(async tx => {
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'manageSubscriptions');
|
||||
|
||||
const groupedFieldsMap = await getGroupedFieldsMap(tx, listId);
|
||||
const groupedFieldsMap = await getGroupedFieldsMapTx(tx, listId);
|
||||
|
||||
const update = {};
|
||||
for (const key in groupedFieldsMap) {
|
||||
|
@ -764,11 +806,12 @@ module.exports.getByCidTx = getByCidTx;
|
|||
module.exports.getByCid = getByCid;
|
||||
module.exports.getByEmail = getByEmail;
|
||||
module.exports.list = list;
|
||||
module.exports.listIterator = listIterator;
|
||||
module.exports.listDTAjax = listDTAjax;
|
||||
module.exports.listTestUsersDTAjax = listTestUsersDTAjax;
|
||||
module.exports.serverValidate = serverValidate;
|
||||
module.exports.create = create;
|
||||
module.exports.getGroupedFieldsMap = getGroupedFieldsMap;
|
||||
module.exports.getGroupedFieldsMapTx = getGroupedFieldsMapTx;
|
||||
module.exports.createTxWithGroupedFieldsMap = createTxWithGroupedFieldsMap;
|
||||
module.exports.updateWithConsistencyCheck = updateWithConsistencyCheck;
|
||||
module.exports.remove = remove;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue