Skip to content

Commit f9a9c9a

Browse files
committed
feat(cli): Non destructive database and collection update
1 parent ed76075 commit f9a9c9a

File tree

3 files changed

+145
-10
lines changed

3 files changed

+145
-10
lines changed

templates/cli/lib/commands/push.js.twig

Lines changed: 139 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1+
const chalk = require('chalk');
12
const inquirer = require("inquirer");
23
const JSONbig = require("json-bigint")({ storeAsString: false });
34
const { Command } = require("commander");
45
const { localConfig, globalConfig } = require("../config");
56
const { Spinner, SPINNER_ARC, SPINNER_DOTS } = require('../spinner');
67
const { paginate } = require('../paginate');
78
const { questionsPushBuckets, questionsPushTeams, questionsPushFunctions, questionsGetEntrypoint, questionsPushCollections, questionsConfirmPushCollections, questionsPushMessagingTopics } = require("../questions");
8-
const { actionRunner, success, log, error, commandDescriptions } = require("../parser");
9+
const { actionRunner, success, log, error, commandDescriptions, drawTable } = require("../parser");
910
const { functionsGet, functionsCreate, functionsUpdate, functionsCreateDeployment, functionsUpdateDeployment, functionsGetDeployment, functionsListVariables, functionsDeleteVariable, functionsCreateVariable } = require('./functions');
1011
const {
1112
databasesGet,
@@ -140,6 +141,39 @@ const awaitPools = {
140141
iteration + 1
141142
);
142143
},
144+
deleteAttributes: async (databaseId, collectionId, attributeKeys, iteration = 1) => {
145+
if (iteration > pollMaxDebounces) {
146+
return false;
147+
}
148+
149+
let steps = Math.max(1, Math.ceil(attributeKeys.length / STEP_SIZE));
150+
if (steps > 1 && iteration === 1) {
151+
pollMaxDebounces *= steps;
152+
153+
log('Found a large number of deleting attributes, increasing timeout to ' + (pollMaxDebounces * POLL_DEBOUNCE / 1000 / 60) + ' minutes')
154+
}
155+
156+
const { attributes } = await paginate(databasesListAttributes, {
157+
databaseId,
158+
collectionId,
159+
parseOutput: false
160+
}, 100, 'attributes');
161+
162+
const ready = attributeKeys.filter(attribute => attributes.includes(attribute.key));
163+
164+
if (ready.length === 0) {
165+
return true;
166+
}
167+
168+
await new Promise(resolve => setTimeout(resolve, POLL_DEBOUNCE));
169+
170+
return await awaitPools.expectAttributes(
171+
databaseId,
172+
collectionId,
173+
attributeKeys,
174+
iteration + 1
175+
);
176+
},
143177
expectAttributes: async (databaseId, collectionId, attributeKeys, iteration = 1) => {
144178
if (iteration > pollMaxDebounces) {
145179
return false;
@@ -645,6 +679,91 @@ const createAttribute = async (databaseId, collectionId, attribute) => {
645679
}
646680
}
647681

682+
const deleteAttribute = async (collection, attribute) => {
683+
log(`Deleting attribute ${attribute.key} of ${collection.name} ( ${collection['$id']} )`);
684+
685+
await databasesDeleteAttribute({
686+
databaseId: collection['databaseId'],
687+
collectionId: collection['$id'],
688+
key: attribute.key,
689+
parseOutput: false
690+
});
691+
}
692+
693+
const deepSimilar = (remote, local, collection) => {
694+
if (local === undefined) {
695+
return undefined;
696+
}
697+
698+
const key = `${chalk.yellow(local.key)} in ${collection.name} (${collection['$id']})`;
699+
700+
if (remote.type !== local.type) {
701+
return { key, attribute: remote, reason: `type changed from ${chalk.red(remote.type)} to ${chalk.green(local.type)}` };
702+
}
703+
704+
if (remote.array !== local.array) {
705+
return { key, attribute: remote, reason: `array changed from ${chalk.red(remote.array)} to ${chalk.green(local.array)}` };
706+
}
707+
708+
if (remote.size !== local.size) {
709+
return { key, attribute: remote, reason: `size changed from ${chalk.red(remote.size)} to ${chalk.green(local.size)}` };
710+
}
711+
712+
if (remote.relatedCollectionId !== local.relatedCollectionId) {
713+
return { key, attribute: remote, reason: `relationships collection id changed from ${chalk.red(remote.relatedCollectionId)} to ${chalk.green(local.relatedCollectionId)}` };
714+
}
715+
716+
if (remote.twoWay !== local.twoWay) {
717+
return { key, attribute: remote, reason: `relationships twoWay changed from ${chalk.red(remote.twoWay)} to ${chalk.green(local.twoWay)}` };
718+
}
719+
720+
if (remote.twoWayKey !== local.twoWayKey) {
721+
return { key, attribute: remote, reason: `relationships twoWayKey changed from ${chalk.red(remote.twoWayKey)} to ${chalk.green(local.twoWayKey)}` };
722+
}
723+
724+
return undefined;
725+
}
726+
727+
const findMatch = (attribute, attributes) => attributes.find((attr) => attr.key === attribute.key);
728+
729+
const updatedList = async (remoteAttributes, localAttributes, collection) => {
730+
const deleting = remoteAttributes.filter((attribute) => !findMatch(attribute, localAttributes));
731+
const changes = remoteAttributes.map((attribute) => deepSimilar(attribute, findMatch(attribute, localAttributes), collection)).filter(attribute => attribute !== undefined);
732+
let changedAttributes = [];
733+
734+
if (changes.length > 0) {
735+
log('There is a conflict in your collection deployment');
736+
drawTable(changes.map((change) => {
737+
return { Key: change.key, Reason: change.reason };
738+
}));
739+
const answers = await inquirer.prompt(questionsPushCollections[1]);
740+
741+
if (answers.changes.toLowerCase() !== 'yes') {
742+
return [];
743+
}
744+
745+
changedAttributes = changes.map((change) => change.attribute);
746+
747+
await Promise.all(changedAttributes.map((changed) => deleteAttribute(collection, changed)));
748+
749+
remoteAttributes = remoteAttributes.filter((attribute) => !findMatch(attribute, changedAttributes))
750+
}
751+
752+
await Promise.all(deleting.map((attribute) => deleteAttribute(collection, attribute)));
753+
754+
const attributeKeys = [...remoteAttributes.map(attribute => attribute.key), ...deleting.map(attribute => attribute.key)]
755+
756+
if (attributeKeys.length) {
757+
const deleteAttributesPoolStatus = await awaitPools.deleteAttributes(collection['databaseId'], collection['$id'], attributeKeys);
758+
759+
if (!deleteAttributesPoolStatus) {
760+
throw new Error("Attribute deletion timed out.");
761+
}
762+
}
763+
764+
return localAttributes.filter((attribute) => !findMatch(attribute, remoteAttributes));
765+
}
766+
648767
const pushCollection = async ({ all, yes } = {}) => {
649768
const collections = [];
650769

@@ -666,7 +785,7 @@ const pushCollection = async ({ all, yes } = {}) => {
666785
}
667786
const databases = Array.from(new Set(collections.map(c => c['databaseId'])));
668787

669-
// Parallel db action
788+
// Parallel db actions
670789
await Promise.all(databases.map(async (dbId) => {
671790
const localDatabase = localConfig.getDatabase(dbId);
672791

@@ -696,7 +815,7 @@ const pushCollection = async ({ all, yes } = {}) => {
696815
}
697816
}));
698817

699-
// Parallel collection action
818+
// Parallel collection actions
700819
await Promise.all(collections.map(async (collection) => {
701820
try {
702821
const remoteCollection = await databasesGetCollection({
@@ -715,6 +834,9 @@ const pushCollection = async ({ all, yes } = {}) => {
715834

716835
success(`Updated ${collection.name} ( ${collection['$id']} ) name`);
717836
}
837+
collection.remoteVersion = remoteCollection;
838+
839+
collection.isExisted = true;
718840
} catch (e) {
719841
if (e.code == 404) {
720842
log(`Collection ${collection.name} does not exist in the project. Creating ... `);
@@ -726,19 +848,30 @@ const pushCollection = async ({ all, yes } = {}) => {
726848
permissions: collection['$permissions'],
727849
parseOutput: false
728850
})
729-
851+
collection.isNew = true;
730852
} else {
731853
throw e;
732854
}
733855
}
734856
}))
735857

736-
// Serialize attribute creation
858+
// Serialize attribute actions
737859
for (let collection of collections) {
860+
let attributes = collection.attributes;
861+
862+
if (collection.isExisted) {
863+
attributes = await updatedList(collection.remoteVersion.attributes, collection.attributes, collection);
864+
865+
if (Array.isArray(attributes) && attributes.length <= 0) {
866+
log(`No changes has been detected. Skipping ${collection.name} ( ${collection['$id']} )`);
867+
continue;
868+
}
869+
}
870+
738871
log(`Pushing collection ${collection.name} ( ${collection['databaseId']} - ${collection['$id']} ) attributes`)
739872

740873
try {
741-
await createAttributes(collection.attributes, collection)
874+
await createAttributes(attributes, collection)
742875
} catch (e) {
743876
throw e;
744877
}

templates/cli/lib/parser.js.twig

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,5 +192,6 @@ module.exports = {
192192
success,
193193
error,
194194
commandDescriptions,
195-
cliConfig
195+
cliConfig,
196+
drawTable
196197
}

templates/cli/lib/questions.js.twig

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
const chalk = require("chalk")
12
const { localConfig } = require('./config');
23
const { projectsList } = require('./commands/projects');
34
const { teamsList } = require('./commands/teams');
@@ -368,9 +369,9 @@ const questionsPushCollections = [
368369
},
369370
{
370371
type: "input",
371-
name: "override",
372-
message: 'Are you sure you want to override this collection? This can lead to loss of data! Type "YES" to confirm.'
373-
},
372+
name: "changes",
373+
message: `These are the pending destructive changes. To create the new fields, all data in the old ones will be ${chalk.red('deleted')}. Type "YES" to confirm`
374+
}
374375
]
375376

376377
const questionsPushBuckets = [

0 commit comments

Comments
 (0)