@saltcorn/server 0.8.8-beta.2 → 0.8.8-beta.4
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/locales/en.json +6 -2
- package/package.json +8 -8
- package/public/saltcorn.js +30 -0
- package/routes/admin.js +127 -2
- package/routes/sync.js +34 -5
- package/tests/sync.test.js +96 -1
package/locales/en.json
CHANGED
|
@@ -1242,5 +1242,9 @@
|
|
|
1242
1242
|
"unsynched": "unsynched",
|
|
1243
1243
|
"synched": "synched",
|
|
1244
1244
|
"Sync information": "Sync information",
|
|
1245
|
-
"Sync information tracks the last modification or deletion timestamp so that the table data can be synchronized with the mobile app": "Sync information tracks the last modification or deletion timestamp so that the table data can be synchronized with the mobile app"
|
|
1246
|
-
|
|
1245
|
+
"Sync information tracks the last modification or deletion timestamp so that the table data can be synchronized with the mobile app": "Sync information tracks the last modification or deletion timestamp so that the table data can be synchronized with the mobile app",
|
|
1246
|
+
"Included Plugins": "Included Plugins",
|
|
1247
|
+
"exclude": "exclude",
|
|
1248
|
+
"include": "include",
|
|
1249
|
+
"Auto public login": "Auto public login"
|
|
1250
|
+
}
|
package/package.json
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@saltcorn/server",
|
|
3
|
-
"version": "0.8.8-beta.
|
|
3
|
+
"version": "0.8.8-beta.4",
|
|
4
4
|
"description": "Server app for Saltcorn, open-source no-code platform",
|
|
5
5
|
"homepage": "https://saltcorn.com",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"dependencies": {
|
|
9
|
-
"@saltcorn/base-plugin": "0.8.8-beta.
|
|
10
|
-
"@saltcorn/builder": "0.8.8-beta.
|
|
11
|
-
"@saltcorn/data": "0.8.8-beta.
|
|
12
|
-
"@saltcorn/admin-models": "0.8.8-beta.
|
|
13
|
-
"@saltcorn/filemanager": "0.8.8-beta.
|
|
14
|
-
"@saltcorn/markup": "0.8.8-beta.
|
|
15
|
-
"@saltcorn/sbadmin2": "0.8.8-beta.
|
|
9
|
+
"@saltcorn/base-plugin": "0.8.8-beta.4",
|
|
10
|
+
"@saltcorn/builder": "0.8.8-beta.4",
|
|
11
|
+
"@saltcorn/data": "0.8.8-beta.4",
|
|
12
|
+
"@saltcorn/admin-models": "0.8.8-beta.4",
|
|
13
|
+
"@saltcorn/filemanager": "0.8.8-beta.4",
|
|
14
|
+
"@saltcorn/markup": "0.8.8-beta.4",
|
|
15
|
+
"@saltcorn/sbadmin2": "0.8.8-beta.4",
|
|
16
16
|
"@socket.io/cluster-adapter": "^0.2.1",
|
|
17
17
|
"@socket.io/sticky": "^1.0.1",
|
|
18
18
|
"adm-zip": "0.5.10",
|
package/public/saltcorn.js
CHANGED
|
@@ -678,6 +678,10 @@ function build_mobile_app(button) {
|
|
|
678
678
|
params.synchedTables = Array.from($("#synched-tbls-select-id")[0].options)
|
|
679
679
|
.filter((option) => !option.hidden)
|
|
680
680
|
.map((option) => option.value);
|
|
681
|
+
const pluginsSelect = $("#included-plugins-select-id")[0];
|
|
682
|
+
params.includedPlugins = Array.from(pluginsSelect.options)
|
|
683
|
+
.filter((option) => !option.hidden)
|
|
684
|
+
.map((option) => option.value);
|
|
681
685
|
ajax_post("/admin/build-mobile-app", {
|
|
682
686
|
data: params,
|
|
683
687
|
success: (data) => {
|
|
@@ -717,6 +721,32 @@ function move_to_unsynched() {
|
|
|
717
721
|
}
|
|
718
722
|
}
|
|
719
723
|
|
|
724
|
+
function move_plugin_to_included() {
|
|
725
|
+
const opts = $("#excluded-plugins-select-id");
|
|
726
|
+
$("#included-plugins-select-id").removeAttr("selected");
|
|
727
|
+
for (const selected of opts.val()) {
|
|
728
|
+
const jExclOpt = $(`[id='${selected}_excluded_opt']`);
|
|
729
|
+
jExclOpt.attr("hidden", "true");
|
|
730
|
+
jExclOpt.removeAttr("selected");
|
|
731
|
+
const jInclOpt = $(`[id='${selected}_included_opt']`);
|
|
732
|
+
jInclOpt.removeAttr("hidden");
|
|
733
|
+
jInclOpt.removeAttr("selected");
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function move_plugin_to_excluded() {
|
|
738
|
+
const opts = $("#included-plugins-select-id");
|
|
739
|
+
$("#excluded-plugins-select-id").removeAttr("selected");
|
|
740
|
+
for (const selected of opts.val()) {
|
|
741
|
+
const jInclOpt = $(`[id='${selected}_included_opt']`);
|
|
742
|
+
jInclOpt.attr("hidden", "true");
|
|
743
|
+
jInclOpt.removeAttr("selected");
|
|
744
|
+
const jExclOpt = $(`[id='${selected}_excluded_opt']`);
|
|
745
|
+
jExclOpt.removeAttr("hidden");
|
|
746
|
+
jExclOpt.removeAttr("selected");
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
|
|
720
750
|
function toggle_tbl_sync() {
|
|
721
751
|
if ($("#offlineModeBoxId")[0].checked === true) {
|
|
722
752
|
$("#tblSyncSelectorId").attr("hidden", false);
|
package/routes/admin.js
CHANGED
|
@@ -1492,6 +1492,9 @@ router.get(
|
|
|
1492
1492
|
image.filename?.endsWith(".png")
|
|
1493
1493
|
);
|
|
1494
1494
|
const withSyncInfo = await Table.find({ has_sync_info: true });
|
|
1495
|
+
const plugins = (await Plugin.find()).filter(
|
|
1496
|
+
(plugin) => ["base", "sbadmin2"].indexOf(plugin.name) < 0
|
|
1497
|
+
);
|
|
1495
1498
|
send_admin_page({
|
|
1496
1499
|
res,
|
|
1497
1500
|
req,
|
|
@@ -1750,7 +1753,29 @@ router.get(
|
|
|
1750
1753
|
)
|
|
1751
1754
|
)
|
|
1752
1755
|
),
|
|
1753
|
-
|
|
1756
|
+
// auto public login box
|
|
1757
|
+
div(
|
|
1758
|
+
{ class: "row pb-2" },
|
|
1759
|
+
div(
|
|
1760
|
+
{ class: "col-sm-4" },
|
|
1761
|
+
input({
|
|
1762
|
+
type: "checkbox",
|
|
1763
|
+
id: "autoPublLoginId",
|
|
1764
|
+
class: "form-check-input me-2",
|
|
1765
|
+
name: "autoPublicLogin",
|
|
1766
|
+
value: "autoPublicLogin",
|
|
1767
|
+
checked: false,
|
|
1768
|
+
}),
|
|
1769
|
+
label(
|
|
1770
|
+
{
|
|
1771
|
+
for: "autoPublLoginId",
|
|
1772
|
+
class: "form-label",
|
|
1773
|
+
},
|
|
1774
|
+
req.__("Auto public login")
|
|
1775
|
+
)
|
|
1776
|
+
)
|
|
1777
|
+
),
|
|
1778
|
+
// allow offline mode box
|
|
1754
1779
|
div(
|
|
1755
1780
|
{ class: "row pb-2" },
|
|
1756
1781
|
div(
|
|
@@ -1773,10 +1798,11 @@ router.get(
|
|
|
1773
1798
|
)
|
|
1774
1799
|
)
|
|
1775
1800
|
),
|
|
1801
|
+
// synched/unsynched tables
|
|
1776
1802
|
div(
|
|
1777
1803
|
{
|
|
1778
1804
|
id: "tblSyncSelectorId",
|
|
1779
|
-
class: "row pb-
|
|
1805
|
+
class: "row pb-3",
|
|
1780
1806
|
},
|
|
1781
1807
|
div(
|
|
1782
1808
|
label(
|
|
@@ -1864,6 +1890,97 @@ router.get(
|
|
|
1864
1890
|
)
|
|
1865
1891
|
)
|
|
1866
1892
|
)
|
|
1893
|
+
),
|
|
1894
|
+
// included/excluded plugins
|
|
1895
|
+
div(
|
|
1896
|
+
{
|
|
1897
|
+
id: "pluginsSelectorId",
|
|
1898
|
+
class: "row pb-2",
|
|
1899
|
+
},
|
|
1900
|
+
div(
|
|
1901
|
+
label({ class: "form-label fw-bold" }, req.__("Plugins"))
|
|
1902
|
+
),
|
|
1903
|
+
div(
|
|
1904
|
+
{ class: "container" },
|
|
1905
|
+
div(
|
|
1906
|
+
{ class: "row" },
|
|
1907
|
+
div(
|
|
1908
|
+
{ class: "col-sm-4 text-center" },
|
|
1909
|
+
req.__("exclude")
|
|
1910
|
+
),
|
|
1911
|
+
div({ class: "col-sm-1" }),
|
|
1912
|
+
div(
|
|
1913
|
+
{ class: "col-sm-4 text-center" },
|
|
1914
|
+
req.__("include")
|
|
1915
|
+
)
|
|
1916
|
+
),
|
|
1917
|
+
div(
|
|
1918
|
+
{ class: "row" },
|
|
1919
|
+
div(
|
|
1920
|
+
{ class: "col-sm-4" },
|
|
1921
|
+
select(
|
|
1922
|
+
{
|
|
1923
|
+
id: "excluded-plugins-select-id",
|
|
1924
|
+
class: "form-control form-select",
|
|
1925
|
+
multiple: true,
|
|
1926
|
+
},
|
|
1927
|
+
plugins.map((plugin) =>
|
|
1928
|
+
option({
|
|
1929
|
+
id: `${plugin.name}_excluded_opt`,
|
|
1930
|
+
value: plugin.name,
|
|
1931
|
+
label: plugin.name,
|
|
1932
|
+
hidden: "true",
|
|
1933
|
+
})
|
|
1934
|
+
)
|
|
1935
|
+
)
|
|
1936
|
+
),
|
|
1937
|
+
div(
|
|
1938
|
+
{ class: "col-sm-1 d-flex justify-content-center" },
|
|
1939
|
+
div(
|
|
1940
|
+
div(
|
|
1941
|
+
button(
|
|
1942
|
+
{
|
|
1943
|
+
id: "move-plugin-right-btn-id",
|
|
1944
|
+
type: "button",
|
|
1945
|
+
onClick: `move_plugin_to_included()`,
|
|
1946
|
+
class: "btn btn-light pt-1 mb-1",
|
|
1947
|
+
},
|
|
1948
|
+
i({ class: "fas fa-arrow-right" })
|
|
1949
|
+
)
|
|
1950
|
+
),
|
|
1951
|
+
div(
|
|
1952
|
+
button(
|
|
1953
|
+
{
|
|
1954
|
+
id: "move-plugin-left-btn-id",
|
|
1955
|
+
type: "button",
|
|
1956
|
+
onClick: `move_plugin_to_excluded()`,
|
|
1957
|
+
class: "btn btn-light pt-1",
|
|
1958
|
+
},
|
|
1959
|
+
i({ class: "fas fa-arrow-left" })
|
|
1960
|
+
)
|
|
1961
|
+
)
|
|
1962
|
+
)
|
|
1963
|
+
),
|
|
1964
|
+
div(
|
|
1965
|
+
{ class: "col-sm-4" },
|
|
1966
|
+
select(
|
|
1967
|
+
{
|
|
1968
|
+
id: "included-plugins-select-id",
|
|
1969
|
+
class: "form-control form-select",
|
|
1970
|
+
multiple: true,
|
|
1971
|
+
},
|
|
1972
|
+
plugins.map((plugin) =>
|
|
1973
|
+
option({
|
|
1974
|
+
id: `${plugin.name}_included_opt`,
|
|
1975
|
+
value: plugin.name,
|
|
1976
|
+
label: plugin.name,
|
|
1977
|
+
// hidden: "true",
|
|
1978
|
+
})
|
|
1979
|
+
)
|
|
1980
|
+
)
|
|
1981
|
+
)
|
|
1982
|
+
)
|
|
1983
|
+
)
|
|
1867
1984
|
)
|
|
1868
1985
|
),
|
|
1869
1986
|
button(
|
|
@@ -1969,8 +2086,10 @@ router.post(
|
|
|
1969
2086
|
appIcon,
|
|
1970
2087
|
serverURL,
|
|
1971
2088
|
splashPage,
|
|
2089
|
+
autoPublicLogin,
|
|
1972
2090
|
allowOfflineMode,
|
|
1973
2091
|
synchedTables,
|
|
2092
|
+
includedPlugins,
|
|
1974
2093
|
} = req.body;
|
|
1975
2094
|
if (!androidPlatform && !iOSPlatform) {
|
|
1976
2095
|
return res.json({
|
|
@@ -2022,8 +2141,14 @@ router.post(
|
|
|
2022
2141
|
if (serverURL) spawnParams.push("-s", serverURL);
|
|
2023
2142
|
if (splashPage) spawnParams.push("--splashPage", splashPage);
|
|
2024
2143
|
if (allowOfflineMode) spawnParams.push("--allowOfflineMode");
|
|
2144
|
+
if (autoPublicLogin) spawnParams.push("--autoPublicLogin");
|
|
2025
2145
|
if (synchedTables?.length > 0)
|
|
2026
2146
|
spawnParams.push("--synchedTables", ...synchedTables.map((tbl) => tbl));
|
|
2147
|
+
if (includedPlugins?.length > 0)
|
|
2148
|
+
spawnParams.push(
|
|
2149
|
+
"--includedPlugins",
|
|
2150
|
+
...includedPlugins.map((pluginName) => pluginName)
|
|
2151
|
+
);
|
|
2027
2152
|
if (
|
|
2028
2153
|
db.is_it_multi_tenant() &&
|
|
2029
2154
|
db.getTenantSchema() !== db.connectObj.default_schema
|
package/routes/sync.js
CHANGED
|
@@ -24,9 +24,30 @@ router.get(
|
|
|
24
24
|
})
|
|
25
25
|
);
|
|
26
26
|
|
|
27
|
-
const getSyncRows = async (syncInfo, table, syncUntil, client) => {
|
|
27
|
+
const getSyncRows = async (syncInfo, table, syncUntil, client, user) => {
|
|
28
28
|
const tblName = table.name;
|
|
29
29
|
const pkName = table.pk_name;
|
|
30
|
+
const minRole = table.min_role_read;
|
|
31
|
+
const role = user?.role_id || 100;
|
|
32
|
+
let ownerFieldName = null;
|
|
33
|
+
if (
|
|
34
|
+
role > minRole &&
|
|
35
|
+
((!table.ownership_field_id && !table.ownership_formula) || role === 100)
|
|
36
|
+
)
|
|
37
|
+
return null;
|
|
38
|
+
if (user?.id && role < 100 && role > minRole && table.ownership_field_id) {
|
|
39
|
+
const ownerField = table
|
|
40
|
+
.getFields()
|
|
41
|
+
.find((f) => f.id === table.ownership_field_id);
|
|
42
|
+
if (!ownerField) {
|
|
43
|
+
getState().log(
|
|
44
|
+
5,
|
|
45
|
+
`GET /load_changes: The ownership field of '${table.name}' does not exist.`
|
|
46
|
+
);
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
ownerFieldName = ownerField.name;
|
|
50
|
+
}
|
|
30
51
|
const schema = db.getTenantSchemaPrefix();
|
|
31
52
|
if (!syncInfo.syncFrom) {
|
|
32
53
|
const { rows } = await client.query(
|
|
@@ -43,9 +64,9 @@ const getSyncRows = async (syncInfo, table, syncUntil, client) => {
|
|
|
43
64
|
on info_tbl.ref = data_tbl."${db.sqlsanitize(
|
|
44
65
|
pkName
|
|
45
66
|
)}" and info_tbl.deleted = false
|
|
46
|
-
where data_tbl."${db.sqlsanitize(pkName)}" > ${
|
|
47
|
-
|
|
48
|
-
|
|
67
|
+
where data_tbl."${db.sqlsanitize(pkName)}" > ${syncInfo.maxLoadedId}
|
|
68
|
+
${ownerFieldName ? `and data_tbl."${ownerFieldName}" = ${user.id}` : ""}
|
|
69
|
+
order by data_tbl."${db.sqlsanitize(pkName)}"`
|
|
49
70
|
);
|
|
50
71
|
for (const row of rows) {
|
|
51
72
|
if (row._sync_info_tbl_last_modified_)
|
|
@@ -76,6 +97,7 @@ const getSyncRows = async (syncInfo, table, syncUntil, client) => {
|
|
|
76
97
|
})
|
|
77
98
|
and info_tbl.deleted = false
|
|
78
99
|
and info_tbl.ref > ${syncInfo.maxLoadedId}
|
|
100
|
+
${ownerFieldName ? `and data_tbl."${ownerFieldName}" = ${user.id}` : ""}
|
|
79
101
|
order by info_tbl.ref`
|
|
80
102
|
);
|
|
81
103
|
for (const row of rows) {
|
|
@@ -114,7 +136,14 @@ router.post(
|
|
|
114
136
|
const table = Table.findOne({ name: tblName });
|
|
115
137
|
if (!table) throw new Error(`The table '${tblName}' does not exists`);
|
|
116
138
|
const pkName = table.pk_name;
|
|
117
|
-
let rows = await getSyncRows(
|
|
139
|
+
let rows = await getSyncRows(
|
|
140
|
+
syncInfo,
|
|
141
|
+
table,
|
|
142
|
+
loadUntil,
|
|
143
|
+
client,
|
|
144
|
+
req.user
|
|
145
|
+
);
|
|
146
|
+
if (!rows) continue;
|
|
118
147
|
if (role > table.min_role_read) {
|
|
119
148
|
if (
|
|
120
149
|
role === 100 ||
|
package/tests/sync.test.js
CHANGED
|
@@ -10,6 +10,8 @@ const db = require("@saltcorn/data/db");
|
|
|
10
10
|
const { sleep } = require("@saltcorn/data/tests/mocks");
|
|
11
11
|
|
|
12
12
|
const Table = require("@saltcorn/data/models/table");
|
|
13
|
+
const Field = require("@saltcorn/data/models/field");
|
|
14
|
+
const User = require("@saltcorn/data/models/user");
|
|
13
15
|
|
|
14
16
|
beforeAll(async () => {
|
|
15
17
|
await resetToFixtures();
|
|
@@ -32,7 +34,7 @@ const initSyncInfo = async (tbls) => {
|
|
|
32
34
|
describe("load remote insert/updates", () => {
|
|
33
35
|
if (!db.isSQLite) {
|
|
34
36
|
beforeAll(async () => {
|
|
35
|
-
await initSyncInfo(["books", "publisher"]);
|
|
37
|
+
await initSyncInfo(["books", "publisher", "patients"]);
|
|
36
38
|
});
|
|
37
39
|
it("check params", async () => {
|
|
38
40
|
const app = await getApp({ disableCsrf: true });
|
|
@@ -178,6 +180,99 @@ describe("load remote insert/updates", () => {
|
|
|
178
180
|
expect(data.books.rows[1].author).toBe("Leo Tolstoy");
|
|
179
181
|
}
|
|
180
182
|
});
|
|
183
|
+
|
|
184
|
+
it("load sync not authorized", async () => {
|
|
185
|
+
const app = await getApp({ disableCsrf: true });
|
|
186
|
+
const loginCookie = await getUserLoginCookie();
|
|
187
|
+
const loadUntil = new Date();
|
|
188
|
+
const resp = await request(app)
|
|
189
|
+
.post("/sync/load_changes")
|
|
190
|
+
.set("Cookie", loginCookie)
|
|
191
|
+
.send({
|
|
192
|
+
loadUntil: loadUntil.valueOf(),
|
|
193
|
+
syncInfos: {
|
|
194
|
+
patients: {
|
|
195
|
+
maxLoadedId: 0,
|
|
196
|
+
syncFrom: 1000,
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
expect(resp.status).toBe(200);
|
|
201
|
+
const data = resp._body;
|
|
202
|
+
expect(Object.keys(data).length).toBe(0);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
const addOwnerField = async () => {
|
|
206
|
+
const patients = Table.findOne({ name: "patients" });
|
|
207
|
+
const users = Table.findOne({ name: "users" });
|
|
208
|
+
const ownerField = await Field.create({
|
|
209
|
+
table: patients,
|
|
210
|
+
name: "owner",
|
|
211
|
+
label: "Pages",
|
|
212
|
+
type: "Key",
|
|
213
|
+
reftable: users,
|
|
214
|
+
attributes: { summary_field: "id" },
|
|
215
|
+
});
|
|
216
|
+
patients.ownership_field_id = ownerField.id;
|
|
217
|
+
await patients.update(patients);
|
|
218
|
+
const user = await User.findOne({ email: "user@foo.com" });
|
|
219
|
+
await patients.updateRow({ owner: user.id }, 1);
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
it("load sync authorized with ownership", async () => {
|
|
223
|
+
await addOwnerField();
|
|
224
|
+
const app = await getApp({ disableCsrf: true });
|
|
225
|
+
const loginCookie = await getUserLoginCookie();
|
|
226
|
+
const loadUntil = new Date();
|
|
227
|
+
const resp = await request(app)
|
|
228
|
+
.post("/sync/load_changes")
|
|
229
|
+
.set("Cookie", loginCookie)
|
|
230
|
+
.send({
|
|
231
|
+
loadUntil: loadUntil.valueOf(),
|
|
232
|
+
syncInfos: {
|
|
233
|
+
patients: {
|
|
234
|
+
maxLoadedId: 0,
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
expect(resp.status).toBe(200);
|
|
239
|
+
const data = resp._body;
|
|
240
|
+
expect(Object.keys(data).length).toBe(1);
|
|
241
|
+
expect(data.patients).toBeDefined();
|
|
242
|
+
expect(data.patients.rows.length).toBe(1);
|
|
243
|
+
expect(data.patients.rows[0].id).toBe(1);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it("load sync authorized with ownership and syncFrom", async () => {
|
|
247
|
+
const patients = Table.findOne({ name: "patients" });
|
|
248
|
+
if (!patients.ownership_field_id) await addOwnerField();
|
|
249
|
+
const rows = await patients.getRows();
|
|
250
|
+
for (const row of rows) {
|
|
251
|
+
await patients.updateRow(row, row.id);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const app = await getApp({ disableCsrf: true });
|
|
255
|
+
const loginCookie = await getUserLoginCookie();
|
|
256
|
+
const loadUntil = new Date();
|
|
257
|
+
const resp = await request(app)
|
|
258
|
+
.post("/sync/load_changes")
|
|
259
|
+
.set("Cookie", loginCookie)
|
|
260
|
+
.send({
|
|
261
|
+
loadUntil: loadUntil.valueOf(),
|
|
262
|
+
syncInfos: {
|
|
263
|
+
patients: {
|
|
264
|
+
maxLoadedId: 0,
|
|
265
|
+
syncFrom: 1000,
|
|
266
|
+
},
|
|
267
|
+
},
|
|
268
|
+
});
|
|
269
|
+
expect(resp.status).toBe(200);
|
|
270
|
+
const data = resp._body;
|
|
271
|
+
expect(Object.keys(data).length).toBe(1);
|
|
272
|
+
expect(data.patients).toBeDefined();
|
|
273
|
+
expect(data.patients.rows.length).toBe(1);
|
|
274
|
+
expect(data.patients.rows[0].id).toBe(1);
|
|
275
|
+
});
|
|
181
276
|
} else
|
|
182
277
|
it("only pq support", () => {
|
|
183
278
|
expect(true).toBe(true);
|