@saltcorn/mobile-app 1.6.0-beta.5 → 1.6.0-beta.6

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@saltcorn/mobile-app",
3
3
  "displayName": "Saltcorn mobile app",
4
- "version": "1.6.0-beta.5",
4
+ "version": "1.6.0-beta.6",
5
5
  "description": "Saltcorn mobile app for Android and iOS",
6
6
  "main": "index.js",
7
7
  "scripts": {
@@ -102,49 +102,77 @@ export async function updateUserDefinedTables() {
102
102
  }
103
103
  }
104
104
 
105
+ const createSyncInfoIndexes = async (safeName) => {
106
+ const tbl = `${safeName}_sync_info`;
107
+ await saltcorn.data.db.query(
108
+ `CREATE INDEX IF NOT EXISTS ${tbl}_ref_index ON ${tbl}(ref)`
109
+ );
110
+ await saltcorn.data.db.query(
111
+ `CREATE INDEX IF NOT EXISTS ${tbl}_lm_index ON ${tbl}(last_modified)`
112
+ );
113
+ await saltcorn.data.db.query(
114
+ `CREATE INDEX IF NOT EXISTS ${tbl}_deleted_index ON ${tbl}(deleted)`
115
+ );
116
+ await saltcorn.data.db.query(
117
+ `CREATE INDEX IF NOT EXISTS ${tbl}_ml_index ON ${tbl}(modified_local)`
118
+ );
119
+ };
120
+
121
+ // Each entry migrates sync_info tables from version (index) to version (index + 1).
122
+ // To add a migration: append a function and bump SYNC_INFO_SCHEMA_VERSION.
123
+ const SYNC_INFO_SCHEMA_VERSION = 1;
124
+
125
+ const syncInfoMigrations = [
126
+ // v0 → v1: ref integer → ref text (UUID primary key support)
127
+ // SQLite does not support ALTER COLUMN, so we rename → recreate → copy → drop
128
+ async (safeName) => {
129
+ const tbl = `${safeName}_sync_info`;
130
+ const tmp = `${tbl}_migrate_tmp`;
131
+ await saltcorn.data.db.query(`ALTER TABLE "${tbl}" RENAME TO "${tmp}"`);
132
+ await saltcorn.data.db.query(`CREATE TABLE "${tbl}" (
133
+ ref text,
134
+ last_modified timestamp,
135
+ deleted integer,
136
+ modified_local integer
137
+ )`);
138
+ await saltcorn.data.db.query(
139
+ `INSERT INTO "${tbl}" (ref, last_modified, deleted, modified_local)
140
+ SELECT CAST(ref AS TEXT), last_modified, deleted, modified_local FROM "${tmp}"`
141
+ );
142
+ await saltcorn.data.db.query(`DROP TABLE "${tmp}"`);
143
+ await createSyncInfoIndexes(safeName);
144
+ },
145
+ ];
146
+
147
+ export async function migrateSyncInfoTables(synchTbls) {
148
+ const state = saltcorn.data.state.getState();
149
+ const currentVersion =
150
+ (await state.getConfig("sync_info_schema_version")) ?? -1;
151
+ if (currentVersion >= SYNC_INFO_SCHEMA_VERSION) return;
152
+ for (const synchTbl of synchTbls) {
153
+ const safeName = saltcorn.data.db.sqlsanitize(synchTbl);
154
+ if (!(await saltcorn.data.db.tableExists(`${safeName}_sync_info`)))
155
+ continue;
156
+ for (let v = currentVersion + 1; v <= SYNC_INFO_SCHEMA_VERSION; v++) {
157
+ await syncInfoMigrations[v - 1]?.(safeName);
158
+ }
159
+ }
160
+ await state.setConfig("sync_info_schema_version", SYNC_INFO_SCHEMA_VERSION);
161
+ }
162
+
105
163
  export async function createSyncInfoTables(synchTbls) {
106
- const infoTbls = (await saltcorn.data.db.listTables()).filter(({ name }) => {
107
- name.endsWith("_sync_info");
108
- });
164
+ await migrateSyncInfoTables(synchTbls);
109
165
  for (const synchTbl of synchTbls) {
110
- if (!infoTbls.find(({ name }) => name.startsWith(synchTbl))) {
111
- await saltcorn.data.db
112
- .query(`CREATE TABLE IF NOT EXISTS ${saltcorn.data.db.sqlsanitize(
113
- synchTbl
114
- )}_sync_info (
115
- ref integer,
116
- last_modified timestamp,
117
- deleted integer,
118
- modified_local integer
166
+ const safeName = saltcorn.data.db.sqlsanitize(synchTbl);
167
+ const tblName = `${safeName}_sync_info`;
168
+ if (!(await saltcorn.data.db.tableExists(tblName))) {
169
+ await saltcorn.data.db.query(`CREATE TABLE IF NOT EXISTS ${tblName} (
170
+ ref text,
171
+ last_modified timestamp,
172
+ deleted integer,
173
+ modified_local integer
119
174
  )`);
120
- await saltcorn.data.db.query(
121
- `CREATE INDEX IF NOT EXISTS ${saltcorn.data.db.sqlsanitize(
122
- synchTbl
123
- )}_sync_info_ref_index on ${saltcorn.data.db.sqlsanitize(
124
- synchTbl
125
- )}_sync_info(ref);`
126
- );
127
- await saltcorn.data.db.query(
128
- `CREATE INDEX IF NOT EXISTS ${saltcorn.data.db.sqlsanitize(
129
- synchTbl
130
- )}_sync_info_lm_index on ${saltcorn.data.db.sqlsanitize(
131
- synchTbl
132
- )}_sync_info(last_modified);`
133
- );
134
- await saltcorn.data.db.query(
135
- `CREATE INDEX IF NOT EXISTS ${saltcorn.data.db.sqlsanitize(
136
- synchTbl
137
- )}_sync_info_deleted_index on ${saltcorn.data.db.sqlsanitize(
138
- synchTbl
139
- )}_sync_info(deleted);`
140
- );
141
- await saltcorn.data.db.query(
142
- `CREATE INDEX IF NOT EXISTS ${saltcorn.data.db.sqlsanitize(
143
- synchTbl
144
- )}_sync_info_ml_index on ${saltcorn.data.db.sqlsanitize(
145
- synchTbl
146
- )}_sync_info(modified_local);`
147
- );
175
+ await createSyncInfoIndexes(safeName);
148
176
  }
149
177
  }
150
178
  }
@@ -178,12 +206,6 @@ export async function updateDb(tablesJSON) {
178
206
  });
179
207
  }
180
208
 
181
- export async function getTableIds(tableNames) {
182
- return (await saltcorn.data.models.Table.find())
183
- .filter((table) => tableNames.indexOf(table.name) > -1)
184
- .map((table) => table.id);
185
- }
186
-
187
209
  export async function createJwtTable() {
188
210
  await saltcorn.data.db.query(`CREATE TABLE IF NOT EXISTS ${jwtTableName} (
189
211
  jwt VARCHAR(500)
@@ -11,6 +11,9 @@ import {
11
11
  removeLoadSpinner,
12
12
  } from "./common";
13
13
 
14
+ // Safely embed a ref value (integer or UUID string) in raw SQL
15
+ const sqlRef = (ref) => `'${String(ref).replace(/'/g, "''")}'`;
16
+
14
17
  const setUploadStarted = async (started, time) => {
15
18
  const state = saltcorn.data.state.getState();
16
19
  const oldSession = await state.getConfig("last_offline_session");
@@ -35,7 +38,7 @@ const prepare = async () => {
35
38
  const { synchedTables } = state.mobileConfig;
36
39
  const syncInfos = {};
37
40
  for (const tblName of synchedTables) {
38
- const syncInfo = { maxLoadedId: 0 };
41
+ const syncInfo = { lastModifiedAt: 0, lastRef: "" };
39
42
  const maxLm = await maxLastModified(tblName);
40
43
  if (maxLm) syncInfo.syncFrom = maxLm.valueOf();
41
44
  syncInfos[tblName] = syncInfo;
@@ -59,20 +62,20 @@ const insertRemoteData = async (table, rows, syncTimestamp) => {
59
62
  const infos = await saltcorn.data.db.select(
60
63
  `${saltcorn.data.db.sqlsanitize(tblName)}_sync_info`,
61
64
  {
62
- ref: rest[pkName],
65
+ ref: String(rest[pkName]),
63
66
  }
64
67
  );
65
68
  if (infos.length > 0) {
66
69
  await saltcorn.data.db.query(
67
70
  `update "${saltcorn.data.db.sqlsanitize(tblName)}_sync_info"
68
71
  set last_modified=${last_modified}, deleted=false, modified_local = false
69
- where ref=${rest[pkName]}`
72
+ where ref=${sqlRef(rest[pkName])}`
70
73
  );
71
74
  } else {
72
75
  await saltcorn.data.db.insert(
73
76
  `${saltcorn.data.db.sqlsanitize(tblName)}_sync_info`,
74
77
  {
75
- ref,
78
+ ref: String(ref),
76
79
  last_modified: syncTimestamp,
77
80
  deleted: false,
78
81
  modified_local: false,
@@ -103,14 +106,15 @@ const syncRemoteData = async (syncInfos, syncTimestamp) => {
103
106
  loadUntil: syncTimestamp,
104
107
  },
105
108
  });
106
- for (const [tblName, { rows, maxLoadedId }] of Object.entries(
109
+ for (const [tblName, { rows, maxModifiedAt, maxRef }] of Object.entries(
107
110
  loadResp.data
108
111
  )) {
109
112
  if (rows?.length > 0) {
110
113
  const table = getTable(tblName);
111
114
  hasMoreData = true;
112
115
  await insertRemoteData(table, rows, syncTimestamp);
113
- syncInfos[tblName].maxLoadedId = maxLoadedId;
116
+ syncInfos[tblName].lastModifiedAt = maxModifiedAt ?? 0;
117
+ syncInfos[tblName].lastRef = maxRef ?? "";
114
118
  }
115
119
  }
116
120
  }
@@ -123,7 +127,7 @@ const prepDeletes = async (table, deletes) => {
123
127
  // don't delete if it's local modifed or unsynched
124
128
  const tblConflicts = await saltcorn.data.db.query(
125
129
  `select ref from "${saltcorn.data.db.sqlsanitize(tblName)}_sync_info"
126
- where ref in (${deletes.map(({ ref }) => ref).join(",")}) and
130
+ where ref in (${deletes.map(({ ref }) => sqlRef(ref)).join(",")}) and
127
131
  (last_modified is null or modified_local = true)`
128
132
  );
129
133
  if (tblConflicts.rows.length > 0) {
@@ -132,7 +136,7 @@ const prepDeletes = async (table, deletes) => {
132
136
  await saltcorn.data.db.query(
133
137
  `update "${saltcorn.data.db.sqlsanitize(tblName)}_sync_info"
134
138
  set last_modified = null, modified_local = true
135
- where ref in (${conflicts.join(",")})`
139
+ where ref in (${conflicts.map(sqlRef).join(",")})`
136
140
  );
137
141
  const conflictsSet = new Set(conflicts);
138
142
  result = result.filter((del) => !conflictsSet.has(del.ref));
@@ -153,10 +157,12 @@ const prepDeletes = async (table, deletes) => {
153
157
  )}" as data_tbl join "${saltcorn.data.db.sqlsanitize(
154
158
  srcTbl.name
155
159
  )}_sync_info" as info_tbl
156
- on data_tbl."${saltcorn.data.db.sqlsanitize(pkName)}" = info_tbl.ref
160
+ on CAST(info_tbl.ref AS TEXT) = CAST(data_tbl."${saltcorn.data.db.sqlsanitize(
161
+ pkName
162
+ )}" AS TEXT)
157
163
  where data_tbl."${saltcorn.data.db.sqlsanitize(
158
164
  field.name
159
- )}" in (${result.map(({ ref }) => ref).join(",")})
165
+ )}" in (${result.map(({ ref }) => sqlRef(ref)).join(",")})
160
166
  and (info_tbl.last_modified is null or info_tbl.modified_local = true)`
161
167
  );
162
168
  if (fkConflicts.rows.length > 0) {
@@ -164,12 +170,12 @@ const prepDeletes = async (table, deletes) => {
164
170
  const conflicts = fkConflicts.rows.map(
165
171
  (conflict) => conflict[field.name]
166
172
  );
167
- const conflictsSet = new Set(conflicts);
168
- result = result.filter((del) => !conflictsSet.has(del.ref));
173
+ const conflictsSet = new Set(conflicts.map(String));
174
+ result = result.filter((del) => !conflictsSet.has(String(del.ref)));
169
175
  await saltcorn.data.db.query(
170
176
  `update "${saltcorn.data.db.sqlsanitize(tblName)}_sync_info"
171
177
  set last_modified = null, modified_local = true
172
- where ref in (${conflicts.join(",")})`
178
+ where ref in (${conflicts.map(sqlRef).join(",")})`
173
179
  );
174
180
  }
175
181
  }
@@ -184,16 +190,16 @@ const applyDeletes = async (allDeletes, syncTimestamp) => {
184
190
  const pkName = table.pk_name;
185
191
  const safeDeletes = await prepDeletes(table, deletes);
186
192
  if (safeDeletes.length > 0) {
187
- const delIds = safeDeletes.map(({ ref }) => ref).join(",");
193
+ const delRefs = safeDeletes.map(({ ref }) => sqlRef(ref)).join(",");
188
194
  await saltcorn.data.db.query(
189
195
  `delete from "${saltcorn.data.db.sqlsanitize(
190
196
  tblName
191
- )}" where "${saltcorn.data.db.sqlsanitize(pkName)}" in (${delIds})`
197
+ )}" where "${saltcorn.data.db.sqlsanitize(pkName)}" in (${delRefs})`
192
198
  );
193
199
  await saltcorn.data.db.query(
194
200
  `update "${saltcorn.data.db.sqlsanitize(tblName)}_sync_info"
195
201
  set deleted = true, last_modified = ${syncTimestamp}, modified_local = false
196
- where ref in (${delIds}) and deleted = false`
202
+ where ref in (${delRefs}) and deleted = false`
197
203
  );
198
204
  }
199
205
  }
@@ -226,9 +232,11 @@ const loadOfflineChanges = async (synchedTbls) => {
226
232
  from "${saltcorn.data.db.sqlsanitize(
227
233
  synchedTbl
228
234
  )}_sync_info" as info_tbl left join "${saltcorn.data.db.sqlsanitize(
229
- synchedTbl
230
- )}" as data_tbl
231
- on info_tbl.ref = data_tbl."${saltcorn.data.db.sqlsanitize(pkName)}"
235
+ synchedTbl
236
+ )}" as data_tbl
237
+ on CAST(info_tbl.ref AS TEXT) = CAST(data_tbl."${saltcorn.data.db.sqlsanitize(
238
+ pkName
239
+ )}" AS TEXT)
232
240
  where info_tbl.modified_local = true`
233
241
  );
234
242
  const inserts = [];
@@ -280,8 +288,8 @@ const handleTranslatedIds = async (allUniqueConflicts, allTranslations) => {
280
288
  await saltcorn.data.db.update(tblName, { [table.pk_name]: to }, from);
281
289
  await saltcorn.data.db.query(
282
290
  `update "${saltcorn.data.db.sqlsanitize(tblName)}_sync_info"
283
- set ref = ${to}
284
- where ref = ${from} and deleted = false`
291
+ set ref = ${sqlRef(to)}
292
+ where ref = ${sqlRef(from)} and deleted = false`
285
293
  );
286
294
  }
287
295
  for (const fk of fks) {
@@ -311,7 +319,7 @@ const handleUniqueConflicts = async (uniqueConflicts, translatedIds) => {
311
319
  if (to === conflict[pkName]) {
312
320
  await table.deleteRows({ [pkName]: from });
313
321
  await saltcorn.data.db.deleteWhere(`${table.name}_sync_info`, {
314
- ref: from,
322
+ ref: String(from),
315
323
  });
316
324
  }
317
325
  }
@@ -376,11 +384,11 @@ const updateSyncInfos = async (
376
384
  )
377
385
  );
378
386
  const values = refIds.map(
379
- (ref) => `(${ref}, ${syncTimestamp}, ${deleted}, false)`
387
+ (ref) => `(${sqlRef(ref)}, ${syncTimestamp}, ${deleted}, false)`
380
388
  );
381
389
  await saltcorn.data.db.query(
382
390
  `delete from "${saltcorn.data.db.sqlsanitize(tblName)}_sync_info"
383
- where ref in (${refIds.join(",")})`
391
+ where ref in (${refIds.map(sqlRef).join(",")})`
384
392
  );
385
393
  await saltcorn.data.db.query(
386
394
  `insert into "${saltcorn.data.db.sqlsanitize(tblName)}_sync_info"
@@ -714,7 +722,9 @@ export async function hasOfflineRows() {
714
722
  tblName
715
723
  )}_sync_info" as info_tbl
716
724
  join "${saltcorn.data.db.sqlsanitize(tblName)}" as data_tbl
717
- on info_tbl.ref = data_tbl."${saltcorn.data.db.sqlsanitize(pkName)}"
725
+ on CAST(info_tbl.ref AS TEXT) = CAST(data_tbl."${saltcorn.data.db.sqlsanitize(
726
+ pkName
727
+ )}" AS TEXT)
718
728
  where info_tbl.modified_local = true`
719
729
  );
720
730
  if (rows?.length > 0 && parseInt(rows[0].total) > 0) return true;
package/src/init.js CHANGED
@@ -11,7 +11,6 @@ import {
11
11
  createSyncInfoTables,
12
12
  dbUpdateNeeded,
13
13
  updateDb,
14
- getTableIds,
15
14
  createJwtTable,
16
15
  } from "./helpers/db_schema.js";
17
16
  import { publicLogin, checkJWT } from "./helpers/auth.js";
@@ -438,9 +437,6 @@ export async function init(mobileConfig) {
438
437
  await state.refresh_triggers();
439
438
  await state.refresh_config();
440
439
  await state.refresh_codepages();
441
- state.mobileConfig.localTableIds = await getTableIds(
442
- mobileConfig.localUserTables
443
- );
444
440
  await state.setConfig("base_url", mobileConfig.server_path);
445
441
  await initI18Next();
446
442
  state.mobileConfig.encodedSiteLogo = await readSiteLogo();
@@ -14,10 +14,7 @@ export const loadTableRows = async (context) => {
14
14
  const mobileConfig = saltcorn.data.state.getState().mobileConfig;
15
15
  const table = saltcorn.data.models.Table.findOne({ name: tableName });
16
16
  if (!table) throw new Error(`The table '${tableName}' does not exist.`);
17
- if (
18
- mobileConfig.isOfflineMode ||
19
- mobileConfig.localTableIds.indexOf(table.id) >= 0
20
- ) {
17
+ if (mobileConfig.isOfflineMode) {
21
18
  const rows = await table.getRows(query, {
22
19
  orderBy: table.pk_name,
23
20
  forUser: mobileConfig.user,
@@ -38,10 +35,7 @@ export const updateTableRow = async (context) => {
38
35
  const mobileConfig = saltcorn.data.state.getState().mobileConfig;
39
36
  const table = saltcorn.data.models.Table.findOne({ name: tableName });
40
37
  if (!table) throw new Error(`The table '${tableName}' does not exist.`);
41
- if (
42
- mobileConfig.isOfflineMode ||
43
- mobileConfig.localTableIds.indexOf(table.id) >= 0
44
- ) {
38
+ if (mobileConfig.isOfflineMode) {
45
39
  const row = {};
46
40
  for (const [k, v] of new URLSearchParams(context.query).entries()) {
47
41
  row[k] = v;
@@ -73,10 +67,7 @@ export const insertTableRow = async (context) => {
73
67
  const table = saltcorn.data.models.Table.findOne({ name: tableName });
74
68
  const mobileConfig = saltcorn.data.state.getState().mobileConfig;
75
69
  if (!table) throw new Error(`The table '${tableName}' does not exist.`);
76
- if (
77
- mobileConfig.isOfflineMode ||
78
- mobileConfig.localTableIds.indexOf(table.id) >= 0
79
- ) {
70
+ if (mobileConfig.isOfflineMode) {
80
71
  const row = {};
81
72
  for (const [k, v] of new URLSearchParams(context.query).entries()) {
82
73
  row[k] = v;
@@ -7,19 +7,29 @@ import i18next from "i18next";
7
7
  export const deleteRows = async (context) => {
8
8
  const { tableName, id } = context.params;
9
9
  const table = await saltcorn.data.models.Table.findOne({ name: tableName });
10
- const { isOfflineMode, localTableIds, user } =
11
- saltcorn.data.state.getState().mobileConfig;
12
- if (isOfflineMode || localTableIds.indexOf(table.id) >= 0) {
13
- if (user.role_id <= table.min_role_write) {
14
- await table.deleteRows({ id });
15
- // TODO 'table.is_owner' check?
16
- } else
10
+ const { isOfflineMode, user } = saltcorn.data.state.getState().mobileConfig;
11
+ if (isOfflineMode) {
12
+ const role = user?.role_id || 100;
13
+ const where = { [table.pk_name]: id };
14
+ if (role <= table.min_role_write) {
15
+ await table.deleteRows(where, user);
16
+ } else if ((table.ownership_field_id || table.ownership_formula) && user) {
17
+ const row = await table.getJoinedRow({
18
+ where,
19
+ forUser: user,
20
+ forPublic: !user,
21
+ });
22
+ if (row && table.is_owner(user, row)) {
23
+ await table.deleteRows(where, user);
24
+ } else {
25
+ throw new saltcorn.data.utils.NotAuthorized(
26
+ i18next.t("Not authorized")
27
+ );
28
+ }
29
+ } else {
17
30
  throw new saltcorn.data.utils.NotAuthorized(i18next.t("Not authorized"));
18
- if (isOfflineMode && (await hasOfflineRows()))
19
- await setHasOfflineData(true);
20
- // if (isOfflineMode && !(await offlineHelper.hasOfflineRows())) {
21
- // await offlineHelper.setOfflineSession(null);
22
- // }
31
+ }
32
+ if (await hasOfflineRows()) await setHasOfflineData(true);
23
33
  } else {
24
34
  await apiCall({ method: "POST", path: `/delete/${tableName}/${id}` });
25
35
  }
@@ -8,8 +8,8 @@ export const postToggleField = async (context) => {
8
8
  const { name, id, field_name } = context.params;
9
9
  const table = await saltcorn.data.models.Table.findOne({ name });
10
10
  const state = saltcorn.data.state.getState();
11
- const { isOfflineMode, localTableIds, user } = state.mobileConfig;
12
- if (isOfflineMode || localTableIds.indexOf(table.id) >= 0) {
11
+ const { isOfflineMode, user } = state.mobileConfig;
12
+ if (isOfflineMode) {
13
13
  if (user.role_id > table.min_role_write)
14
14
  throw new saltcorn.data.utils.NotAuthorized(i18next.t("Not authorized"));
15
15
  await table.toggleBool(+id, field_name, user);
@@ -10,10 +10,7 @@ export const postShowCalculated = async (context) => {
10
10
  const mobileConfig = saltcorn.data.state.getState().mobileConfig;
11
11
  const table = saltcorn.data.models.Table.findOne({ name: tableName });
12
12
  if (!table) throw new Error(`The table '${tableName}' does not exist.`);
13
- if (
14
- mobileConfig.isOfflineMode ||
15
- mobileConfig.localTableIds.indexOf(table.id) >= 0
16
- ) {
13
+ if (mobileConfig.isOfflineMode) {
17
14
  const req = new MobileRequest({
18
15
  query: context.query ? parseQuery(context.query) : {},
19
16
  body: context.data || {},
@@ -783,10 +783,7 @@ async function view_post(viewnameOrElem, route, data, onDone, sendState) {
783
783
  showLoadSpinner();
784
784
  let respData = undefined;
785
785
  const query = sendState ? buildQuery() : "";
786
- if (
787
- mobileConfig.isOfflineMode ||
788
- (view?.table_id && mobileConfig.localTableIds.indexOf(view.table_id) >= 0)
789
- ) {
786
+ if (mobileConfig.isOfflineMode) {
790
787
  respData = await parent.saltcorn.mobileApp.navigation.router.resolve({
791
788
  pathname: `post/view/${viewname}/${route}`,
792
789
  data,