@saltcorn/server 0.9.4-beta.8 → 0.9.4-beta.9
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 +8 -2
- package/package.json +8 -8
- package/public/saltcorn.js +3 -0
- package/routes/actions.js +17 -2
- package/routes/common_lists.js +304 -135
- package/routes/pageedit.js +19 -4
- package/routes/tables.js +17 -5
- package/routes/tag_entries.js +12 -4
- package/routes/tags.js +61 -12
- package/routes/view.js +7 -0
- package/routes/viewedit.js +37 -3
- package/tests/view.test.js +115 -15
- package/wrapper.js +0 -1
- package/public/relation_helpers.js +0 -351
|
@@ -1,351 +0,0 @@
|
|
|
1
|
-
var relationHelpers = (() => {
|
|
2
|
-
// internal helper to build an object, structured for the picker
|
|
3
|
-
const buildLayers = (path, pathArr, result) => {
|
|
4
|
-
let currentLevel = result;
|
|
5
|
-
for (const relation of pathArr) {
|
|
6
|
-
if (relation.type === "Inbound") {
|
|
7
|
-
const existing = currentLevel.inboundKeys.find(
|
|
8
|
-
(key) => key.name === relation.key && key.table === relation.table
|
|
9
|
-
);
|
|
10
|
-
if (existing) {
|
|
11
|
-
currentLevel = existing;
|
|
12
|
-
} else {
|
|
13
|
-
const nextLevel = {
|
|
14
|
-
name: relation.key,
|
|
15
|
-
table: relation.table,
|
|
16
|
-
inboundKeys: [],
|
|
17
|
-
fkeys: [],
|
|
18
|
-
};
|
|
19
|
-
currentLevel.inboundKeys.push(nextLevel);
|
|
20
|
-
currentLevel = nextLevel;
|
|
21
|
-
}
|
|
22
|
-
} else if (relation.type === "Foreign") {
|
|
23
|
-
const existing = currentLevel.fkeys.find(
|
|
24
|
-
(key) => key.name === relation.key
|
|
25
|
-
);
|
|
26
|
-
if (existing) {
|
|
27
|
-
currentLevel = existing;
|
|
28
|
-
} else {
|
|
29
|
-
const nextLevel = {
|
|
30
|
-
name: relation.key,
|
|
31
|
-
table: relation.table,
|
|
32
|
-
inboundKeys: [],
|
|
33
|
-
fkeys: [],
|
|
34
|
-
};
|
|
35
|
-
currentLevel.fkeys.push(nextLevel);
|
|
36
|
-
currentLevel = nextLevel;
|
|
37
|
-
}
|
|
38
|
-
} else if (relation.type === "Independent") {
|
|
39
|
-
currentLevel.fkeys.push({
|
|
40
|
-
name: "None (no relation)",
|
|
41
|
-
table: relation.table,
|
|
42
|
-
inboundKeys: [],
|
|
43
|
-
fkeys: [],
|
|
44
|
-
relPath: path,
|
|
45
|
-
});
|
|
46
|
-
} else if (relation.type === "Own") {
|
|
47
|
-
currentLevel.fkeys.push({
|
|
48
|
-
name: "Same table",
|
|
49
|
-
table: "",
|
|
50
|
-
inboundKeys: [],
|
|
51
|
-
fkeys: [],
|
|
52
|
-
relPath: path,
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
currentLevel.relPath = path;
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* build an array of relation objects from a path string
|
|
61
|
-
* '.' stands for no relation
|
|
62
|
-
* '.table' stands for same table
|
|
63
|
-
* @param {*} path relation path string separated by '.', the first token is the source table
|
|
64
|
-
* @param {*} tableNameCache an object with table name as key and table object as value
|
|
65
|
-
* @returns
|
|
66
|
-
*/
|
|
67
|
-
const parseRelationPath = (path, tableNameCache) => {
|
|
68
|
-
if (path === ".")
|
|
69
|
-
return [{ type: "Independent", table: "None (no relation)" }];
|
|
70
|
-
const tokens = path.split(".");
|
|
71
|
-
if (tokens.length === 2)
|
|
72
|
-
return [{ type: "Own", table: `${tokens[1]} (same table)` }];
|
|
73
|
-
else if (tokens.length >= 3) {
|
|
74
|
-
const result = [];
|
|
75
|
-
let currentTbl = tokens[1];
|
|
76
|
-
for (const relation of tokens.slice(2)) {
|
|
77
|
-
if (relation.indexOf("$") > 0) {
|
|
78
|
-
const [inboundTbl, inboundKey] = relation.split("$");
|
|
79
|
-
result.push({ type: "Inbound", table: inboundTbl, key: inboundKey });
|
|
80
|
-
currentTbl = inboundTbl;
|
|
81
|
-
} else {
|
|
82
|
-
const srcTbl = tableNameCache[currentTbl];
|
|
83
|
-
const fk = srcTbl.foreign_keys.find((fk) => fk.name === relation);
|
|
84
|
-
if (fk) {
|
|
85
|
-
const targetTbl = tableNameCache[fk.reftable_name];
|
|
86
|
-
result.push({
|
|
87
|
-
type: "Foreign",
|
|
88
|
-
table: targetTbl.name,
|
|
89
|
-
key: relation,
|
|
90
|
-
});
|
|
91
|
-
currentTbl = targetTbl.name;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
return result;
|
|
96
|
-
}
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* build an array of relation objects from a legacy relation
|
|
101
|
-
* @param {string} type relation type (ChildList, Independent, Own, OneToOneShow, ParentShow)
|
|
102
|
-
* @param {string} rest rest of the legaccy relation
|
|
103
|
-
* @param {string} parentTbl source table
|
|
104
|
-
* @returns
|
|
105
|
-
*/
|
|
106
|
-
const parseLegacyRelation = (type, rest, parentTbl) => {
|
|
107
|
-
switch (type) {
|
|
108
|
-
case "ChildList": {
|
|
109
|
-
const path = rest ? rest.split(".") : [];
|
|
110
|
-
if (path.length === 3) {
|
|
111
|
-
const [viewName, table, key] = path;
|
|
112
|
-
return [
|
|
113
|
-
{
|
|
114
|
-
type: "Inbound",
|
|
115
|
-
table,
|
|
116
|
-
key,
|
|
117
|
-
},
|
|
118
|
-
];
|
|
119
|
-
} else if (path.length === 5) {
|
|
120
|
-
const [viewName, thrTbl, thrTblFkey, fromTbl, fromTblFkey] = path;
|
|
121
|
-
return [
|
|
122
|
-
{
|
|
123
|
-
type: "Inbound",
|
|
124
|
-
table: thrTbl,
|
|
125
|
-
key: thrTblFkey,
|
|
126
|
-
},
|
|
127
|
-
{
|
|
128
|
-
type: "Inbound",
|
|
129
|
-
table: fromTbl,
|
|
130
|
-
key: fromTblFkey,
|
|
131
|
-
},
|
|
132
|
-
];
|
|
133
|
-
}
|
|
134
|
-
break;
|
|
135
|
-
}
|
|
136
|
-
case "Independent": {
|
|
137
|
-
return [{ type: "Independent", table: "None (no relation)" }];
|
|
138
|
-
}
|
|
139
|
-
case "Own": {
|
|
140
|
-
return [{ type: "Own", table: `${parentTbl} (same table)` }];
|
|
141
|
-
}
|
|
142
|
-
case "OneToOneShow": {
|
|
143
|
-
const tokens = rest ? rest.split(".") : [];
|
|
144
|
-
if (tokens.length !== 3) break;
|
|
145
|
-
const [viewname, relatedTbl, fkey] = tokens;
|
|
146
|
-
return [{ type: "Inbound", table: relatedTbl, key: fkey }];
|
|
147
|
-
}
|
|
148
|
-
case "ParentShow": {
|
|
149
|
-
const tokens = rest ? rest.split(".") : [];
|
|
150
|
-
if (tokens.length !== 3) break;
|
|
151
|
-
const [viewname, parentTbl, fkey] = tokens;
|
|
152
|
-
return [{ type: "Foreign", table: parentTbl, key: fkey }];
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
return [];
|
|
156
|
-
};
|
|
157
|
-
|
|
158
|
-
const ViewDisplayType = {
|
|
159
|
-
ROW_REQUIRED: "ROW_REQUIRED",
|
|
160
|
-
NO_ROW_LIMIT: "NO_ROW_LIMIT",
|
|
161
|
-
INVALID: "INVALID",
|
|
162
|
-
};
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* prepare the relations finder
|
|
166
|
-
* @param {object} tablesCache
|
|
167
|
-
* @param {object} allViews
|
|
168
|
-
* @param {number} maxDepth
|
|
169
|
-
*/
|
|
170
|
-
const RelationsFinder = function (tablesCache, allViews, maxDepth) {
|
|
171
|
-
this.maxDepth = +maxDepth;
|
|
172
|
-
if (isNaN(this.maxDepth)) {
|
|
173
|
-
console.log(`maxDepth '${maxDepth}' is not a number, set to 6`);
|
|
174
|
-
this.maxDepth = 6;
|
|
175
|
-
}
|
|
176
|
-
this.allViews = allViews;
|
|
177
|
-
const { tableIdCache, tableNameCache, fieldCache } = tablesCache;
|
|
178
|
-
this.tableIdCache = tableIdCache;
|
|
179
|
-
this.tableNameCache = tableNameCache;
|
|
180
|
-
this.fieldCache = fieldCache;
|
|
181
|
-
};
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* find relations between a source table and a subview
|
|
185
|
-
* @param {string} sourceTblName
|
|
186
|
-
* @param {string} subView
|
|
187
|
-
* @param {string[]} excluded
|
|
188
|
-
* @returns {object} {paths: string[], layers: object}
|
|
189
|
-
*/
|
|
190
|
-
RelationsFinder.prototype.findRelations = function (
|
|
191
|
-
sourceTblName,
|
|
192
|
-
subView,
|
|
193
|
-
excluded
|
|
194
|
-
) {
|
|
195
|
-
let paths = [];
|
|
196
|
-
const layers = { table: sourceTblName, inboundKeys: [], fkeys: [] };
|
|
197
|
-
try {
|
|
198
|
-
const view = this.allViews.find((v) => v.name === subView);
|
|
199
|
-
if (!view) throw new Error(`The view ${subView} does not exist`);
|
|
200
|
-
if (excluded?.find((e) => e === view.viewtemplate)) {
|
|
201
|
-
console.log(`view ${subView} is excluded`);
|
|
202
|
-
return { paths, layers };
|
|
203
|
-
}
|
|
204
|
-
switch (view.display_type) {
|
|
205
|
-
case ViewDisplayType.ROW_REQUIRED:
|
|
206
|
-
paths = this.singleRelationPaths(sourceTblName, subView, excluded);
|
|
207
|
-
break;
|
|
208
|
-
case ViewDisplayType.NO_ROW_LIMIT:
|
|
209
|
-
paths = this.multiRelationPaths(sourceTblName, subView, excluded);
|
|
210
|
-
break;
|
|
211
|
-
default:
|
|
212
|
-
throw new Error(
|
|
213
|
-
`view ${subView}: The displayType (${view.display_type}) is not valid`
|
|
214
|
-
);
|
|
215
|
-
}
|
|
216
|
-
for (const path of paths)
|
|
217
|
-
buildLayers(path, parseRelationPath(path, this.tableNameCache), layers);
|
|
218
|
-
} catch (error) {
|
|
219
|
-
console.log(error);
|
|
220
|
-
} finally {
|
|
221
|
-
return { paths, layers };
|
|
222
|
-
}
|
|
223
|
-
};
|
|
224
|
-
|
|
225
|
-
/**
|
|
226
|
-
* find relations between a source table and a subview with single row display (e.g. show)
|
|
227
|
-
* @param {string} sourceTblName
|
|
228
|
-
* @param {string} subView
|
|
229
|
-
* @param {string[]} excluded
|
|
230
|
-
* @returns
|
|
231
|
-
*/
|
|
232
|
-
RelationsFinder.prototype.singleRelationPaths = function (
|
|
233
|
-
sourceTblName,
|
|
234
|
-
subView,
|
|
235
|
-
excluded
|
|
236
|
-
) {
|
|
237
|
-
const result = [];
|
|
238
|
-
const subViewObj = this.allViews.find((v) => v.name === subView);
|
|
239
|
-
if (!subViewObj) throw new Error(`The view ${subView} does not exist`);
|
|
240
|
-
if (excluded?.find((e) => e === subViewObj.viewtemplate)) {
|
|
241
|
-
console.log(`view ${subView} is excluded`);
|
|
242
|
-
return result;
|
|
243
|
-
}
|
|
244
|
-
const sourceTbl = this.tableNameCache[sourceTblName];
|
|
245
|
-
if (!sourceTbl)
|
|
246
|
-
throw new Error(`The table ${sourceTblName} does not exist`);
|
|
247
|
-
// 1. parent relations
|
|
248
|
-
const parentRelations = sourceTbl.foreign_keys;
|
|
249
|
-
if (sourceTbl.id === subViewObj.table_id) result.push(`.${sourceTblName}`);
|
|
250
|
-
for (const relation of parentRelations) {
|
|
251
|
-
const targetTbl = this.tableNameCache[relation.reftable_name];
|
|
252
|
-
if (!targetTbl)
|
|
253
|
-
throw new Error(`The table ${relation.reftable_name} does not exist`);
|
|
254
|
-
if (targetTbl.id === subViewObj.table_id)
|
|
255
|
-
result.push(`.${sourceTblName}.${relation.name}`);
|
|
256
|
-
}
|
|
257
|
-
// 2. OneToOneShow
|
|
258
|
-
const uniqueFksToSrc = (this.fieldCache[sourceTblName] || []).filter(
|
|
259
|
-
(f) => f.is_unique
|
|
260
|
-
);
|
|
261
|
-
for (const relation of uniqueFksToSrc) {
|
|
262
|
-
const targetTbl = this.tableIdCache[relation.table_id];
|
|
263
|
-
if (!targetTbl)
|
|
264
|
-
throw new Error(`The table ${relation.table_id} does not exist`);
|
|
265
|
-
if (targetTbl.id === subViewObj.table_id)
|
|
266
|
-
result.push(`.${sourceTblName}.${targetTbl.name}$${relation.name}`);
|
|
267
|
-
}
|
|
268
|
-
// 3. inbound_self_relations
|
|
269
|
-
const srcFks = sourceTbl.foreign_keys;
|
|
270
|
-
for (const fkToSrc of uniqueFksToSrc) {
|
|
271
|
-
const refTable = this.tableIdCache[fkToSrc.table_id];
|
|
272
|
-
if (!refTable)
|
|
273
|
-
throw new Error(`The table ${fkToSrc.table_id} does not exist`);
|
|
274
|
-
const fromSrcToRef = srcFks.filter(
|
|
275
|
-
(field) => field.reftable_name === refTable.name
|
|
276
|
-
);
|
|
277
|
-
for (const toRef of fromSrcToRef) {
|
|
278
|
-
if (fkToSrc.reftable_name === sourceTblName)
|
|
279
|
-
result.push(`.${sourceTblName}.${toRef.name}.${fkToSrc.name}`);
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
return result;
|
|
283
|
-
};
|
|
284
|
-
|
|
285
|
-
/**
|
|
286
|
-
* find relations between a source table and a subview with multiple rows display (e.g. list)
|
|
287
|
-
* @param {string} sourceTblName
|
|
288
|
-
* @param {string} subView
|
|
289
|
-
* @param {string[]} excluded
|
|
290
|
-
* @returns
|
|
291
|
-
*/
|
|
292
|
-
RelationsFinder.prototype.multiRelationPaths = function (
|
|
293
|
-
sourceTblName,
|
|
294
|
-
subView,
|
|
295
|
-
excluded
|
|
296
|
-
) {
|
|
297
|
-
const result = ["."]; // none no relation
|
|
298
|
-
const subViewObj = this.allViews.find((v) => v.name === subView);
|
|
299
|
-
if (!subViewObj) throw new Error(`The view ${subView} does not exist`);
|
|
300
|
-
if (excluded?.find((e) => e === subViewObj.viewtemplate)) {
|
|
301
|
-
console.log(`view ${subView} is excluded`);
|
|
302
|
-
return result;
|
|
303
|
-
}
|
|
304
|
-
const sourceTbl = this.tableNameCache[sourceTblName];
|
|
305
|
-
if (!sourceTbl)
|
|
306
|
-
throw new Error(`The table ${sourceTblName} does not exist`);
|
|
307
|
-
if (sourceTbl.id === subViewObj.table_id) result.push(`.${sourceTblName}`);
|
|
308
|
-
const searcher = (current, path, level, visited) => {
|
|
309
|
-
if (level > this.maxDepth) return;
|
|
310
|
-
const visitedFkCopy = new Set(visited);
|
|
311
|
-
for (const fk of current.foreign_keys) {
|
|
312
|
-
if (visitedFkCopy.has(fk.id)) continue;
|
|
313
|
-
visitedFkCopy.add(fk.id);
|
|
314
|
-
const target = this.tableNameCache[fk.reftable_name];
|
|
315
|
-
if (!target)
|
|
316
|
-
throw new Error(`The table ${fk.reftable_name} does not exist`);
|
|
317
|
-
const newPath = `${path}.${fk.name}`;
|
|
318
|
-
if (target.id === subViewObj.table_id) result.push(newPath);
|
|
319
|
-
searcher(target, newPath, level + 1, visitedFkCopy);
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
const visitedInboundCopy = new Set(visited);
|
|
323
|
-
for (const inbound of this.fieldCache[current.name] || []) {
|
|
324
|
-
if (visitedInboundCopy.has(inbound.id)) continue;
|
|
325
|
-
visitedInboundCopy.add(inbound.id);
|
|
326
|
-
const target = this.tableIdCache[inbound.table_id];
|
|
327
|
-
if (!target)
|
|
328
|
-
throw new Error(`The table ${inbound.table_id} does not exist`);
|
|
329
|
-
const newPath = `${path}.${target.name}$${inbound.name}`;
|
|
330
|
-
if (target.id === subViewObj.table_id) result.push(newPath);
|
|
331
|
-
searcher(target, newPath, level + 1, visitedInboundCopy);
|
|
332
|
-
}
|
|
333
|
-
};
|
|
334
|
-
const path = `.${sourceTblName}`;
|
|
335
|
-
const visited = new Set();
|
|
336
|
-
searcher(sourceTbl, path, 0, visited);
|
|
337
|
-
return result;
|
|
338
|
-
};
|
|
339
|
-
|
|
340
|
-
return {
|
|
341
|
-
RelationsFinder: RelationsFinder,
|
|
342
|
-
ViewDisplayType: ViewDisplayType,
|
|
343
|
-
parseRelationPath: parseRelationPath,
|
|
344
|
-
parseLegacyRelation: parseLegacyRelation,
|
|
345
|
-
};
|
|
346
|
-
})();
|
|
347
|
-
|
|
348
|
-
// make the module available for jest with react
|
|
349
|
-
if (typeof process !== "undefined" && process.env?.NODE_ENV === "test") {
|
|
350
|
-
module.exports = relationHelpers;
|
|
351
|
-
}
|