@saltcorn/data 0.6.1-beta.0 → 0.6.1
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/base-plugin/actions.js +172 -1
- package/base-plugin/fieldviews.js +63 -0
- package/base-plugin/fileviews.js +41 -0
- package/base-plugin/index.js +35 -0
- package/base-plugin/types.js +345 -9
- package/base-plugin/viewtemplates/edit.js +107 -0
- package/base-plugin/viewtemplates/feed.js +46 -0
- package/base-plugin/viewtemplates/filter.js +43 -0
- package/base-plugin/viewtemplates/list.js +73 -1
- package/base-plugin/viewtemplates/listshowlist.js +54 -3
- package/base-plugin/viewtemplates/room.js +100 -0
- package/base-plugin/viewtemplates/show.js +86 -0
- package/base-plugin/viewtemplates/viewable_fields.js +124 -0
- package/contracts.js +58 -0
- package/db/connect.js +13 -5
- package/db/db.test.js +0 -154
- package/db/fixtures.js +13 -1
- package/db/index.js +42 -3
- package/db/reset_schema.js +11 -0
- package/db/state.js +105 -36
- package/index.js +13 -0
- package/migrate.js +4 -1
- package/models/backup.js +78 -0
- package/models/config.js +113 -22
- package/models/crash.js +44 -0
- package/models/discovery.js +13 -11
- package/models/email.js +17 -0
- package/models/eventlog.js +51 -0
- package/models/expression.js +49 -1
- package/models/field.js +88 -9
- package/models/fieldrepeat.js +33 -0
- package/models/file.js +23 -4
- package/models/form.js +34 -0
- package/models/index.js +42 -0
- package/models/layout.js +33 -0
- package/models/library.js +44 -0
- package/models/pack.js +88 -0
- package/models/page.js +13 -3
- package/models/plugin.js +9 -2
- package/models/random.js +36 -0
- package/models/role.js +36 -0
- package/models/scheduler.js +28 -0
- package/models/table.js +34 -15
- package/models/table_constraints.js +44 -0
- package/models/tenant.js +46 -9
- package/models/trigger.js +24 -11
- package/models/user.js +33 -8
- package/models/view.js +89 -8
- package/models/workflow.js +31 -0
- package/package.json +7 -5
- package/plugin-helper.js +102 -44
- package/plugin-testing.js +4 -0
- package/tests/exact_views.test.js +5 -5
- package/utils.js +4 -0
- package/db/internal.js +0 -229
- package/db/multi-tenant.js +0 -24
- package/db/pg.js +0 -374
- package/db/single-tenant.js +0 -8
- package/db/sqlite.js +0 -280
- package/db/tenants.js +0 -6
|
@@ -128,7 +128,7 @@ describe("Show view", () => {
|
|
|
128
128
|
configuration: { code: 'console.log("1")' },
|
|
129
129
|
},
|
|
130
130
|
],
|
|
131
|
-
response: `<div class="row"><div class="col-6">1</div><div class="col-6"><a href="javascript:view_post('testshow', 'run_action', {rndid:'1a8ac3', id:1});" class="btn btn-success btn-sm">you're my number</a></div></div>`,
|
|
131
|
+
response: `<div class="row w-100"><div class="col-6">1</div><div class="col-6"><a href="javascript:view_post('testshow', 'run_action', {rndid:'1a8ac3', id:1});" class="btn btn-success btn-sm">you're my number</a></div></div>`,
|
|
132
132
|
});
|
|
133
133
|
await test_show({
|
|
134
134
|
layout: {
|
|
@@ -179,7 +179,7 @@ describe("Show view", () => {
|
|
|
179
179
|
in_modal: true,
|
|
180
180
|
},
|
|
181
181
|
],
|
|
182
|
-
response: `<div class="row"><div class="col-6"><div class="card mt-4 shadow"><div class="card-body"><button class="btn btn-link" onClick="ajax_modal('/view/authorshow?id=1')">foo it</button></div></div></div><div class="col-6"><div class="text-left" style="min-height: 100px;border: 1px solid black; background-color: #a9a7a7; "><a href="https://countto.com/967">Herman Melville</a></div></div></div>`,
|
|
182
|
+
response: `<div class="row w-100"><div class="col-6"><div class="card mt-4 shadow"><div class="card-body"><button class="btn btn-link" onClick="ajax_modal('/view/authorshow?id=1')">foo it</button></div></div></div><div class="col-6"><div class="text-left" style="min-height: 100px;border: 1px solid black; background-color: #a9a7a7; "><a href="https://countto.com/967">Herman Melville</a></div></div></div>`,
|
|
183
183
|
});
|
|
184
184
|
await test_show({
|
|
185
185
|
layout: {
|
|
@@ -287,13 +287,13 @@ describe("Edit view", () => {
|
|
|
287
287
|
await test_edit({
|
|
288
288
|
layout,
|
|
289
289
|
columns,
|
|
290
|
-
response: `<form action="/view/testedit" class="form-namespace " method="post"><input type="hidden" name="_csrf" value=""><div class="row"><div class="col-2">Name</div><div class="col-10"><input type="text" class="form-control " data-fieldname="name" name="name" id="inputname"></div></div><br /><div class="row"><div class="col-2">Favourite book</div><div class="col-10"><select class="form-control " data-fieldname="favbook" name="favbook" id="inputfavbook"><option value=""></option><option value="1">Herman Melville</option><option value="2">Leo Tolstoy</option></select></div></div><br /><div class="row"><div class="col-2">Parent</div><div class="col-10"><select class="form-control " data-fieldname="parent" name="parent" id="inputparent"><option value=""></option><option value="1">1</option><option value="2">2</option></select></div></div><br /><button type="submit" class="btn btn-primary ">Save</button></form>`,
|
|
290
|
+
response: `<form action="/view/testedit" class="form-namespace " method="post"><input type="hidden" name="_csrf" value=""><div class="row w-100"><div class="col-2">Name</div><div class="col-10"><input type="text" class="form-control " data-fieldname="name" name="name" id="inputname"></div></div><br /><div class="row w-100"><div class="col-2">Favourite book</div><div class="col-10"><select class="form-control " data-fieldname="favbook" name="favbook" id="inputfavbook"><option value=""></option><option value="1">Herman Melville</option><option value="2">Leo Tolstoy</option></select></div></div><br /><div class="row w-100"><div class="col-2">Parent</div><div class="col-10"><select class="form-control " data-fieldname="parent" name="parent" id="inputparent"><option value=""></option><option value="1">1</option><option value="2">2</option></select></div></div><br /><button type="submit" class="btn btn-primary ">Save</button></form>`,
|
|
291
291
|
});
|
|
292
292
|
await test_edit({
|
|
293
293
|
id: 1,
|
|
294
294
|
layout,
|
|
295
295
|
columns,
|
|
296
|
-
response: `<form action="/view/testedit" class="form-namespace " method="post"><input type="hidden" name="_csrf" value=""><input type="hidden" class="form-control " name="id" value="1"><div class="row"><div class="col-2">Name</div><div class="col-10"><input type="text" class="form-control " data-fieldname="name" name="name" id="inputname" value="Kirk Douglas"></div></div><br /><div class="row"><div class="col-2">Favourite book</div><div class="col-10"><select class="form-control " data-fieldname="favbook" name="favbook" id="inputfavbook"><option value=""></option><option value="1" selected>Herman Melville</option><option value="2">Leo Tolstoy</option></select></div></div><br /><div class="row"><div class="col-2">Parent</div><div class="col-10"><select class="form-control " data-fieldname="parent" name="parent" id="inputparent"><option value=""></option><option value="1">1</option><option value="2">2</option></select></div></div><br /><button type="submit" class="btn btn-primary ">Save</button></form>`,
|
|
296
|
+
response: `<form action="/view/testedit" class="form-namespace " method="post"><input type="hidden" name="_csrf" value=""><input type="hidden" class="form-control " name="id" value="1"><div class="row w-100"><div class="col-2">Name</div><div class="col-10"><input type="text" class="form-control " data-fieldname="name" name="name" id="inputname" value="Kirk Douglas"></div></div><br /><div class="row w-100"><div class="col-2">Favourite book</div><div class="col-10"><select class="form-control " data-fieldname="favbook" name="favbook" id="inputfavbook"><option value=""></option><option value="1" selected>Herman Melville</option><option value="2">Leo Tolstoy</option></select></div></div><br /><div class="row w-100"><div class="col-2">Parent</div><div class="col-10"><select class="form-control " data-fieldname="parent" name="parent" id="inputparent"><option value=""></option><option value="1">1</option><option value="2">2</option></select></div></div><br /><button type="submit" class="btn btn-primary ">Save</button></form>`,
|
|
297
297
|
});
|
|
298
298
|
});
|
|
299
299
|
});
|
|
@@ -394,7 +394,7 @@ describe("Filter view", () => {
|
|
|
394
394
|
],
|
|
395
395
|
viewname: "testfilter",
|
|
396
396
|
response:
|
|
397
|
-
'<div class="row"><div class="col-6"><select name="ddfilterpatients.favbook.name" class="form-control d-inline" style="width: unset;" onchange="this.value==\'\' ? unset_state_field(\'patients.favbook.name\'): set_state_field(\'patients.favbook.name\', this.value)"><option value="" class="text-muted"></option><option value="Kirk Douglas">Kirk Douglas</option><option value="Michael Douglas">Michael Douglas</option></select></div><div class="col-6"><button class="btn btn-outline-primary" onClick="set_state_field(\'pages\', \'13\')">thirteen</button></div></div><button class="btn btn-outline-primary" onClick="set_state_field(\'patients.favbook.name\', \'Jim\')">Jim</button><select name="ddfilterauthor" class="form-control d-inline" style="width: unset;" onchange="this.value==\'\' ? unset_state_field(\'author\'): set_state_field(\'author\', this.value)"><option value="" class="text-muted"></option><option value="Herman Melville">Herman Melville</option><option value="Leo Tolstoy">Leo Tolstoy</option></select>',
|
|
397
|
+
'<div class="row w-100"><div class="col-6"><select name="ddfilterpatients.favbook.name" class="form-control d-inline" style="width: unset;" onchange="this.value==\'\' ? unset_state_field(\'patients.favbook.name\'): set_state_field(\'patients.favbook.name\', this.value)"><option value="" class="text-muted"></option><option value="Kirk Douglas">Kirk Douglas</option><option value="Michael Douglas">Michael Douglas</option></select></div><div class="col-6"><button class="btn btn-outline-primary" onClick="set_state_field(\'pages\', \'13\')">thirteen</button></div></div><button class="btn btn-outline-primary" onClick="set_state_field(\'patients.favbook.name\', \'Jim\')">Jim</button><select name="ddfilterauthor" class="form-control d-inline" style="width: unset;" onchange="this.value==\'\' ? unset_state_field(\'author\'): set_state_field(\'author\', this.value)"><option value="" class="text-muted"></option><option value="Herman Melville">Herman Melville</option><option value="Leo Tolstoy">Leo Tolstoy</option></select>',
|
|
398
398
|
});
|
|
399
399
|
});
|
|
400
400
|
});
|
package/utils.js
CHANGED
package/db/internal.js
DELETED
|
@@ -1,229 +0,0 @@
|
|
|
1
|
-
const { footer } = require("@saltcorn/markup/tags");
|
|
2
|
-
const { contract, is } = require("contractis");
|
|
3
|
-
const { is_sqlite } = require("./connect");
|
|
4
|
-
|
|
5
|
-
//https://stackoverflow.com/questions/15300704/regex-with-my-jquery-function-for-sql-variable-name-validation
|
|
6
|
-
/**
|
|
7
|
-
* Transform value to correct sql name.
|
|
8
|
-
* Note! Dont use other symbols than ^A-Za-z_0-9
|
|
9
|
-
* @type {*|(function(...[*]=): *)}
|
|
10
|
-
*/
|
|
11
|
-
const sqlsanitize = contract(is.fun(is.str, is.str), (nm) => {
|
|
12
|
-
const s = nm.replace(/[^A-Za-z_0-9]*/g, "");
|
|
13
|
-
if (s[0] >= "0" && s[0] <= "9") return `_${s}`;
|
|
14
|
-
else return s;
|
|
15
|
-
});
|
|
16
|
-
/**
|
|
17
|
-
* Transform value to correct sql name.
|
|
18
|
-
* Instead of sqlsanitize also allows .
|
|
19
|
-
* For e.g. table name
|
|
20
|
-
* Note! Dont use other symbols than ^A-Za-z_0-9.
|
|
21
|
-
* @type {*|(function(...[*]=): *)}
|
|
22
|
-
*/
|
|
23
|
-
const sqlsanitizeAllowDots = contract(is.fun(is.str, is.str), (nm) => {
|
|
24
|
-
const s = nm.replace(/[^A-Za-z_0-9."]*/g, "");
|
|
25
|
-
if (s[0] >= "0" && s[0] <= "9") return `_${s}`;
|
|
26
|
-
else return s;
|
|
27
|
-
});
|
|
28
|
-
/**
|
|
29
|
-
*
|
|
30
|
-
* @param v
|
|
31
|
-
* @param i
|
|
32
|
-
* @param is_sqlite
|
|
33
|
-
* @returns {`to_tsvector('english', ${*}) @@ plainto_tsquery('english', $${string})`|`${*} LIKE '%' || ? || '%'`}
|
|
34
|
-
*/
|
|
35
|
-
const whereFTS = (v, i, is_sqlite) => {
|
|
36
|
-
const { fields, table } = v;
|
|
37
|
-
var flds = fields
|
|
38
|
-
.filter((f) => f.type && f.type.sql_name === "text")
|
|
39
|
-
.map(
|
|
40
|
-
(f) =>
|
|
41
|
-
"coalesce(" +
|
|
42
|
-
(table
|
|
43
|
-
? `"${sqlsanitize(table)}"."${sqlsanitize(f.name)}"`
|
|
44
|
-
: `"${sqlsanitize(f.name)}"`) +
|
|
45
|
-
",'')"
|
|
46
|
-
)
|
|
47
|
-
.join(" || ' ' || ");
|
|
48
|
-
if (flds === "") flds = "''";
|
|
49
|
-
if (is_sqlite) return `${flds} LIKE '%' || ? || '%'`;
|
|
50
|
-
else
|
|
51
|
-
return `to_tsvector('english', ${flds}) @@ plainto_tsquery('english', $${i})`;
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
const placeHolder = (is_sqlite, i) => (is_sqlite ? `?` : `$${i}`);
|
|
55
|
-
|
|
56
|
-
const mkCounter = () => {
|
|
57
|
-
let i = 0;
|
|
58
|
-
return () => {
|
|
59
|
-
i += 1;
|
|
60
|
-
return i;
|
|
61
|
-
};
|
|
62
|
-
};
|
|
63
|
-
const subSelectWhere = (is_sqlite, i) => (k, v) => {
|
|
64
|
-
const whereObj = v.inSelect.where;
|
|
65
|
-
const wheres = whereObj ? Object.entries(whereObj) : [];
|
|
66
|
-
const where =
|
|
67
|
-
whereObj && wheres.length > 0
|
|
68
|
-
? "where " + wheres.map(whereClause(is_sqlite, i)).join(" and ")
|
|
69
|
-
: "";
|
|
70
|
-
return `${quote(sqlsanitizeAllowDots(k))} in (select ${
|
|
71
|
-
v.inSelect.field
|
|
72
|
-
} from ${v.inSelect.table} ${where})`;
|
|
73
|
-
};
|
|
74
|
-
const subSelectVals = (v) => {
|
|
75
|
-
const whereObj = v.inSelect.where;
|
|
76
|
-
const wheres = whereObj ? Object.entries(whereObj) : [];
|
|
77
|
-
const xs = wheres
|
|
78
|
-
.map(getVal)
|
|
79
|
-
.flat(1)
|
|
80
|
-
.filter((v) => v !== null);
|
|
81
|
-
return xs;
|
|
82
|
-
};
|
|
83
|
-
const wrapParens = (s) => (s ? `(${s})` : s);
|
|
84
|
-
const quote = (s) => (s.includes(".") || s.includes('"') ? s : `"${s}"`);
|
|
85
|
-
const whereOr = (is_sqlite, i) => (ors) =>
|
|
86
|
-
wrapParens(
|
|
87
|
-
ors
|
|
88
|
-
.map((vi) =>
|
|
89
|
-
Object.entries(vi)
|
|
90
|
-
.map((kv) => whereClause(is_sqlite, i)(kv))
|
|
91
|
-
.join(" and ")
|
|
92
|
-
)
|
|
93
|
-
.join(" or ")
|
|
94
|
-
);
|
|
95
|
-
|
|
96
|
-
const whereClause = (is_sqlite, i) => ([k, v]) =>
|
|
97
|
-
k === "_fts"
|
|
98
|
-
? whereFTS(v, i(), is_sqlite)
|
|
99
|
-
: typeof (v || {}).in !== "undefined"
|
|
100
|
-
? `${quote(sqlsanitizeAllowDots(k))} = ${
|
|
101
|
-
is_sqlite ? "" : "ANY"
|
|
102
|
-
} (${placeHolder(is_sqlite, i())})`
|
|
103
|
-
: k === "or" && Array.isArray(v)
|
|
104
|
-
? whereOr(is_sqlite, i)(v)
|
|
105
|
-
: k === "not" && typeof v === "object"
|
|
106
|
-
? `not (${Object.entries(v)
|
|
107
|
-
.map((kv) => whereClause(is_sqlite, i)(kv))
|
|
108
|
-
.join(" and ")})`
|
|
109
|
-
: v && v.or && Array.isArray(v.or)
|
|
110
|
-
? wrapParens(
|
|
111
|
-
v.or.map((vi) => whereClause(is_sqlite, i)([k, vi])).join(" or ")
|
|
112
|
-
)
|
|
113
|
-
: Array.isArray(v)
|
|
114
|
-
? v.map((vi) => whereClause(is_sqlite, i)([k, vi])).join(" and ")
|
|
115
|
-
: typeof (v || {}).ilike !== "undefined"
|
|
116
|
-
? `${quote(sqlsanitizeAllowDots(k))} ${
|
|
117
|
-
is_sqlite ? "LIKE" : "ILIKE"
|
|
118
|
-
} '%' || ${placeHolder(is_sqlite, i())} || '%'`
|
|
119
|
-
: typeof (v || {}).gt !== "undefined"
|
|
120
|
-
? `${quote(sqlsanitizeAllowDots(k))}>${v.equal ? "=" : ""}${placeHolder(
|
|
121
|
-
is_sqlite,
|
|
122
|
-
i()
|
|
123
|
-
)}`
|
|
124
|
-
: typeof (v || {}).lt !== "undefined"
|
|
125
|
-
? `${quote(sqlsanitizeAllowDots(k))}<${v.equal ? "=" : ""}${placeHolder(
|
|
126
|
-
is_sqlite,
|
|
127
|
-
i()
|
|
128
|
-
)}`
|
|
129
|
-
: typeof (v || {}).inSelect !== "undefined"
|
|
130
|
-
? subSelectWhere(is_sqlite, i)(k, v)
|
|
131
|
-
: typeof (v || {}).json !== "undefined"
|
|
132
|
-
? is_sqlite
|
|
133
|
-
? `json_extract(${quote(
|
|
134
|
-
sqlsanitizeAllowDots(k)
|
|
135
|
-
)}, '$.${sqlsanitizeAllowDots(v.json[0])}')=${placeHolder(
|
|
136
|
-
is_sqlite,
|
|
137
|
-
i()
|
|
138
|
-
)}`
|
|
139
|
-
: `${quote(sqlsanitizeAllowDots(k))}->>'${sqlsanitizeAllowDots(
|
|
140
|
-
v.json[0]
|
|
141
|
-
)}'=${placeHolder(is_sqlite, i())}`
|
|
142
|
-
: v === null
|
|
143
|
-
? `${quote(sqlsanitizeAllowDots(k))} is null`
|
|
144
|
-
: `${quote(sqlsanitizeAllowDots(k))}=${placeHolder(is_sqlite, i())}`;
|
|
145
|
-
|
|
146
|
-
const getVal = ([k, v]) =>
|
|
147
|
-
k === "_fts"
|
|
148
|
-
? v.searchTerm
|
|
149
|
-
: typeof (v || {}).in !== "undefined"
|
|
150
|
-
? [v.in]
|
|
151
|
-
: k === "not" && typeof v === "object"
|
|
152
|
-
? Object.entries(v).map(getVal).flat(1)
|
|
153
|
-
: k === "or" && Array.isArray(v)
|
|
154
|
-
? v.map((vi) => Object.entries(vi).map(getVal)).flat(1)
|
|
155
|
-
: v && v.or && Array.isArray(v.or)
|
|
156
|
-
? v.or.map((vi) => getVal([k, vi])).flat(1)
|
|
157
|
-
: Array.isArray(v)
|
|
158
|
-
? v.map((vi) => getVal([k, vi])).flat(1)
|
|
159
|
-
: typeof (v || {}).ilike !== "undefined"
|
|
160
|
-
? v.ilike
|
|
161
|
-
: typeof (v || {}).inSelect !== "undefined"
|
|
162
|
-
? subSelectVals(v)
|
|
163
|
-
: typeof (v || {}).lt !== "undefined"
|
|
164
|
-
? v.lt
|
|
165
|
-
: typeof (v || {}).gt !== "undefined"
|
|
166
|
-
? v.gt
|
|
167
|
-
: typeof (v || {}).sql !== "undefined"
|
|
168
|
-
? null
|
|
169
|
-
: typeof (v || {}).json !== "undefined"
|
|
170
|
-
? v.json[1]
|
|
171
|
-
: v;
|
|
172
|
-
|
|
173
|
-
const mkWhere = (whereObj, is_sqlite) => {
|
|
174
|
-
const wheres = whereObj ? Object.entries(whereObj) : [];
|
|
175
|
-
//console.log({ wheres });
|
|
176
|
-
const where =
|
|
177
|
-
whereObj && wheres.length > 0
|
|
178
|
-
? "where " + wheres.map(whereClause(is_sqlite, mkCounter())).join(" and ")
|
|
179
|
-
: "";
|
|
180
|
-
const values = wheres
|
|
181
|
-
.map(getVal)
|
|
182
|
-
.flat(1)
|
|
183
|
-
.filter((v) => v !== null);
|
|
184
|
-
return { where, values };
|
|
185
|
-
};
|
|
186
|
-
|
|
187
|
-
const toInt = (x) =>
|
|
188
|
-
typeof x === "number"
|
|
189
|
-
? Math.round(x)
|
|
190
|
-
: typeof x === "string"
|
|
191
|
-
? parseInt(x)
|
|
192
|
-
: null;
|
|
193
|
-
|
|
194
|
-
const getDistanceOrder = ({ latField, longField, lat, long }) => {
|
|
195
|
-
const cos_lat_2 = Math.pow(Math.cos((+lat * Math.PI) / 180), 2);
|
|
196
|
-
return `((${sqlsanitizeAllowDots(
|
|
197
|
-
latField
|
|
198
|
-
)} - ${+lat})*(${sqlsanitizeAllowDots(
|
|
199
|
-
latField
|
|
200
|
-
)} - ${+lat})) + ((${sqlsanitizeAllowDots(
|
|
201
|
-
longField
|
|
202
|
-
)} - ${+long})*(${sqlsanitizeAllowDots(longField)} - ${+long})*${cos_lat_2})`;
|
|
203
|
-
};
|
|
204
|
-
const mkSelectOptions = (selopts) => {
|
|
205
|
-
const orderby =
|
|
206
|
-
selopts.orderBy === "RANDOM()"
|
|
207
|
-
? "order by RANDOM()"
|
|
208
|
-
: selopts.orderBy && selopts.orderBy.distance
|
|
209
|
-
? `order by ${getDistanceOrder(selopts.orderBy.distance)}`
|
|
210
|
-
: selopts.orderBy && selopts.nocase
|
|
211
|
-
? `order by lower(${sqlsanitizeAllowDots(selopts.orderBy)})${
|
|
212
|
-
selopts.orderDesc ? " DESC" : ""
|
|
213
|
-
}`
|
|
214
|
-
: selopts.orderBy
|
|
215
|
-
? `order by ${sqlsanitizeAllowDots(selopts.orderBy)}${
|
|
216
|
-
selopts.orderDesc ? " DESC" : ""
|
|
217
|
-
}`
|
|
218
|
-
: "";
|
|
219
|
-
const limit = selopts.limit ? `limit ${toInt(selopts.limit)}` : "";
|
|
220
|
-
const offset = selopts.offset ? `offset ${toInt(selopts.offset)}` : "";
|
|
221
|
-
return [orderby, limit, offset].filter((s) => s).join(" ");
|
|
222
|
-
};
|
|
223
|
-
|
|
224
|
-
module.exports = {
|
|
225
|
-
sqlsanitize,
|
|
226
|
-
mkWhere,
|
|
227
|
-
mkSelectOptions,
|
|
228
|
-
sqlsanitizeAllowDots,
|
|
229
|
-
};
|
package/db/multi-tenant.js
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
const { AsyncLocalStorage } = require("async_hooks");
|
|
2
|
-
const { sqlsanitize } = require("./internal");
|
|
3
|
-
|
|
4
|
-
var is_multi_tenant = true;
|
|
5
|
-
const is_it_multi_tenant = () => is_multi_tenant;
|
|
6
|
-
|
|
7
|
-
const tenantNamespace = new AsyncLocalStorage();
|
|
8
|
-
|
|
9
|
-
const enable_multi_tenant = () => {};
|
|
10
|
-
|
|
11
|
-
const runWithTenant = (tenant, f) => {
|
|
12
|
-
if (!is_multi_tenant) return f();
|
|
13
|
-
else return tenantNamespace.run(sqlsanitize(tenant).toLowerCase(), f);
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
module.exports = (connObj) => ({
|
|
17
|
-
getTenantSchema() {
|
|
18
|
-
const storeVal = tenantNamespace.getStore();
|
|
19
|
-
return storeVal || connObj.default_schema;
|
|
20
|
-
},
|
|
21
|
-
enable_multi_tenant,
|
|
22
|
-
runWithTenant,
|
|
23
|
-
is_it_multi_tenant,
|
|
24
|
-
});
|
package/db/pg.js
DELETED
|
@@ -1,374 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* PostgreSQL data access layer
|
|
3
|
-
*/
|
|
4
|
-
// TODO move postgresql specific to this module
|
|
5
|
-
const { Pool } = require("pg");
|
|
6
|
-
const copyStreams = require("pg-copy-streams");
|
|
7
|
-
const { promisify } = require("util");
|
|
8
|
-
const { pipeline } = require("stream");
|
|
9
|
-
const { sqlsanitize, mkWhere, mkSelectOptions } = require("./internal");
|
|
10
|
-
const { getConnectObject } = require("./connect");
|
|
11
|
-
const { getTenantSchema } = require("./tenants");
|
|
12
|
-
|
|
13
|
-
var connectObj = getConnectObject();
|
|
14
|
-
|
|
15
|
-
var pool;
|
|
16
|
-
if (connectObj) pool = new Pool(connectObj);
|
|
17
|
-
|
|
18
|
-
var log_sql_enabled = false;
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Control Logging sql statements to console
|
|
22
|
-
* @param val - if true then log sql statements to console
|
|
23
|
-
*/
|
|
24
|
-
function set_sql_logging(val = true) {
|
|
25
|
-
log_sql_enabled = val;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Get sql logging state
|
|
30
|
-
* @returns {boolean} if true then sql logging eabled
|
|
31
|
-
*/
|
|
32
|
-
function get_sql_logging() {
|
|
33
|
-
return log_sql_enabled;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Log SQL statement to console
|
|
38
|
-
* @param sql - SQL statement
|
|
39
|
-
* @param vs - any additional parameter
|
|
40
|
-
*/
|
|
41
|
-
function sql_log(sql, vs) {
|
|
42
|
-
if (log_sql_enabled)
|
|
43
|
-
if (typeof vs === "undefined") console.log(sql);
|
|
44
|
-
else console.log(sql, vs);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Close database connection
|
|
49
|
-
* @returns {Promise<void>}
|
|
50
|
-
*/
|
|
51
|
-
const close = async () => {
|
|
52
|
-
if (pool) await pool.end();
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Change connection (close connection and open new connection from connObj)
|
|
57
|
-
* @param connObj - connection object
|
|
58
|
-
* @returns {Promise<void>}
|
|
59
|
-
*/
|
|
60
|
-
const changeConnection = async (connObj = {}) => {
|
|
61
|
-
await close();
|
|
62
|
-
pool = new Pool(getConnectObject(connObj));
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Execute Select statement
|
|
67
|
-
* @param tbl - table name
|
|
68
|
-
* @param whereObj - where object
|
|
69
|
-
* @param selectopts - select options
|
|
70
|
-
* @returns {Promise<*>} return rows
|
|
71
|
-
*/
|
|
72
|
-
const select = async (tbl, whereObj, selectopts = {}) => {
|
|
73
|
-
const { where, values } = mkWhere(whereObj);
|
|
74
|
-
const sql = `SELECT * FROM "${getTenantSchema()}"."${sqlsanitize(
|
|
75
|
-
tbl
|
|
76
|
-
)}" ${where} ${mkSelectOptions(selectopts)}`;
|
|
77
|
-
sql_log(sql, values);
|
|
78
|
-
const tq = await pool.query(sql, values);
|
|
79
|
-
|
|
80
|
-
return tq.rows;
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Reset DB Schema using drop schema and recreate it
|
|
85
|
-
* Atterntion! You will lost data after call this function!
|
|
86
|
-
* @param schema - db schema name
|
|
87
|
-
* @returns {Promise<void>} no result
|
|
88
|
-
*/
|
|
89
|
-
const drop_reset_schema = async (schema) => {
|
|
90
|
-
const sql = `DROP SCHEMA IF EXISTS "${schema}" CASCADE;
|
|
91
|
-
CREATE SCHEMA "${schema}";
|
|
92
|
-
GRANT ALL ON SCHEMA "${schema}" TO postgres;
|
|
93
|
-
GRANT ALL ON SCHEMA "${schema}" TO "public" ;
|
|
94
|
-
COMMENT ON SCHEMA "${schema}" IS 'standard public schema';`;
|
|
95
|
-
sql_log(sql);
|
|
96
|
-
|
|
97
|
-
await pool.query(sql);
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Get count of rows in table
|
|
102
|
-
* @param tbl - table name
|
|
103
|
-
* @param whereObj - where object
|
|
104
|
-
* @returns {Promise<number>} count of tables
|
|
105
|
-
*/
|
|
106
|
-
const count = async (tbl, whereObj) => {
|
|
107
|
-
const { where, values } = mkWhere(whereObj);
|
|
108
|
-
const sql = `SELECT COUNT(*) FROM "${getTenantSchema()}"."${sqlsanitize(
|
|
109
|
-
tbl
|
|
110
|
-
)}" ${where}`;
|
|
111
|
-
sql_log(sql, values);
|
|
112
|
-
const tq = await pool.query(sql, values);
|
|
113
|
-
|
|
114
|
-
return parseInt(tq.rows[0].count);
|
|
115
|
-
};
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Get version of PostgreSQL
|
|
119
|
-
* @param short - if true return short version info else full version info
|
|
120
|
-
* @returns {Promise<*>} returns version
|
|
121
|
-
*/
|
|
122
|
-
const getVersion = async (short) => {
|
|
123
|
-
const sql = `SELECT version();`;
|
|
124
|
-
sql_log(sql);
|
|
125
|
-
const tq = await pool.query(sql);
|
|
126
|
-
const v = tq.rows[0].version;
|
|
127
|
-
if (short) {
|
|
128
|
-
const ws = v.split(" ");
|
|
129
|
-
return ws[1];
|
|
130
|
-
}
|
|
131
|
-
return v;
|
|
132
|
-
};
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Delete rows in table
|
|
136
|
-
* @param tbl - table name
|
|
137
|
-
* @param whereObj - where object
|
|
138
|
-
* @returns {Promise<*>} result of delete execution
|
|
139
|
-
*/
|
|
140
|
-
const deleteWhere = async (tbl, whereObj, opts = {}) => {
|
|
141
|
-
const { where, values } = mkWhere(whereObj);
|
|
142
|
-
const sql = `delete FROM "${getTenantSchema()}"."${sqlsanitize(
|
|
143
|
-
tbl
|
|
144
|
-
)}" ${where}`;
|
|
145
|
-
sql_log(sql, values);
|
|
146
|
-
|
|
147
|
-
const tq = await (opts.client || pool).query(sql, values);
|
|
148
|
-
|
|
149
|
-
return tq.rows;
|
|
150
|
-
};
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* Insert rows into table
|
|
154
|
-
* @param tbl - table name
|
|
155
|
-
* @param obj - columns names and data
|
|
156
|
-
* @param opts - columns attributes
|
|
157
|
-
* @returns {Promise<*>} returns primary key column or Id column value. If primary key column is not defined then return value of Id column.
|
|
158
|
-
*/
|
|
159
|
-
const insert = async (tbl, obj, opts = {}) => {
|
|
160
|
-
const kvs = Object.entries(obj);
|
|
161
|
-
const fnameList = kvs.map(([k, v]) => `"${sqlsanitize(k)}"`).join();
|
|
162
|
-
var valPosList = [];
|
|
163
|
-
var valList = [];
|
|
164
|
-
const schema = getTenantSchema();
|
|
165
|
-
kvs.forEach(([k, v]) => {
|
|
166
|
-
if (v && v.next_version_by_id) {
|
|
167
|
-
valPosList.push(
|
|
168
|
-
`coalesce((select max(_version) from "${schema}"."${sqlsanitize(
|
|
169
|
-
tbl
|
|
170
|
-
)}" where id=${+v.next_version_by_id}), 0)+1`
|
|
171
|
-
);
|
|
172
|
-
} else {
|
|
173
|
-
valList.push(v);
|
|
174
|
-
valPosList.push(`$${valList.length}`);
|
|
175
|
-
}
|
|
176
|
-
});
|
|
177
|
-
const sql =
|
|
178
|
-
valPosList.length > 0
|
|
179
|
-
? `insert into "${schema}"."${sqlsanitize(
|
|
180
|
-
tbl
|
|
181
|
-
)}"(${fnameList}) values(${valPosList.join()}) returning ${
|
|
182
|
-
opts.noid ? "*" : opts.pk_name || "id"
|
|
183
|
-
}`
|
|
184
|
-
: `insert into "${schema}"."${sqlsanitize(
|
|
185
|
-
tbl
|
|
186
|
-
)}" DEFAULT VALUES returning ${opts.noid ? "*" : opts.pk_name || "id"}`;
|
|
187
|
-
sql_log(sql, valList);
|
|
188
|
-
const { rows } = await (opts.client || pool).query(sql, valList);
|
|
189
|
-
if (opts.noid) return;
|
|
190
|
-
else return rows[0][opts.pk_name || "id"];
|
|
191
|
-
};
|
|
192
|
-
|
|
193
|
-
/**
|
|
194
|
-
* Update table records
|
|
195
|
-
* @param tbl - table name
|
|
196
|
-
* @param obj - columns names and data
|
|
197
|
-
* @param id - id of record (primary key column value)
|
|
198
|
-
* @param opts - columns attributes
|
|
199
|
-
* @returns {Promise<void>} no result
|
|
200
|
-
*/
|
|
201
|
-
const update = async (tbl, obj, id, opts = {}) => {
|
|
202
|
-
const kvs = Object.entries(obj);
|
|
203
|
-
const assigns = kvs
|
|
204
|
-
.map(([k, v], ix) => `"${sqlsanitize(k)}"=$${ix + 1}`)
|
|
205
|
-
.join();
|
|
206
|
-
var valList = kvs.map(([k, v]) => v);
|
|
207
|
-
// TBD check that is correct - because in insert function opts.noid ? "*" : opts.pk_name || "id"
|
|
208
|
-
//valList.push(id === "undefined"? obj[opts.pk_name]: id);
|
|
209
|
-
valList.push(id === "undefined" ? obj[opts.pk_name || "id"] : id);
|
|
210
|
-
const q = `update "${getTenantSchema()}"."${sqlsanitize(
|
|
211
|
-
tbl
|
|
212
|
-
)}" set ${assigns} where ${opts.pk_name || "id"}=$${kvs.length + 1}`;
|
|
213
|
-
sql_log(q, valList);
|
|
214
|
-
await pool.query(q, valList);
|
|
215
|
-
};
|
|
216
|
-
|
|
217
|
-
/**
|
|
218
|
-
* Select one record
|
|
219
|
-
* @param tbl - table name
|
|
220
|
-
* @param where - where object
|
|
221
|
-
* @returns {Promise<*>} return first record from sql result
|
|
222
|
-
*/
|
|
223
|
-
const selectOne = async (tbl, where) => {
|
|
224
|
-
const rows = await select(tbl, where);
|
|
225
|
-
if (rows.length === 0) {
|
|
226
|
-
const w = mkWhere(where);
|
|
227
|
-
throw new Error(`no ${tbl} ${w.where} are ${w.values}`);
|
|
228
|
-
} else return rows[0];
|
|
229
|
-
};
|
|
230
|
-
|
|
231
|
-
/**
|
|
232
|
-
* Select one record or null if no records
|
|
233
|
-
* @param tbl - table name
|
|
234
|
-
* @param where - where object
|
|
235
|
-
* @returns {Promise<null|*>} - null if no record or first record data
|
|
236
|
-
*/
|
|
237
|
-
const selectMaybeOne = async (tbl, where) => {
|
|
238
|
-
const rows = await select(tbl, where);
|
|
239
|
-
if (rows.length === 0) return null;
|
|
240
|
-
else return rows[0];
|
|
241
|
-
};
|
|
242
|
-
|
|
243
|
-
/**
|
|
244
|
-
* Open db connection
|
|
245
|
-
* Only for PG.
|
|
246
|
-
* @returns {Promise<*>} db connection object
|
|
247
|
-
*/
|
|
248
|
-
// TBD Currently this function supported only for PG
|
|
249
|
-
const getClient = async () => await pool.connect();
|
|
250
|
-
|
|
251
|
-
/**
|
|
252
|
-
* Reset sequence
|
|
253
|
-
* Only for PG
|
|
254
|
-
* @param tblname - table name
|
|
255
|
-
* @returns {Promise<void>} no result
|
|
256
|
-
*/
|
|
257
|
-
const reset_sequence = async (tblname) => {
|
|
258
|
-
const sql = `SELECT setval(pg_get_serial_sequence('"${getTenantSchema()}"."${sqlsanitize(
|
|
259
|
-
tblname
|
|
260
|
-
)}"', 'id'), coalesce(max(id),0) + 1, false) FROM "${getTenantSchema()}"."${sqlsanitize(
|
|
261
|
-
tblname
|
|
262
|
-
)}";`;
|
|
263
|
-
await pool.query(sql);
|
|
264
|
-
};
|
|
265
|
-
|
|
266
|
-
/**
|
|
267
|
-
* Add unique constraint
|
|
268
|
-
* @param table_name - table name
|
|
269
|
-
* @param field_names - list of columns (members of constraint)
|
|
270
|
-
* @returns {Promise<void>} no result
|
|
271
|
-
*/
|
|
272
|
-
const add_unique_constraint = async (table_name, field_names) => {
|
|
273
|
-
// TBD check that there are no problems with lenght of constraint name
|
|
274
|
-
const sql = `alter table "${getTenantSchema()}"."${sqlsanitize(
|
|
275
|
-
table_name
|
|
276
|
-
)}" add CONSTRAINT "${sqlsanitize(table_name)}_${field_names
|
|
277
|
-
.map((f) => sqlsanitize(f))
|
|
278
|
-
.join("_")}_unique" UNIQUE (${field_names
|
|
279
|
-
.map((f) => `"${sqlsanitize(f)}"`)
|
|
280
|
-
.join(",")});`;
|
|
281
|
-
sql_log(sql);
|
|
282
|
-
await pool.query(sql);
|
|
283
|
-
};
|
|
284
|
-
|
|
285
|
-
/**
|
|
286
|
-
* Drop unique constraint
|
|
287
|
-
* @param table_name - table name
|
|
288
|
-
* @param field_names - list of columns (members of constraint)
|
|
289
|
-
* @returns {Promise<void>} no results
|
|
290
|
-
*/
|
|
291
|
-
const drop_unique_constraint = async (table_name, field_names) => {
|
|
292
|
-
// TBD check that there are no problems with lenght of constraint name
|
|
293
|
-
const sql = `alter table "${getTenantSchema()}"."${sqlsanitize(
|
|
294
|
-
table_name
|
|
295
|
-
)}" drop CONSTRAINT "${sqlsanitize(table_name)}_${field_names
|
|
296
|
-
.map((f) => sqlsanitize(f))
|
|
297
|
-
.join("_")}_unique";`;
|
|
298
|
-
sql_log(sql);
|
|
299
|
-
await pool.query(sql);
|
|
300
|
-
};
|
|
301
|
-
/**
|
|
302
|
-
* Copy data from CSV to table?
|
|
303
|
-
* Only for PG
|
|
304
|
-
* @param fileStream - file stream
|
|
305
|
-
* @param tableName - table name
|
|
306
|
-
* @param fieldNames - list of columns
|
|
307
|
-
* @param client - db connection
|
|
308
|
-
* @returns {Promise<unknown>} new Promise
|
|
309
|
-
*/
|
|
310
|
-
const copyFrom1 = (fileStream, tableName, fieldNames, client) => {
|
|
311
|
-
// TBD describe difference between CopyFrom and CopyFrom1
|
|
312
|
-
const quote = (s) => `"${s}"`;
|
|
313
|
-
const sql = `COPY "${sqlsanitize(tableName)}" (${fieldNames
|
|
314
|
-
.map(quote)
|
|
315
|
-
.join(",")}) FROM STDIN CSV HEADER`;
|
|
316
|
-
sql_log(sql);
|
|
317
|
-
|
|
318
|
-
var stream = client.query(copyStreams.from(sql));
|
|
319
|
-
|
|
320
|
-
return new Promise((resolve, reject) => {
|
|
321
|
-
fileStream.on("error", reject);
|
|
322
|
-
stream.on("error", reject);
|
|
323
|
-
stream.on("finish", resolve);
|
|
324
|
-
fileStream.pipe(stream).on("error", reject);
|
|
325
|
-
});
|
|
326
|
-
};
|
|
327
|
-
/**
|
|
328
|
-
* Copy data from CSV to table?
|
|
329
|
-
* Only for PG
|
|
330
|
-
* @param fileStream - file stream
|
|
331
|
-
* @param tableName - table name
|
|
332
|
-
* @param fieldNames - list of columns
|
|
333
|
-
* @param client - db connection
|
|
334
|
-
* @returns {Promise<void>} no results
|
|
335
|
-
*/
|
|
336
|
-
const copyFrom = async (fileStream, tableName, fieldNames, client) => {
|
|
337
|
-
// TBD describe difference between CopyFrom and CopyFrom1
|
|
338
|
-
const quote = (s) => `"${s}"`;
|
|
339
|
-
const sql = `COPY "${sqlsanitize(tableName)}" (${fieldNames
|
|
340
|
-
.map(quote)
|
|
341
|
-
.join(",")}) FROM STDIN CSV HEADER`;
|
|
342
|
-
sql_log(sql);
|
|
343
|
-
|
|
344
|
-
const stream = client.query(copyStreams.from(sql));
|
|
345
|
-
return await promisify(pipeline)(fileStream, stream);
|
|
346
|
-
};
|
|
347
|
-
|
|
348
|
-
module.exports = {
|
|
349
|
-
pool,
|
|
350
|
-
query: (text, params) => {
|
|
351
|
-
sql_log(text, params);
|
|
352
|
-
return pool.query(text, params);
|
|
353
|
-
},
|
|
354
|
-
select,
|
|
355
|
-
selectOne,
|
|
356
|
-
selectMaybeOne,
|
|
357
|
-
count,
|
|
358
|
-
insert,
|
|
359
|
-
update,
|
|
360
|
-
deleteWhere,
|
|
361
|
-
close,
|
|
362
|
-
sql_log,
|
|
363
|
-
changeConnection,
|
|
364
|
-
set_sql_logging,
|
|
365
|
-
get_sql_logging,
|
|
366
|
-
getClient,
|
|
367
|
-
mkWhere,
|
|
368
|
-
drop_reset_schema,
|
|
369
|
-
add_unique_constraint,
|
|
370
|
-
drop_unique_constraint,
|
|
371
|
-
reset_sequence,
|
|
372
|
-
getVersion,
|
|
373
|
-
copyFrom,
|
|
374
|
-
};
|