@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.
- package/README.md +88 -58
- package/npm-shrinkwrap.json +1128 -1293
- package/oclif.manifest.json +65 -3
- package/package.json +8 -8
- package/src/commands/build-app.js +23 -0
- package/src/commands/dev/release.js +11 -0
- package/src/commands/pre-install-modules.js +105 -0
- package/src/commands/sync-upload-data.js +264 -182
|
@@ -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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
|
240
|
-
|
|
241
|
-
|
|
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
|
-
|
|
245
|
-
|
|
338
|
+
),
|
|
339
|
+
flags.oldSyncTimestamp,
|
|
340
|
+
flags.newSyncTimestamp,
|
|
341
|
+
flags.userEmail
|
|
246
342
|
? await User.findOne({ email: flags.userEmail })
|
|
247
|
-
: undefined
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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,
|
|
352
|
+
await db.runWithTenant(flags.tenantAppName, fn);
|
|
276
353
|
} else {
|
|
277
|
-
await
|
|
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
|
-
|
|
301
|
-
name: "
|
|
302
|
-
string: "
|
|
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;
|