@saltcorn/cli 1.5.0-beta.9 → 1.5.0-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -38,45 +38,6 @@ const pickFields = (table, pkName, row, keepId) => {
38
38
  return result;
39
39
  };
40
40
 
41
- const translateInsertFks = async (allChanges, allTranslations) => {
42
- const schema = db.getTenantSchemaPrefix();
43
- const rowIds = (fk, targetTrans, tblName, pkName, changes) => {
44
- if (Object.keys(targetTrans || {}).length > 0) {
45
- const srcTrans = allTranslations[tblName] || {};
46
- // ids with a fk where the target was translated
47
- const insertIds = (changes.inserts || [])
48
- .filter((row) => targetTrans[row[fk.name]] !== undefined)
49
- .map((row) => srcTrans[row[pkName]] || row[pkName]);
50
- return insertIds;
51
- }
52
- return null;
53
- };
54
-
55
- for (const [tblName, changes] of Object.entries(allChanges)) {
56
- const table = Table.findOne({ name: tblName });
57
- if (!table) throw new Error(`The table '${tblName}' does not exists`);
58
- const pkName = table.pk_name;
59
- for (const fk of table.getForeignKeys()) {
60
- const targetTrans = allTranslations[fk.reftable_name];
61
- const ids = rowIds(fk, targetTrans, table.name, pkName, changes);
62
- if (ids?.length > 0) {
63
- for (const [from, to] of Object.entries(targetTrans)) {
64
- await db.query(
65
- `update ${schema}"${db.sqlsanitize(tblName)}" set "${db.sqlsanitize(
66
- fk.name
67
- )}" = ${to}
68
- where "${db.sqlsanitize(
69
- fk.name
70
- )}" = ${from} and "${db.sqlsanitize(pkName)}" in (${ids.join(
71
- ","
72
- )})`
73
- );
74
- }
75
- }
76
- }
77
- }
78
- };
79
-
80
41
  const checkConstraints = async (table, row) => {
81
42
  const uniques = table.constraints.filter((c) => c.type === "Unique");
82
43
  for (const { configuration } of uniques) {
@@ -90,191 +51,307 @@ const checkConstraints = async (table, row) => {
90
51
  return null;
91
52
  };
92
53
 
93
- const applyInserts = async (changes, syncTimestamp, user) => {
94
- const schema = db.getTenantSchemaPrefix();
95
- const allTranslations = {};
96
- const allUniqueConflicts = {};
97
- for (const [tblName, vals] of Object.entries(changes)) {
98
- const table = Table.findOne({ name: tblName });
99
- if (!table) throw new Error(`The table '${tblName}' does not exists`);
54
+ /**
55
+ * internal helper class
56
+ */
57
+ class SyncHelper {
58
+ constructor(changes, oldSyncTimestamp, newSyncTimestamp, user, directory) {
59
+ this.changes = changes;
60
+ this.oldSyncTimestamp = oldSyncTimestamp;
61
+ this.newSyncTimestamp = newSyncTimestamp;
62
+ this.user = user;
63
+ this.directory = directory;
64
+ this.allTranslations = {};
65
+ this.allUniqueConflicts = {};
66
+ this.allDataConflicts = {};
67
+ this.inTransaction = false;
68
+ }
69
+
70
+ async doSync() {
71
+ let returnCode = 0;
100
72
  try {
101
- if (vals.inserts?.length > 0) {
102
- const pkName = table.pk_name;
103
- await db.query(
104
- `alter table ${schema}"${db.sqlsanitize(
105
- tblName
106
- )}" disable trigger all`
107
- );
108
- const translations = {};
109
- const uniqueConflicts = [];
110
- for (const insert of vals.inserts || []) {
111
- const row = pickFields(table, pkName, insert);
112
- const conflictRow = await checkConstraints(table, row);
113
- if (!conflictRow) {
114
- const newId = await table.insertRow(
115
- row,
116
- user,
117
- undefined,
118
- true,
119
- syncTimestamp
73
+ // db operations
74
+ await db.begin();
75
+ this.inTransaction = true;
76
+ await this.applyInserts();
77
+ await this.translateInsertFks();
78
+ await this.applyUpdates();
79
+ await this.applyDeletes();
80
+ await db.commit();
81
+
82
+ // write output files
83
+ await this.writeTranslatedIds();
84
+ await this.writeUniqueConflicts();
85
+ await this.writeDataConflicts();
86
+ } catch (error) {
87
+ returnCode = 1;
88
+ getState().log(2, `Unable to sync: ${error.message}`);
89
+ await this.writeErrorFile(error.message);
90
+ if (this.inTransaction) await db.rollback();
91
+ }
92
+ return returnCode;
93
+ }
94
+
95
+ async translateInsertFks() {
96
+ const schema = db.getTenantSchemaPrefix();
97
+ const rowIds = (fk, targetTrans, tblName, pkName, changes) => {
98
+ if (Object.keys(targetTrans || {}).length > 0) {
99
+ const srcTrans = this.allTranslations[tblName] || {};
100
+ // ids with a fk where the target was translated
101
+ const insertIds = (changes.inserts || [])
102
+ .filter((row) => targetTrans[row[fk.name]] !== undefined)
103
+ .map((row) => srcTrans[row[pkName]] || row[pkName]);
104
+ return insertIds;
105
+ }
106
+ return null;
107
+ };
108
+
109
+ for (const [tblName, changes] of Object.entries(this.changes)) {
110
+ const table = Table.findOne({ name: tblName });
111
+ if (!table) throw new Error(`The table '${tblName}' does not exists`);
112
+ const pkName = table.pk_name;
113
+ for (const fk of table.getForeignKeys()) {
114
+ const targetTrans = this.allTranslations[fk.reftable_name];
115
+ const ids = rowIds(fk, targetTrans, table.name, pkName, changes);
116
+ if (ids?.length > 0) {
117
+ for (const [from, to] of Object.entries(targetTrans)) {
118
+ await db.query(
119
+ `update ${schema}"${db.sqlsanitize(tblName)}" set "${db.sqlsanitize(
120
+ fk.name
121
+ )}" = ${to}
122
+ where "${db.sqlsanitize(
123
+ fk.name
124
+ )}" = ${from} and "${db.sqlsanitize(pkName)}" in (${ids.join(
125
+ ","
126
+ )})`
120
127
  );
121
- if (!newId) throw new Error(`Unable to insert into ${tblName}`);
122
- else if (newId !== insert[pkName])
123
- translations[insert[pkName]] = newId;
124
- } else {
125
- translations[insert[pkName]] = conflictRow[pkName];
126
- uniqueConflicts.push(conflictRow);
127
128
  }
128
129
  }
129
- allTranslations[tblName] = translations;
130
- allUniqueConflicts[tblName] = uniqueConflicts;
131
- await db.query(
132
- `alter table ${schema}"${db.sqlsanitize(tblName)}" enable trigger all`
133
- );
134
130
  }
135
- } catch (error) {
136
- throw new Error(table.normalise_error_message(error.message));
137
131
  }
138
132
  }
139
- return { allTranslations, allUniqueConflicts };
140
- };
141
133
 
142
- const applyUpdates = async (changes, allTranslations, syncTimestamp, user) => {
143
- for (const [tblName, vals] of Object.entries(changes)) {
144
- if (vals.updates?.length > 0) {
134
+ async applyInserts() {
135
+ const schema = db.getTenantSchemaPrefix();
136
+ for (const [tblName, vals] of Object.entries(this.changes)) {
145
137
  const table = Table.findOne({ name: tblName });
146
138
  if (!table) throw new Error(`The table '${tblName}' does not exists`);
147
139
  try {
148
- const pkName = table.pk_name;
149
- const insertTranslations = allTranslations[tblName];
150
- for (const update of vals.updates) {
151
- const row = pickFields(table, pkName, update, true);
152
- if (insertTranslations?.[row[pkName]])
153
- row[pkName] = insertTranslations[row[pkName]];
154
- for (const fk of table.getForeignKeys()) {
155
- const oldVal = row[fk.name];
156
- if (oldVal) {
157
- const newVal = allTranslations[fk.reftable_name]?.[oldVal];
158
- if (newVal) row[fk.name] = newVal;
140
+ if (vals.inserts?.length > 0) {
141
+ const pkName = table.pk_name;
142
+ await db.query(
143
+ `alter table ${schema}"${db.sqlsanitize(
144
+ tblName
145
+ )}" disable trigger all`
146
+ );
147
+ const translations = {};
148
+ const uniqueConflicts = [];
149
+ for (const insert of vals.inserts || []) {
150
+ const row = pickFields(table, pkName, insert);
151
+ const conflictRow = await checkConstraints(table, row);
152
+ if (!conflictRow) {
153
+ const newId = await table.insertRow(
154
+ row,
155
+ this.user,
156
+ undefined,
157
+ true,
158
+ this.newSyncTimestamp
159
+ );
160
+ if (!newId) throw new Error(`Unable to insert into ${tblName}`);
161
+ else if (newId !== insert[pkName])
162
+ translations[insert[pkName]] = newId;
163
+ } else {
164
+ translations[insert[pkName]] = conflictRow[pkName];
165
+ uniqueConflicts.push(conflictRow);
159
166
  }
160
167
  }
161
- const result = await table.updateRow(
162
- row,
163
- row[pkName],
164
- user,
165
- true,
166
- undefined,
167
- undefined,
168
- syncTimestamp
168
+ this.allTranslations[tblName] = translations;
169
+ this.allUniqueConflicts[tblName] = uniqueConflicts;
170
+ await db.query(
171
+ `alter table ${schema}"${db.sqlsanitize(tblName)}" enable trigger all`
169
172
  );
170
- if (result) throw new Error(`Unable to update ${tblName}: ${result}`);
171
173
  }
172
174
  } catch (error) {
173
175
  throw new Error(table.normalise_error_message(error.message));
174
176
  }
175
177
  }
176
178
  }
177
- };
178
179
 
179
- const applyDeletes = async (changes, user) => {
180
- for (const [tblName, vals] of Object.entries(changes)) {
181
- const table = Table.findOne({ name: tblName });
182
- if (!table) throw new Error(`The table '${tblName}' does not exists`);
183
- const pkName = table.pk_name;
184
- if (vals.deletes?.length > 0) {
185
- const delIds = [];
186
- const latestInfos = await table.latestSyncInfos(
187
- vals.deletes.map((del) => del[pkName])
188
- );
189
- const refToInfo = {};
190
- for (const info of latestInfos) {
191
- refToInfo[info.ref] = info;
192
- }
193
- for (const del of vals.deletes) {
194
- const appTimestamp = new Date(del.last_modified);
195
- const info = refToInfo[del[pkName]];
196
- if (!info || appTimestamp >= info.last_modified)
197
- delIds.push(del[pkName]);
198
- }
199
- if (delIds.length > 0) {
200
- await table.deleteRows({ [pkName]: { in: delIds } }, user, true);
201
- if ((await table.countRows({ [pkName]: { in: delIds } })) !== 0)
202
- throw new Error(
203
- `Unable to delete in '${tblName}': Some rows were not deleted`
180
+ async applyUpdates() {
181
+ for (const [tblName, vals] of Object.entries(this.changes)) {
182
+ if (vals.updates?.length > 0) {
183
+ const table = Table.findOne({ name: tblName });
184
+ if (!table) throw new Error(`The table '${tblName}' does not exists`);
185
+ try {
186
+ const dataConflicts = [];
187
+ const pkName = table.pk_name;
188
+ const insertTranslations = this.allTranslations[tblName];
189
+
190
+ const collected = [];
191
+ for (const update of vals.updates) {
192
+ const row = pickFields(table, pkName, update, true);
193
+ if (insertTranslations?.[row[pkName]])
194
+ row[pkName] = insertTranslations[row[pkName]];
195
+ for (const fk of table.getForeignKeys()) {
196
+ const oldVal = row[fk.name];
197
+ if (oldVal) {
198
+ const newVal = this.allTranslations[fk.reftable_name]?.[oldVal];
199
+ if (newVal) row[fk.name] = newVal;
200
+ }
201
+ }
202
+ collected.push(row);
203
+ }
204
+
205
+ const syncInfos = await table.latestSyncInfos(
206
+ collected.map((row) => row[pkName])
204
207
  );
208
+ const infoLookup = {};
209
+ for (const info of syncInfos) {
210
+ infoLookup[info.ref] = info;
211
+ }
212
+
213
+ for (const row of collected) {
214
+ // check conflict on row level
215
+ const incomingTimestamp = new Date(this.oldSyncTimestamp);
216
+ const syncInfo = infoLookup[row[pkName]];
217
+ if (syncInfo?.last_modified > incomingTimestamp) {
218
+ const conflictUpdates = {
219
+ id: row[pkName],
220
+ };
221
+ const currentRow = await table.getRow({ [pkName]: row[pkName] });
222
+ for (const [field, ts] of Object.entries(
223
+ syncInfo?.updated_fields || {}
224
+ )) {
225
+ if (row[field] !== undefined) {
226
+ const fieldTimestamp = new Date(ts);
227
+ if (incomingTimestamp < fieldTimestamp) {
228
+ // app-syncTimestamp is older than server-field-timestamp
229
+ conflictUpdates[field] = currentRow[field];
230
+ delete row[field];
231
+ }
232
+ }
233
+ }
234
+ if (Object.keys(conflictUpdates).length > 1)
235
+ dataConflicts.push(conflictUpdates);
236
+ }
237
+
238
+ const result = await table.updateRow(
239
+ row,
240
+ row[pkName],
241
+ this.user,
242
+ true,
243
+ undefined,
244
+ undefined,
245
+ this.newSyncTimestamp
246
+ );
247
+ if (result)
248
+ throw new Error(`Unable to update ${tblName}: ${result}`);
249
+ }
250
+ if (dataConflicts.length > 0)
251
+ this.allDataConflicts[tblName] = dataConflicts;
252
+ } catch (error) {
253
+ throw new Error(table.normalise_error_message(error.message));
254
+ }
205
255
  }
206
256
  }
207
257
  }
208
- };
209
258
 
210
- const writeTranslatedIds = async (translatedIds, directory) => {
211
- const writeName = path.join(directory, "translated-ids.out");
212
- await fs.writeFile(writeName, JSON.stringify(translatedIds));
213
- await fs.rename(writeName, path.join(directory, "translated-ids.json"));
214
- };
259
+ async applyDeletes() {
260
+ for (const [tblName, vals] of Object.entries(this.changes)) {
261
+ const table = Table.findOne({ name: tblName });
262
+ if (!table) throw new Error(`The table '${tblName}' does not exists`);
263
+ const pkName = table.pk_name;
264
+ if (vals.deletes?.length > 0) {
265
+ const delIds = [];
266
+ const latestInfos = await table.latestSyncInfos(
267
+ vals.deletes.map((del) => del[pkName])
268
+ );
269
+ const refToInfo = {};
270
+ for (const info of latestInfos) {
271
+ refToInfo[info.ref] = info;
272
+ }
273
+ for (const del of vals.deletes) {
274
+ const appTimestamp = new Date(del.last_modified);
275
+ const info = refToInfo[del[pkName]];
276
+ if (!info || appTimestamp >= info.last_modified)
277
+ delIds.push(del[pkName]);
278
+ }
279
+ if (delIds.length > 0) {
280
+ await table.deleteRows({ [pkName]: { in: delIds } }, this.user, true);
281
+ if ((await table.countRows({ [pkName]: { in: delIds } })) !== 0)
282
+ throw new Error(
283
+ `Unable to delete in '${tblName}': Some rows were not deleted`
284
+ );
285
+ }
286
+ }
287
+ }
288
+ }
215
289
 
216
- const writeUniqueConflicts = async (uniqueConflicts, directory) => {
217
- const writeName = path.join(directory, "unique-conflicts.out");
218
- await fs.writeFile(writeName, JSON.stringify(uniqueConflicts));
219
- await fs.rename(writeName, path.join(directory, "unique-conflicts.json"));
220
- };
290
+ async writeTranslatedIds() {
291
+ const writeName = path.join(this.directory, "translated-ids.out");
292
+ await fs.writeFile(writeName, JSON.stringify(this.allTranslations));
293
+ await fs.rename(
294
+ writeName,
295
+ path.join(this.directory, "translated-ids.json")
296
+ );
297
+ }
221
298
 
222
- const writeErrorFile = async (message, directory) => {
223
- const writeName = path.join(directory, "error.out");
224
- await fs.writeFile(writeName, JSON.stringify({ message }));
225
- await fs.rename(writeName, path.join(directory, "error.json"));
226
- };
299
+ async writeUniqueConflicts() {
300
+ const writeName = path.join(this.directory, "unique-conflicts.out");
301
+ await fs.writeFile(writeName, JSON.stringify(this.allUniqueConflicts));
302
+ await fs.rename(
303
+ writeName,
304
+ path.join(this.directory, "unique-conflicts.json")
305
+ );
306
+ }
307
+
308
+ async writeDataConflicts() {
309
+ const writeName = path.join(this.directory, "data-conflicts.out");
310
+ await fs.writeFile(writeName, JSON.stringify(this.allDataConflicts));
311
+ await fs.rename(
312
+ writeName,
313
+ path.join(this.directory, "data-conflicts.json")
314
+ );
315
+ }
316
+
317
+ async writeErrorFile(message) {
318
+ const writeName = path.join(this.directory, "error.out");
319
+ await fs.writeFile(writeName, JSON.stringify({ message }));
320
+ await fs.rename(writeName, path.join(this.directory, "error.json"));
321
+ }
322
+ }
227
323
 
228
324
  /**
229
- *
325
+ * CLI command class
230
326
  */
231
327
  class SyncUploadData extends Command {
232
328
  async run() {
233
- let returnCode = 0,
234
- inTransaction = false;
235
329
  const { flags } = await this.parse(SyncUploadData);
236
330
  if (db.is_it_multi_tenant() && flags.tenantAppName) {
237
331
  await init_multi_tenant(loadAllPlugins, true, [flags.tenantAppName]);
238
332
  }
239
- const doSync = async () => {
240
- try {
241
- const changes = JSON.parse(
333
+ const fn = async () => {
334
+ await loadAllPlugins();
335
+ const helper = new SyncHelper(
336
+ JSON.parse(
242
337
  await fs.readFile(path.join(flags.directory, "changes.json"))
243
- );
244
- const syncTimestamp = flags.syncTimestamp;
245
- const user = flags.userEmail
338
+ ),
339
+ flags.oldSyncTimestamp,
340
+ flags.newSyncTimestamp,
341
+ flags.userEmail
246
342
  ? await User.findOne({ email: flags.userEmail })
247
- : undefined;
248
- await loadAllPlugins();
249
- await db.begin();
250
- inTransaction = true;
251
- const { allTranslations, allUniqueConflicts } = await applyInserts(
252
- changes,
253
- syncTimestamp,
254
- user
255
- );
256
- await translateInsertFks(changes, allTranslations);
257
- await applyUpdates(changes, allTranslations, syncTimestamp, user);
258
- await applyDeletes(changes, user);
259
- await db.commit();
260
- await writeTranslatedIds(allTranslations, flags.directory);
261
- await writeUniqueConflicts(allUniqueConflicts, flags.directory);
262
- } catch (error) {
263
- returnCode = 1;
264
- getState().log(2, `Unable to sync: ${error.message}`);
265
- await writeErrorFile(error.message, flags.directory);
266
- if (inTransaction) await db.rollback();
267
- } finally {
268
- process.exit(returnCode);
269
- }
343
+ : undefined,
344
+ flags.directory
345
+ );
346
+ process.exit(await helper.doSync());
270
347
  };
271
348
  if (
272
349
  flags.tenantAppName &&
273
350
  flags.tenantAppName !== db.connectObj.default_schema
274
351
  ) {
275
- await db.runWithTenant(flags.tenantAppName, doSync);
352
+ await db.runWithTenant(flags.tenantAppName, fn);
276
353
  } else {
277
- await doSync();
354
+ await fn();
278
355
  }
279
356
  }
280
357
  }
@@ -297,11 +374,16 @@ SyncUploadData.flags = {
297
374
  string: "directory",
298
375
  description: "directory name for input output data",
299
376
  }),
300
- syncTimestamp: Flags.integer({
301
- name: "syncTimestamp",
302
- string: "syncTimestamp",
377
+ newSyncTimestamp: Flags.integer({
378
+ name: "newSyncTimestamp",
379
+ string: "newSyncTimestamp",
303
380
  description: "new timestamp for the sync_info rows",
304
381
  }),
382
+ oldSyncTimestamp: Flags.integer({
383
+ name: "oldSyncTimestamp",
384
+ string: "oldSyncTimestamp",
385
+ description: "TODO",
386
+ }),
305
387
  };
306
388
 
307
389
  module.exports = SyncUploadData;