@galadrim/galadmin 0.2.12 → 0.3.0-alpha.2.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/.agents/report/blockers.json +1 -0
- package/.agents/report/failures/2026-09-23-api-user-filter-contract.md +31 -0
- package/.agents/report/failures/2026-09-23-api-values-contract.md +25 -0
- package/.agents/report/failures/2026-10-01-cycle-02-create-callback-contract.md +17 -0
- package/.agents/report/failures/2026-10-01-cycle-06-delete-callback-id-contract.md +17 -0
- package/.agents/report/failures/2026-10-01-cycle-07-panel-delete-callback-id-contract.md +24 -0
- package/.agents/report/failures/2026-10-01-cycle-13-update-callback-id-contract.md +17 -0
- package/.agents/report/failures/2026-10-01-cycle-15-rich-text-upload-boundary-contract.md +24 -0
- package/.agents/report/failures/2026-10-03-cycle-06-record-import-parser-contract.md +11 -0
- package/.agents/report/failures/2026-10-03-cycle-07-panel-import-parser-contract.md +24 -0
- package/.agents/report/failures/2026-10-07-resume-cycle-02-api-assembly-contract.md +46 -0
- package/.agents/report/port/2026-09-23-api-config-enrichment.md +13 -0
- package/.agents/report/port/2026-09-23-api-file-upload.md +11 -0
- package/.agents/report/port/2026-09-23-api-query-authorization.md +14 -0
- package/.agents/report/port/2026-09-23-api-request-configuration.md +11 -0
- package/.agents/report/port/2026-09-23-database-manager.md +13 -0
- package/.agents/report/port/2026-10-01-api-adjacent-record-id.md +9 -0
- package/.agents/report/port/2026-10-01-cycle-01-api-config-route.md +9 -0
- package/.agents/report/port/2026-10-01-cycle-03-custom-action.md +9 -0
- package/.agents/report/port/2026-10-01-cycle-04-custom-component.md +7 -0
- package/.agents/report/port/2026-10-01-cycle-05-custom-query.md +7 -0
- package/.agents/report/port/2026-10-01-cycle-08-bulk-delete.md +9 -0
- package/.agents/report/port/2026-10-01-cycle-09-duplicate-record.md +9 -0
- package/.agents/report/port/2026-10-01-cycle-10-notification-badges.md +9 -0
- package/.agents/report/port/2026-10-01-cycle-11-record.md +7 -0
- package/.agents/report/port/2026-10-01-cycle-12-search-companies.md +11 -0
- package/.agents/report/port/2026-10-01-cycle-14-upload-file.md +7 -0
- package/.agents/report/port/2026-10-03-cycle-01-create-record.md +11 -0
- package/.agents/report/port/2026-10-03-cycle-02-delete-record.md +11 -0
- package/.agents/report/port/2026-10-03-cycle-03-panel-delete-record.md +11 -0
- package/.agents/report/port/2026-10-03-cycle-04-update-record.md +11 -0
- package/.agents/report/port/2026-10-03-cycle-05-api-values.md +11 -0
- package/.agents/report/port/2026-10-06-cycle-01-record-import.md +9 -0
- package/.agents/report/port/2026-10-06-cycle-03-rich-text-upload.md +7 -0
- package/.agents/report/port/2026-10-06-cycle-04-user-filters.md +11 -0
- package/.agents/report/port/2026-10-06-resume-cycle-01-panel-import.md +9 -0
- package/.agents/report/port/2026-10-07-cycle-01-api-assembly.md +16 -0
- package/.agents/report/port/2026-10-07-cycle-02-index-entry-point.md +13 -0
- package/.agents/report/remediation/2026-10-02-api-user-filter-remediation.md +71 -0
- package/.agents/report/remediation/2026-10-02-api-values-remediation.md +17 -0
- package/.agents/report/remediation/2026-10-03-callback-id-contract-remediation.md +36 -0
- package/.agents/report/remediation/2026-10-06-import-parser-remediation.md +41 -0
- package/.agents/report/remediation/2026-10-06-rich-text-upload-remediation.md +33 -0
- package/.agents/report/remediation/2026-10-07-api-assembly-remediation.md +32 -0
- package/.agents/skills/port-javascript-to-typescript/SKILL.md +42 -0
- package/.agents/skills/port-javascript-to-typescript/agents/openai.yaml +3 -0
- package/.agents/skills/port-javascript-to-typescript/references/blockers.md +31 -0
- package/.agents/skills/port-javascript-to-typescript/references/planning.md +43 -0
- package/.agents/skills/port-javascript-to-typescript/references/porting.md +9 -0
- package/.agents/skills/port-javascript-to-typescript/references/reporting.md +24 -0
- package/.agents/skills/port-javascript-to-typescript/references/rules.md +8 -0
- package/.agents/skills/port-javascript-to-typescript/references/selection.md +16 -0
- package/.agents/skills/port-javascript-to-typescript/references/tools.md +20 -0
- package/.agents/skills/port-javascript-to-typescript/scripts/check.sh +17 -0
- package/.agents/skills/port-javascript-to-typescript/scripts/graph.sh +30 -0
- package/.agents/skills/port-javascript-to-typescript/scripts/madge.json +12 -0
- package/.agents/skills/port-javascript-to-typescript/scripts/oxlint.json +14 -0
- package/.agents/skills/port-javascript-to-typescript/scripts/test-tools.cjs +76 -0
- package/AGENTS.md +5 -0
- package/dist/api/authorization.d.ts +38 -0
- package/dist/api/authorization.js +74 -0
- package/dist/api/authorization.js.map +1 -0
- package/dist/api/config.d.ts +42 -0
- package/dist/api/config.js +105 -0
- package/dist/api/config.js.map +1 -0
- package/dist/api/filterUser.d.ts +2 -0
- package/dist/api/filterUser.js +24 -0
- package/dist/api/filterUser.js.map +1 -0
- package/dist/api/importRecords.d.ts +1 -0
- package/dist/api/importRecords.js +16 -0
- package/dist/api/importRecords.js.map +1 -0
- package/dist/api/queries.d.ts +48 -0
- package/dist/api/queries.js +139 -0
- package/dist/api/queries.js.map +1 -0
- package/dist/api/request.d.ts +29 -0
- package/dist/api/request.js +3 -0
- package/dist/api/request.js.map +1 -0
- package/dist/api/routes/adjacentRecordId.d.ts +10 -0
- package/dist/api/routes/adjacentRecordId.js +13 -0
- package/dist/api/routes/adjacentRecordId.js.map +1 -0
- package/dist/api/routes/config.d.ts +3 -0
- package/dist/api/routes/config.js +27 -0
- package/dist/api/routes/config.js.map +1 -0
- package/dist/api/routes/createRecord.d.ts +19 -0
- package/dist/api/routes/createRecord.js +93 -0
- package/dist/api/routes/createRecord.js.map +1 -0
- package/dist/api/routes/customAction.d.ts +17 -0
- package/dist/api/routes/customAction.js +35 -0
- package/dist/api/routes/customAction.js.map +1 -0
- package/dist/api/routes/customComponent.d.ts +3 -0
- package/dist/api/routes/customComponent.js +34 -0
- package/dist/api/routes/customComponent.js.map +1 -0
- package/dist/api/routes/customQuery.d.ts +7 -0
- package/dist/api/routes/customQuery.js +33 -0
- package/dist/api/routes/customQuery.js.map +1 -0
- package/dist/api/routes/deleteRecord.d.ts +9 -0
- package/dist/api/routes/deleteRecord.js +46 -0
- package/dist/api/routes/deleteRecord.js.map +1 -0
- package/dist/api/routes/deleteRecordFromPanel.d.ts +10 -0
- package/dist/api/routes/deleteRecordFromPanel.js +36 -0
- package/dist/api/routes/deleteRecordFromPanel.js.map +1 -0
- package/dist/api/routes/deleteRecords.d.ts +9 -0
- package/dist/api/routes/deleteRecords.js +12 -0
- package/dist/api/routes/deleteRecords.js.map +1 -0
- package/dist/api/routes/duplicateRecord.d.ts +17 -0
- package/dist/api/routes/duplicateRecord.js +55 -0
- package/dist/api/routes/duplicateRecord.js.map +1 -0
- package/dist/api/routes/foreignSelectRecords.d.ts +10 -0
- package/dist/api/routes/foreignSelectRecords.js +61 -0
- package/dist/api/routes/foreignSelectRecords.js.map +1 -0
- package/dist/api/routes/notificationBadges.d.ts +6 -0
- package/dist/api/routes/notificationBadges.js +31 -0
- package/dist/api/routes/notificationBadges.js.map +1 -0
- package/dist/api/routes/record.d.ts +7 -0
- package/dist/api/routes/record.js +32 -0
- package/dist/api/routes/record.js.map +1 -0
- package/dist/api/routes/records.d.ts +14 -0
- package/dist/api/routes/records.js +57 -0
- package/dist/api/routes/records.js.map +1 -0
- package/dist/api/routes/recordsExport.d.ts +13 -0
- package/dist/api/routes/recordsExport.js +67 -0
- package/dist/api/routes/recordsExport.js.map +1 -0
- package/dist/api/routes/recordsImport.d.ts +4 -0
- package/dist/api/routes/recordsImport.js +100 -0
- package/dist/api/routes/recordsImport.js.map +1 -0
- package/dist/api/routes/searchCompanies.d.ts +17 -0
- package/dist/api/routes/searchCompanies.js +72 -0
- package/dist/api/routes/searchCompanies.js.map +1 -0
- package/dist/api/routes/subrecordsImport.d.ts +4 -0
- package/dist/api/routes/subrecordsImport.js +94 -0
- package/dist/api/routes/subrecordsImport.js.map +1 -0
- package/dist/api/routes/updateRecord.d.ts +13 -0
- package/dist/api/routes/updateRecord.js +55 -0
- package/dist/api/routes/updateRecord.js.map +1 -0
- package/dist/api/routes/uploadFile.d.ts +3 -0
- package/dist/api/routes/uploadFile.js +13 -0
- package/dist/api/routes/uploadFile.js.map +1 -0
- package/dist/api/routes/uploadFileFromBetterRichTextInput.d.ts +3 -0
- package/dist/api/routes/uploadFileFromBetterRichTextInput.js +33 -0
- package/dist/api/routes/uploadFileFromBetterRichTextInput.js.map +1 -0
- package/dist/api/uploadFile.d.ts +4 -0
- package/dist/api/uploadFile.js +209 -0
- package/dist/api/uploadFile.js.map +1 -0
- package/dist/api/values.d.ts +6 -0
- package/dist/api/values.js +88 -0
- package/dist/api/values.js.map +1 -0
- package/dist/api.d.ts +8 -27
- package/dist/api.js +80 -1387
- package/dist/api.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +8 -2
- package/dist/index.js.map +1 -1
- package/dist/tsconfig.strict.tsbuildinfo +1 -0
- package/dist/types/config.d.ts +15 -5
- package/dist/types/config.js +4 -0
- package/dist/types/config.js.map +1 -1
- package/dist/types/database.d.ts +1 -0
- package/dist/types/database.js +3 -0
- package/dist/types/database.js.map +1 -0
- package/dist/utils/dbManager.d.ts +32 -7
- package/dist/utils/dbManager.js +59 -7
- package/dist/utils/dbManager.js.map +1 -1
- package/package.json +12 -3
- package/src/api/authorization.ts +129 -0
- package/src/api/config.ts +152 -0
- package/src/api/filterUser.ts +28 -0
- package/src/api/importRecords.ts +16 -0
- package/src/api/queries.ts +194 -0
- package/src/api/request.ts +31 -0
- package/src/api/routes/adjacentRecordId.ts +20 -0
- package/src/api/routes/config.ts +15 -0
- package/src/api/routes/createRecord.ts +131 -0
- package/src/api/routes/customAction.ts +60 -0
- package/src/api/routes/customComponent.ts +44 -0
- package/src/api/routes/customQuery.ts +53 -0
- package/src/api/routes/deleteRecord.ts +59 -0
- package/src/api/routes/deleteRecordFromPanel.ts +58 -0
- package/src/api/routes/deleteRecords.ts +28 -0
- package/src/api/routes/duplicateRecord.ts +73 -0
- package/src/api/routes/foreignSelectRecords.ts +80 -0
- package/src/api/routes/notificationBadges.ts +47 -0
- package/src/api/routes/record.ts +48 -0
- package/src/api/routes/records.ts +91 -0
- package/src/api/routes/recordsExport.ts +92 -0
- package/src/api/routes/recordsImport.ts +115 -0
- package/src/api/routes/searchCompanies.ts +96 -0
- package/src/api/routes/subrecordsImport.ts +114 -0
- package/src/api/routes/updateRecord.ts +74 -0
- package/src/api/routes/uploadFile.ts +16 -0
- package/src/api/routes/uploadFileFromBetterRichTextInput.ts +41 -0
- package/src/api/uploadFile.ts +249 -0
- package/src/api/values.ts +96 -0
- package/src/api.ts +94 -1580
- package/src/index.ts +42 -25
- package/src/types/config.ts +15 -5
- package/src/types/database.ts +2 -0
- package/src/utils/dbManager.ts +99 -14
- package/tests/api-helpers.test.ts +59 -0
- package/tests/dbManager.test.ts +16 -1
- package/tests/helpers/fakeDb.ts +9 -7
- package/tests/integration/authorizationBoundary.integration.test.ts +57 -0
- package/tests/integration/configEnrichment.integration.test.ts +79 -0
- package/tests/integration/customComponentBoundary.integration.test.ts +58 -0
- package/tests/integration/dbManager.integration.test.ts +105 -3
- package/tests/integration/importParserChild.cjs +55 -0
- package/tests/integration/indexRuntime.integration.test.ts +22 -0
- package/tests/integration/indexRuntimeChild.cjs +110 -0
- package/tests/integration/queries.integration.test.ts +88 -0
- package/tests/integration/requestConfig.integration.test.ts +118 -0
- package/tests/integration/routes.authorization.integration.test.ts +1 -0
- package/tests/integration/routes.config.integration.test.ts +39 -1
- package/tests/integration/routes.crud.integration.test.ts +233 -1
- package/tests/integration/routes.custom.integration.test.ts +119 -0
- package/tests/integration/routes.foreign.integration.test.ts +3 -2
- package/tests/integration/routes.importExport.integration.test.ts +308 -6
- package/tests/integration/routes.integration.test.ts +4 -0
- package/tests/integration/routes.records.integration.test.ts +82 -6
- package/tests/integration/routes.upload.integration.test.ts +123 -0
- package/tests/integration/userFilters.integration.test.ts +225 -0
- package/tsconfig.json +1 -1
- package/tsconfig.strict.json +45 -0
- package/dist/front/ckeditor5-build/src/ckeditor.d.ts +0 -43
- package/dist/front/ckeditor5-build/src/ckeditor.js +0 -130
- package/dist/front/ckeditor5-build/src/ckeditor.js.map +0 -1
- package/dist/tsconfig.tsbuildinfo +0 -1
package/src/api.ts
CHANGED
|
@@ -1,230 +1,60 @@
|
|
|
1
1
|
import * as express from "express";
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
import
|
|
13
|
-
import
|
|
14
|
-
|
|
15
|
-
import
|
|
16
|
-
import
|
|
17
|
-
import
|
|
18
|
-
import
|
|
19
|
-
import
|
|
20
|
-
|
|
21
|
-
import
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
// (body for POST bodies, query for /uploadFile). Import routes carry the
|
|
59
|
-
// viewIndex inside a multipart body and are guarded inline in their handlers.
|
|
60
|
-
export const MUTATING_VIEW_ROUTES = new Set([
|
|
61
|
-
"/updateRecord",
|
|
62
|
-
"/createRecord",
|
|
63
|
-
"/deleteRecord",
|
|
64
|
-
"/deleteRecords",
|
|
65
|
-
"/deleteRecordFromPanel",
|
|
66
|
-
"/customAction",
|
|
67
|
-
"/uploadFile",
|
|
68
|
-
]);
|
|
69
|
-
// /duplicateRecord references its table via req.body.table (not a viewIndex),
|
|
70
|
-
// so it is guarded inline in its handler instead of by the generic middleware.
|
|
71
|
-
|
|
72
|
-
// Column names are interpolated into INSERT/UPDATE statements; only accept plain
|
|
73
|
-
// SQL identifiers so a crafted key cannot break out of the column list.
|
|
74
|
-
export const areSafeColumnNames = (data) =>
|
|
75
|
-
!data || Object.keys(data).every((k) => /^[A-Za-z0-9_]+$/.test(k));
|
|
76
|
-
|
|
77
|
-
// Tables the current role is allowed to write, gathered from its writable views
|
|
78
|
-
// and their creatable panels — used to bound /duplicateRecord's target table.
|
|
79
|
-
export const writableTables = (req) => {
|
|
80
|
-
const role = roleOf(req);
|
|
81
|
-
const tables = new Set<string>();
|
|
82
|
-
for (const view of req.config?.views || []) {
|
|
83
|
-
if (!canWriteView(view, role)) continue;
|
|
84
|
-
if (view.tableName) tables.add(view.tableName);
|
|
85
|
-
for (const panel of view.recordViewPanels || []) {
|
|
86
|
-
if (panel.tableName) tables.add(panel.tableName);
|
|
87
|
-
if (panel.createConfig?.tableName)
|
|
88
|
-
tables.add(panel.createConfig.tableName);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
return tables;
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
// Reject client-supplied SQL fragments (the `filters` array is concatenated
|
|
95
|
-
// raw into the WHERE clause). With multipleStatements off this already blocks
|
|
96
|
-
// stacked writes; the pattern also blocks UNION/comment/file exfil breakouts.
|
|
97
|
-
export const looksLikeSqlBreakout = (frag) =>
|
|
98
|
-
typeof frag === "string" &&
|
|
99
|
-
/(;|--|\/\*|\bunion\b|\binto\s+(out|dump)file\b|\bsleep\s*\(|\bbenchmark\s*\(|\bload_file\s*\()/i.test(
|
|
100
|
-
frag,
|
|
101
|
-
);
|
|
102
|
-
|
|
103
|
-
export function sqlAnd(conditions) {
|
|
104
|
-
const existingConditions = [];
|
|
105
|
-
for (const condition of conditions) {
|
|
106
|
-
if (condition && condition.length > 0) {
|
|
107
|
-
existingConditions.push(condition);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
if (existingConditions.length <= 1) {
|
|
111
|
-
return existingConditions[0] || null;
|
|
112
|
-
} else {
|
|
113
|
-
let result = "";
|
|
114
|
-
for (let i = 0; i < existingConditions.length; i++) {
|
|
115
|
-
if (i > 0) {
|
|
116
|
-
result += " AND ";
|
|
117
|
-
}
|
|
118
|
-
result += "(" + existingConditions[i] + ")";
|
|
119
|
-
}
|
|
120
|
-
return result;
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
export async function getFormattedValue(column, record) {
|
|
125
|
-
let value;
|
|
126
|
-
try {
|
|
127
|
-
record.options = column.options;
|
|
128
|
-
if (column.formatValueForExport) {
|
|
129
|
-
value = await column.formatValueForExport(record);
|
|
130
|
-
} else if (column.formattedValue) {
|
|
131
|
-
value = evalExpression(column.formattedValue, record);
|
|
132
|
-
} else if (column.formatValue) {
|
|
133
|
-
value = await column.formatValue(record);
|
|
134
|
-
} else if (column.dataType == "tinyint") {
|
|
135
|
-
value = record[column.name] == 1 ? "Oui" : "Non";
|
|
136
|
-
} else {
|
|
137
|
-
value = record[column.name];
|
|
138
|
-
}
|
|
139
|
-
return value === null || typeof value == "undefined" ? "" : value;
|
|
140
|
-
} catch (err) {
|
|
141
|
-
return value || "";
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
export function evalExpression(expression, data) {
|
|
146
|
-
let varString = "";
|
|
147
|
-
let statement = "";
|
|
148
|
-
|
|
149
|
-
try {
|
|
150
|
-
for (let key in data) {
|
|
151
|
-
statement =
|
|
152
|
-
"var " +
|
|
153
|
-
key +
|
|
154
|
-
" = " +
|
|
155
|
-
(typeof data[key] === "string"
|
|
156
|
-
? "'" +
|
|
157
|
-
data[key]
|
|
158
|
-
.replace(/'/g, "\\'")
|
|
159
|
-
.replace(/\n/g, "")
|
|
160
|
-
.replace(/\r/g, "") +
|
|
161
|
-
"'"
|
|
162
|
-
: typeof data[key] === "object"
|
|
163
|
-
? JSON.stringify(data[key])
|
|
164
|
-
: data[key]) +
|
|
165
|
-
";";
|
|
166
|
-
varString += statement;
|
|
167
|
-
}
|
|
168
|
-
const result = eval(varString + expression);
|
|
169
|
-
return result;
|
|
170
|
-
} catch (e) {
|
|
171
|
-
console.log("Problème d'évaluation de l'expression", statement);
|
|
172
|
-
console.error(e);
|
|
173
|
-
return "Error";
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
export function formatValueForInsertion(value: any, dbType: string) {
|
|
178
|
-
if (value !== 0 && !value) {
|
|
179
|
-
return "NULL";
|
|
180
|
-
}
|
|
181
|
-
if (value instanceof Date) {
|
|
182
|
-
return value.toISOString();
|
|
183
|
-
}
|
|
184
|
-
if (typeof value === "string") {
|
|
185
|
-
if (dbType === "POSTGRESQL") {
|
|
186
|
-
return "'" + value.replace(/'/g, "''") + "'";
|
|
187
|
-
} else {
|
|
188
|
-
return "'" + value.replace(/'/g, "\\'") + "'";
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
if (typeof value === "object") {
|
|
192
|
-
return "'" + JSON.stringify(value).replace(/'/g, "\\'") + "'";
|
|
193
|
-
}
|
|
194
|
-
if (typeof value === "boolean") {
|
|
195
|
-
return value ? "TRUE" : "FALSE";
|
|
196
|
-
}
|
|
197
|
-
if (typeof value === "number") {
|
|
198
|
-
return value;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
return value;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
declare global {
|
|
205
|
-
namespace Express {
|
|
206
|
-
interface Request {
|
|
207
|
-
config?: any;
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// Columns of `view` the current role may not see (cf. the config middleware below).
|
|
213
|
-
export const hiddenColumnsOf = (view) => (view && view.__hiddenColumns) || [];
|
|
214
|
-
|
|
215
|
-
// Removes the hidden columns from a record before it is sent.
|
|
216
|
-
export const stripHiddenColumns = (view, record) => {
|
|
217
|
-
for (const name of hiddenColumnsOf(view)) delete record[name];
|
|
218
|
-
return record;
|
|
219
|
-
};
|
|
220
|
-
|
|
221
|
-
// Whether a write payload touches a hidden column.
|
|
222
|
-
export const touchesHiddenColumn = (view, data) =>
|
|
223
|
-
hiddenColumnsOf(view).some(
|
|
224
|
-
(name) => data && Object.prototype.hasOwnProperty.call(data, name),
|
|
225
|
-
);
|
|
226
|
-
|
|
227
|
-
export default (db, config) => {
|
|
2
|
+
import type { Config, Column, StandardColumn } from "./types/config";
|
|
3
|
+
import type DbManager from "./utils/dbManager";
|
|
4
|
+
import "./api/request";
|
|
5
|
+
import {
|
|
6
|
+
roleOf,
|
|
7
|
+
canWriteView,
|
|
8
|
+
viewFromRequest,
|
|
9
|
+
MUTATING_VIEW_ROUTES,
|
|
10
|
+
looksLikeSqlBreakout,
|
|
11
|
+
} from "./api/authorization";
|
|
12
|
+
import { enrichConfig } from "./api/config";
|
|
13
|
+
import createRecordQueries, { collectAllowedQueries } from "./api/queries";
|
|
14
|
+
import configRoute from "./api/routes/config";
|
|
15
|
+
import customComponentRoute from "./api/routes/customComponent";
|
|
16
|
+
import adjacentRecordIdRoute from "./api/routes/adjacentRecordId";
|
|
17
|
+
import foreignSelectRecordsRoute from "./api/routes/foreignSelectRecords";
|
|
18
|
+
import recordsRoute from "./api/routes/records";
|
|
19
|
+
import recordsExportRoute from "./api/routes/recordsExport";
|
|
20
|
+
import recordsImportRoute from "./api/routes/recordsImport";
|
|
21
|
+
import subrecordsImportRoute from "./api/routes/subrecordsImport";
|
|
22
|
+
import recordRoute from "./api/routes/record";
|
|
23
|
+
import updateRecordRoute from "./api/routes/updateRecord";
|
|
24
|
+
import customActionRoute from "./api/routes/customAction";
|
|
25
|
+
import deleteRecordRoute from "./api/routes/deleteRecord";
|
|
26
|
+
import deleteRecordFromPanelRoute from "./api/routes/deleteRecordFromPanel";
|
|
27
|
+
import deleteRecordsRoute from "./api/routes/deleteRecords";
|
|
28
|
+
import notificationBadgesRoute from "./api/routes/notificationBadges";
|
|
29
|
+
import duplicateRecordRoute from "./api/routes/duplicateRecord";
|
|
30
|
+
import createRecordRoute from "./api/routes/createRecord";
|
|
31
|
+
import customQueryRoute from "./api/routes/customQuery";
|
|
32
|
+
import uploadFileFromBetterRichTextInputRoute from "./api/routes/uploadFileFromBetterRichTextInput";
|
|
33
|
+
import uploadFileRoute from "./api/routes/uploadFile";
|
|
34
|
+
import searchCompaniesRoute from "./api/routes/searchCompanies";
|
|
35
|
+
|
|
36
|
+
export {
|
|
37
|
+
ROOT_ROLE,
|
|
38
|
+
roleOf,
|
|
39
|
+
canWriteView,
|
|
40
|
+
viewFromRequest,
|
|
41
|
+
MUTATING_VIEW_ROUTES,
|
|
42
|
+
areSafeColumnNames,
|
|
43
|
+
writableTables,
|
|
44
|
+
hiddenColumnsOf,
|
|
45
|
+
stripHiddenColumns,
|
|
46
|
+
touchesHiddenColumn,
|
|
47
|
+
looksLikeSqlBreakout,
|
|
48
|
+
} from "./api/authorization";
|
|
49
|
+
export { generateUniqueIdForObject, enrichConfig, enrichColumn } from "./api/config";
|
|
50
|
+
export { sqlAnd, collectAllowedQueries } from "./api/queries";
|
|
51
|
+
export {
|
|
52
|
+
getFormattedValue,
|
|
53
|
+
evalExpression,
|
|
54
|
+
formatValueForInsertion,
|
|
55
|
+
} from "./api/values";
|
|
56
|
+
|
|
57
|
+
export default (db: DbManager, config: Config) => {
|
|
228
58
|
const app = express();
|
|
229
59
|
|
|
230
60
|
app.use((req, res, next) => {
|
|
@@ -234,21 +64,21 @@ export default (db, config) => {
|
|
|
234
64
|
.filter(
|
|
235
65
|
(x) =>
|
|
236
66
|
!x.roles ||
|
|
237
|
-
(req.session
|
|
67
|
+
(req.session!.user &&
|
|
238
68
|
[...(x.roles || []), ...(x.readRoles || [])].includes(
|
|
239
|
-
req.session
|
|
69
|
+
req.session!.user.role,
|
|
240
70
|
)),
|
|
241
71
|
)
|
|
242
72
|
.map((view) => {
|
|
243
73
|
const readOnly =
|
|
244
|
-
req.session
|
|
74
|
+
req.session!.user &&
|
|
245
75
|
view.roles &&
|
|
246
|
-
!view.roles.includes(req.session
|
|
76
|
+
!view.roles.includes(req.session!.user.role);
|
|
247
77
|
if (readOnly) {
|
|
248
78
|
return {
|
|
249
79
|
...view,
|
|
250
80
|
canEdit: false,
|
|
251
|
-
canCreate: false,
|
|
81
|
+
canCreate: false as const,
|
|
252
82
|
canDelete: false,
|
|
253
83
|
clickToEdit: false,
|
|
254
84
|
};
|
|
@@ -258,10 +88,10 @@ export default (db, config) => {
|
|
|
258
88
|
})
|
|
259
89
|
.map((view) => {
|
|
260
90
|
if (view.type === "table") {
|
|
261
|
-
const allowed = (column) =>
|
|
91
|
+
const allowed = (column: Column) =>
|
|
262
92
|
!column.roles ||
|
|
263
|
-
(req.session
|
|
264
|
-
column.roles.includes(req.session
|
|
93
|
+
(req.session!.user &&
|
|
94
|
+
column.roles.includes(req.session!.user.role));
|
|
265
95
|
return {
|
|
266
96
|
...view,
|
|
267
97
|
columns: view.columns.filter(allowed),
|
|
@@ -270,7 +100,12 @@ export default (db, config) => {
|
|
|
270
100
|
// here, stripped from every record sent back and refused in every write.
|
|
271
101
|
__hiddenColumns: view.columns
|
|
272
102
|
.filter((column) => !allowed(column))
|
|
273
|
-
.map((column) => column.name)
|
|
103
|
+
.map((column: Column & StandardColumn) => column.name)
|
|
104
|
+
/*
|
|
105
|
+
#PORTING-EDIT
|
|
106
|
+
Nullish names cannot identify protected database fields; excluding them avoids treating missing names as literal "undefined" or "null" keys.
|
|
107
|
+
*/
|
|
108
|
+
.filter((name): name is string => name != null),
|
|
274
109
|
};
|
|
275
110
|
} else {
|
|
276
111
|
return view;
|
|
@@ -281,8 +116,8 @@ export default (db, config) => {
|
|
|
281
116
|
});
|
|
282
117
|
|
|
283
118
|
app.use(async (req, res, next) => {
|
|
284
|
-
if (!req.config
|
|
285
|
-
await enrichConfig(req.config
|
|
119
|
+
if (!req.config!.enriched) {
|
|
120
|
+
await enrichConfig(req.config!, db);
|
|
286
121
|
}
|
|
287
122
|
next();
|
|
288
123
|
});
|
|
@@ -314,960 +149,43 @@ export default (db, config) => {
|
|
|
314
149
|
next();
|
|
315
150
|
});
|
|
316
151
|
|
|
317
|
-
|
|
318
|
-
let queryString = "";
|
|
319
|
-
if (params.filters) {
|
|
320
|
-
queryString = " WHERE (" + params.filters + ")";
|
|
321
|
-
}
|
|
322
|
-
if (params.query) {
|
|
323
|
-
const columns = params.columns;
|
|
324
|
-
let isNewCondition = true;
|
|
325
|
-
for (let i in columns) {
|
|
326
|
-
if (
|
|
327
|
-
columns[i].searchable !== false &&
|
|
328
|
-
(columns[i].searchableName || columns[i].name)
|
|
329
|
-
) {
|
|
330
|
-
if (queryString.length == 0) {
|
|
331
|
-
queryString += " WHERE (";
|
|
332
|
-
} else if (isNewCondition) {
|
|
333
|
-
queryString += " AND (";
|
|
334
|
-
} else {
|
|
335
|
-
queryString += " OR ";
|
|
336
|
-
}
|
|
337
|
-
if (columns[i].searchableName) {
|
|
338
|
-
// on ne doit pas ajouter de backticks sur le searchableName car celui-ci peut être une fonction
|
|
339
|
-
// exemple : searchableName = "CONCAT_WS(' ', firstname, lastname)"
|
|
340
|
-
queryString += columns[i].searchableName;
|
|
341
|
-
} else if (columns[i].name) {
|
|
342
|
-
queryString += columns[i].name
|
|
343
|
-
.split(".")
|
|
344
|
-
.map((x) => quoteIdentifier(x, db.dbType))
|
|
345
|
-
.join(".");
|
|
346
|
-
}
|
|
347
|
-
// SECURITY: escape the user query per dialect — PostgreSQL does not
|
|
348
|
-
// treat backslashes as escapes inside standard strings, so a `\'`
|
|
349
|
-
// would terminate the literal early.
|
|
350
|
-
const searchEscaped =
|
|
351
|
-
db.dbType === "POSTGRESQL"
|
|
352
|
-
? params.query.replace(/'/g, "''")
|
|
353
|
-
: params.query.replace(/'/g, "\\'");
|
|
354
|
-
queryString += " LIKE '%" + searchEscaped + "%'";
|
|
355
|
-
isNewCondition = false;
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
queryString += ")";
|
|
359
|
-
}
|
|
360
|
-
const limit = params.allRecords ? 999999 : parseInt(params.limit);
|
|
361
|
-
const query =
|
|
362
|
-
params.customQuery ||
|
|
363
|
-
`SELECT
|
|
364
|
-
${params.selectExpression || "*"}
|
|
365
|
-
FROM ${params.table}
|
|
366
|
-
${params.join || ""}
|
|
367
|
-
${queryString}
|
|
368
|
-
${params.groupBy ? "GROUP BY " + params.groupBy : ""}
|
|
369
|
-
${params.having ? " HAVING " + params.having : ""}
|
|
370
|
-
${params.orderBy || params.primaryId ? " ORDER BY " + (params.orderBy || `${params.table}.${params.primaryId}`) : ""}`;
|
|
371
|
-
const { err, rows } = await db.query(
|
|
372
|
-
`${query} LIMIT ${limit} OFFSET ${Math.max((params.page - 1) * limit, 0)}`,
|
|
373
|
-
);
|
|
374
|
-
if (err) {
|
|
375
|
-
console.error(err);
|
|
376
|
-
return {
|
|
377
|
-
error: err,
|
|
378
|
-
};
|
|
379
|
-
}
|
|
380
|
-
let count = undefined;
|
|
381
|
-
if (computeCount) {
|
|
382
|
-
const { err: countErr, rows: countRows } = await db.query(
|
|
383
|
-
`SELECT COUNT(*) AS count FROM (${query}) s`,
|
|
384
|
-
);
|
|
385
|
-
if (countErr) {
|
|
386
|
-
const { rows: allRows } = await db.query(query);
|
|
387
|
-
count = allRows.length;
|
|
388
|
-
} else {
|
|
389
|
-
count = countRows?.[0]?.count || 0;
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
return {
|
|
394
|
-
rows,
|
|
395
|
-
queryString,
|
|
396
|
-
input: {
|
|
397
|
-
query,
|
|
398
|
-
params,
|
|
399
|
-
},
|
|
400
|
-
count,
|
|
401
|
-
};
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
async function findRecordByPosition(params) {
|
|
405
|
-
return await getRecords(
|
|
406
|
-
Object.assign(params, {
|
|
407
|
-
page: parseInt(params.position) + parseInt(params.offset),
|
|
408
|
-
limit: 1,
|
|
409
|
-
}),
|
|
410
|
-
);
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
app.get("/config", async (req, res) => {
|
|
414
|
-
try {
|
|
415
|
-
const { mysql, auth, sessionSecret, ...safeConfig } = req.config;
|
|
416
|
-
res.send({
|
|
417
|
-
config: serializeJs(safeConfig),
|
|
418
|
-
});
|
|
419
|
-
} catch (e) {
|
|
420
|
-
console.error(e);
|
|
421
|
-
res.sendStatus(500);
|
|
422
|
-
}
|
|
423
|
-
});
|
|
152
|
+
const { getRecords, findRecordByPosition } = createRecordQueries(db);
|
|
424
153
|
|
|
425
|
-
app.get("/
|
|
426
|
-
try {
|
|
427
|
-
const pkg = JSON.parse(
|
|
428
|
-
fs.readFileSync(
|
|
429
|
-
path.join(__dirname, "../src/front/package.json"),
|
|
430
|
-
"utf-8",
|
|
431
|
-
),
|
|
432
|
-
);
|
|
433
|
-
const external = Object.keys(pkg.dependencies || {});
|
|
434
|
-
const bundle = await rollup.rollup({
|
|
435
|
-
input: path.join(
|
|
436
|
-
__dirname,
|
|
437
|
-
"../../",
|
|
438
|
-
req.query.componentPath as string,
|
|
439
|
-
),
|
|
440
|
-
plugins: [
|
|
441
|
-
resolve(),
|
|
442
|
-
babel({
|
|
443
|
-
configFile: path.join(__dirname, "../babel.config"),
|
|
444
|
-
babelHelpers: "bundled",
|
|
445
|
-
}),
|
|
446
|
-
],
|
|
447
|
-
external,
|
|
448
|
-
});
|
|
449
|
-
const { output } = await bundle.generate({
|
|
450
|
-
format: "cjs",
|
|
451
|
-
exports: "auto",
|
|
452
|
-
});
|
|
453
|
-
res.send(output[0].code);
|
|
454
|
-
} catch (err) {
|
|
455
|
-
console.error(err);
|
|
456
|
-
res.sendStatus(500);
|
|
457
|
-
}
|
|
458
|
-
});
|
|
154
|
+
app.get("/config", configRoute());
|
|
459
155
|
|
|
460
|
-
app.
|
|
461
|
-
const { rows, error } = await findRecordByPosition(req.body);
|
|
462
|
-
if (error) {
|
|
463
|
-
return res.status(500).send(error);
|
|
464
|
-
}
|
|
465
|
-
const recordId = rows[0] ? rows[0][req.body.primaryId] : null;
|
|
466
|
-
res.send({
|
|
467
|
-
recordId: recordId,
|
|
468
|
-
});
|
|
469
|
-
});
|
|
156
|
+
app.get("/customComponent", customComponentRoute(__dirname));
|
|
470
157
|
|
|
471
|
-
app.post("/
|
|
472
|
-
const view = req.config.views[req.body.viewIndex];
|
|
473
|
-
const column = [
|
|
474
|
-
...view.columns,
|
|
475
|
-
...(view.recordViewPanels
|
|
476
|
-
?.map((x) => x?.createConfig?.columns)
|
|
477
|
-
?.filter((x) => x)
|
|
478
|
-
?.flat() || []),
|
|
479
|
-
].find((x) => x.__id__ === req.body.columnId);
|
|
480
|
-
const queryEscaped =
|
|
481
|
-
db.dbType === "POSTGRESQL"
|
|
482
|
-
? req.body.query?.replace(/'/g, "''")
|
|
483
|
-
: req.body.query?.replace(/'/g, "\\'");
|
|
484
|
-
const { rows: records, error } = await getRecords({
|
|
485
|
-
table: column.table,
|
|
486
|
-
page: 1,
|
|
487
|
-
filters: sqlAnd([
|
|
488
|
-
column.filters,
|
|
489
|
-
req.body.query
|
|
490
|
-
? `${column.searchableName} LIKE '${column.wholeSearch ? "%" : ""}${queryEscaped}%'`
|
|
491
|
-
: null,
|
|
492
|
-
column.filtersFromUser
|
|
493
|
-
? column.filtersFromUser(
|
|
494
|
-
req.session.user.userId,
|
|
495
|
-
req.session.user.role,
|
|
496
|
-
)
|
|
497
|
-
: null,
|
|
498
|
-
]),
|
|
499
|
-
join: column.join,
|
|
500
|
-
selectExpression: column.selectExpression,
|
|
501
|
-
orderBy: column.orderBy,
|
|
502
|
-
limit: column.limit || 1000,
|
|
503
|
-
});
|
|
504
|
-
if (error) {
|
|
505
|
-
return res.status(500).send(error);
|
|
506
|
-
}
|
|
507
|
-
const options = await Promise.all(
|
|
508
|
-
records.map(async (record) => {
|
|
509
|
-
let label;
|
|
510
|
-
if (column.formatForeignValue) {
|
|
511
|
-
label = await column.formatForeignValue(record);
|
|
512
|
-
} else if (column.formatValue) {
|
|
513
|
-
label = await column.formatValue(record);
|
|
514
|
-
} else if (column.formattedValue) {
|
|
515
|
-
label = evalExpression(column.formattedValue, record);
|
|
516
|
-
} else {
|
|
517
|
-
label = record[column.name];
|
|
518
|
-
}
|
|
519
|
-
const value = record[column.foreignName] || record[column.name];
|
|
520
|
-
return {
|
|
521
|
-
value,
|
|
522
|
-
label,
|
|
523
|
-
};
|
|
524
|
-
}),
|
|
525
|
-
);
|
|
526
|
-
res.send(options);
|
|
527
|
-
});
|
|
158
|
+
app.post("/adjacentRecordId", adjacentRecordIdRoute(findRecordByPosition));
|
|
528
159
|
|
|
529
|
-
app.post("/
|
|
530
|
-
const view = req.config.views[req.body.viewIndex];
|
|
531
|
-
const isDesc = req.body.isDesc === null ? view.isDesc : req.body.isDesc;
|
|
532
|
-
const subview =
|
|
533
|
-
view.subviews && req.body.subviewIndex !== undefined
|
|
534
|
-
? view.subviews[req.body.subviewIndex]
|
|
535
|
-
: null;
|
|
536
|
-
const orderBy = req.body.orderBy || subview?.orderBy || view.orderBy;
|
|
537
|
-
const {
|
|
538
|
-
rows: records,
|
|
539
|
-
input,
|
|
540
|
-
error,
|
|
541
|
-
count,
|
|
542
|
-
} = await getRecords(
|
|
543
|
-
{
|
|
544
|
-
table: view.tableName,
|
|
545
|
-
page: req.body.page,
|
|
546
|
-
filters: sqlAnd([
|
|
547
|
-
view.filters,
|
|
548
|
-
subview?.filters,
|
|
549
|
-
...req.body.filters,
|
|
550
|
-
view.filtersFromUser
|
|
551
|
-
? view.filtersFromUser(
|
|
552
|
-
req.session.user.userId,
|
|
553
|
-
req.session.user.role,
|
|
554
|
-
)
|
|
555
|
-
: null,
|
|
556
|
-
]),
|
|
557
|
-
having: subview?.having,
|
|
558
|
-
customQuery: view.customQuery,
|
|
559
|
-
join: view.join,
|
|
560
|
-
selectExpression: view.selectExpression,
|
|
561
|
-
orderBy: [
|
|
562
|
-
orderBy ? `${orderBy} ${isDesc ? "DESC" : "ASC"}` : null,
|
|
563
|
-
view.primaryId,
|
|
564
|
-
]
|
|
565
|
-
.filter((x) => x)
|
|
566
|
-
.join(", "),
|
|
567
|
-
primaryId: view.primaryId,
|
|
568
|
-
query: req.body.query,
|
|
569
|
-
groupBy: view.groupBy,
|
|
570
|
-
columns: view.columns,
|
|
571
|
-
limit: view.limit || 10,
|
|
572
|
-
},
|
|
573
|
-
true,
|
|
574
|
-
);
|
|
575
|
-
if (error) {
|
|
576
|
-
return res.status(500).send(error);
|
|
577
|
-
}
|
|
160
|
+
app.post("/foreignSelectRecords", foreignSelectRecordsRoute(db, getRecords));
|
|
578
161
|
|
|
579
|
-
|
|
580
|
-
if (column.formatValue) {
|
|
581
|
-
for (const record of records) {
|
|
582
|
-
record[`__formatValue-${column.__id__}__`] =
|
|
583
|
-
await column.formatValue(record);
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
for (const record of records) stripHiddenColumns(view, record);
|
|
588
|
-
res.send({
|
|
589
|
-
records,
|
|
590
|
-
input,
|
|
591
|
-
count,
|
|
592
|
-
});
|
|
593
|
-
});
|
|
162
|
+
app.post("/records", recordsRoute(getRecords));
|
|
594
163
|
|
|
595
164
|
const formattedValueToFunction = {};
|
|
596
165
|
|
|
597
|
-
app.post("/recordsExport",
|
|
598
|
-
const view = req.config.views[req.body.viewIndex];
|
|
599
|
-
const orderBy = req.body.orderBy || view.orderBy;
|
|
600
|
-
const isDesc = req.body.isDesc === null ? view.isDesc : req.body.isDesc;
|
|
601
|
-
const subview =
|
|
602
|
-
view.subviews && req.body.subviewIndex !== undefined
|
|
603
|
-
? view.subviews[req.body.subviewIndex]
|
|
604
|
-
: null;
|
|
605
|
-
const { rows: records, error } = await getRecords({
|
|
606
|
-
table: view.tableName,
|
|
607
|
-
page: 1,
|
|
608
|
-
allRecords: true,
|
|
609
|
-
filters: sqlAnd([
|
|
610
|
-
view.filters,
|
|
611
|
-
subview?.filters,
|
|
612
|
-
req.body.filters,
|
|
613
|
-
// SECURITY: scope the export exactly like the list view, otherwise a
|
|
614
|
-
// user could download rows their filtersFromUser clause hides.
|
|
615
|
-
view.filtersFromUser
|
|
616
|
-
? view.filtersFromUser(
|
|
617
|
-
req.session.user.userId,
|
|
618
|
-
req.session.user.role,
|
|
619
|
-
)
|
|
620
|
-
: null,
|
|
621
|
-
]),
|
|
622
|
-
having: subview?.having,
|
|
623
|
-
customQuery: view.customQuery,
|
|
624
|
-
join: view.join,
|
|
625
|
-
selectExpression: view.selectExpression,
|
|
626
|
-
orderBy: orderBy ? `${orderBy} ${isDesc ? "DESC" : "ASC"}` : null,
|
|
627
|
-
primaryId: view.primaryId,
|
|
628
|
-
query: req.body.query,
|
|
629
|
-
groupBy: view.groupBy,
|
|
630
|
-
columns: view.columns,
|
|
631
|
-
limit: view.limit || 10,
|
|
632
|
-
});
|
|
633
|
-
if (error) {
|
|
634
|
-
return res.status(500).send(error);
|
|
635
|
-
}
|
|
636
|
-
let output = "";
|
|
637
|
-
const columns = view.columns;
|
|
638
|
-
for (let j = 0; j < columns.length; j++) {
|
|
639
|
-
if (!columns[j].hideInTable || columns[j].includeInExport) {
|
|
640
|
-
if (j > 0) output += ";";
|
|
641
|
-
output += columns[j].label;
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
for (let i in records) {
|
|
645
|
-
const el = records[i];
|
|
646
|
-
output += "\r\n";
|
|
647
|
-
for (let j = 0; j < columns.length; j++) {
|
|
648
|
-
if (!columns[j].hideInTable || columns[j].includeInExport) {
|
|
649
|
-
if (j > 0) output += ";";
|
|
650
|
-
if (columns[j].dataType == "textarea") {
|
|
651
|
-
output += '"';
|
|
652
|
-
}
|
|
653
|
-
output += await getFormattedValue(columns[j], el);
|
|
654
|
-
if (columns[j].dataType == "textarea") {
|
|
655
|
-
output += '"';
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
}
|
|
660
|
-
res.send({
|
|
661
|
-
content: output,
|
|
662
|
-
});
|
|
663
|
-
});
|
|
664
|
-
|
|
665
|
-
app.post("/recordsImport", async function (req, res) {
|
|
666
|
-
const form = new multiparty.Form();
|
|
667
|
-
form.parse(req, async function (err, fields, files) {
|
|
668
|
-
if (err) {
|
|
669
|
-
return res.status(500).send(err);
|
|
670
|
-
}
|
|
671
|
-
// Get viewIndex in fields
|
|
672
|
-
const view = req.config.views[fields.viewIndex[0]];
|
|
673
|
-
// SECURITY: the generic write-guard cannot see a viewIndex buried in a
|
|
674
|
-
// multipart body, so imports enforce the view's write roles here.
|
|
675
|
-
if (!view || !canWriteView(view, roleOf(req))) {
|
|
676
|
-
return res.sendStatus(403);
|
|
677
|
-
}
|
|
678
|
-
if (typeof view.parseImportFile !== "function") {
|
|
679
|
-
return res
|
|
680
|
-
.status(500)
|
|
681
|
-
.send(
|
|
682
|
-
'"parseImportFile" doit être défini dans la configuration de la vue.',
|
|
683
|
-
);
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
const records = await view.parseImportFile(files.data[0]);
|
|
687
|
-
const columns = [];
|
|
688
|
-
if (records.length === 0) {
|
|
689
|
-
return res.send({
|
|
690
|
-
affectedRows: 0,
|
|
691
|
-
});
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
try {
|
|
695
|
-
for (let record of records) {
|
|
696
|
-
for (let key in record) {
|
|
697
|
-
if (!columns.includes(key)) {
|
|
698
|
-
columns.push(key);
|
|
699
|
-
}
|
|
700
|
-
}
|
|
701
|
-
}
|
|
702
|
-
if (columns.some((name) => hiddenColumnsOf(view).includes(name))) {
|
|
703
|
-
return res.sendStatus(403);
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
let queryString;
|
|
707
|
-
if (db.dbType === "POSTGRESQL") {
|
|
708
|
-
queryString = `INSERT INTO ?? (${columns.map((x) => quoteIdentifier(x, db.dbType)).join(", ")}) VALUES`;
|
|
709
|
-
|
|
710
|
-
for (let record of records) {
|
|
711
|
-
const valuesList = columns
|
|
712
|
-
.map((x) => formatValueForInsertion(record[x], db.dbType))
|
|
713
|
-
.join(", ");
|
|
714
|
-
queryString += ` (${valuesList}),`;
|
|
715
|
-
}
|
|
716
|
-
queryString =
|
|
717
|
-
queryString.slice(0, -1) +
|
|
718
|
-
` ON CONFLICT (${view.importFileUniqueField}) DO UPDATE SET `;
|
|
719
|
-
|
|
720
|
-
for (let column of columns) {
|
|
721
|
-
queryString += `${quoteIdentifier(column, db.dbType)} = EXCLUDED.${quoteIdentifier(column, db.dbType)},`;
|
|
722
|
-
}
|
|
723
|
-
} else {
|
|
724
|
-
queryString =
|
|
725
|
-
"INSERT INTO ?? (" +
|
|
726
|
-
columns.map((x) => quoteIdentifier(x, db.dbType)).join(", ") +
|
|
727
|
-
") VALUES";
|
|
728
|
-
for (let record of records) {
|
|
729
|
-
queryString +=
|
|
730
|
-
" (" +
|
|
731
|
-
columns
|
|
732
|
-
.map((x) => formatValueForInsertion(record[x], db.dbType))
|
|
733
|
-
.join(", ") +
|
|
734
|
-
"),";
|
|
735
|
-
}
|
|
736
|
-
queryString = queryString.slice(0, -1) + " ON DUPLICATE KEY UPDATE";
|
|
737
|
-
for (let column of columns) {
|
|
738
|
-
queryString += ` ${quoteIdentifier(column, db.dbType)} = VALUES(${quoteIdentifier(column, db.dbType)}),`;
|
|
739
|
-
}
|
|
740
|
-
}
|
|
741
|
-
queryString = queryString.slice(0, -1); // Remove the trailing comma
|
|
742
|
-
|
|
743
|
-
const { err, rows: r } = await db.query(queryString, [view.tableName]);
|
|
744
|
-
if (err) {
|
|
745
|
-
console.error(err);
|
|
746
|
-
return res.status(500).send({
|
|
747
|
-
err,
|
|
748
|
-
message: "Une erreur est survenue.",
|
|
749
|
-
});
|
|
750
|
-
}
|
|
751
|
-
res.send({
|
|
752
|
-
affectedRows: r.affectedRows,
|
|
753
|
-
});
|
|
754
|
-
} catch (err) {
|
|
755
|
-
console.error(err);
|
|
756
|
-
res.status(500).send({
|
|
757
|
-
err,
|
|
758
|
-
message: "Une erreur est survenue.",
|
|
759
|
-
});
|
|
760
|
-
}
|
|
761
|
-
});
|
|
762
|
-
});
|
|
763
|
-
|
|
764
|
-
app.post("/subrecordsImport", async function (req, res) {
|
|
765
|
-
const form = new multiparty.Form();
|
|
766
|
-
form.parse(req, async function (err, fields, files) {
|
|
767
|
-
if (err) {
|
|
768
|
-
return res.status(500).send(err);
|
|
769
|
-
}
|
|
770
|
-
// Get viewIndex and panelIndex in fields
|
|
771
|
-
const view = req.config.views[fields.viewIndex[0]];
|
|
772
|
-
const panel = view
|
|
773
|
-
? view.recordViewPanels[fields.panelIndex[0]]
|
|
774
|
-
: undefined;
|
|
775
|
-
// SECURITY: same write-guard as /recordsImport, plus the panel's own
|
|
776
|
-
// roles when it declares any.
|
|
777
|
-
if (
|
|
778
|
-
!view ||
|
|
779
|
-
!canWriteView(view, roleOf(req)) ||
|
|
780
|
-
!panel ||
|
|
781
|
-
!canWriteView(panel, roleOf(req))
|
|
782
|
-
) {
|
|
783
|
-
return res.sendStatus(403);
|
|
784
|
-
}
|
|
785
|
-
if (typeof panel.parseImportFile !== "function") {
|
|
786
|
-
return res
|
|
787
|
-
.status(500)
|
|
788
|
-
.send(
|
|
789
|
-
'"parseImportFile" doit être défini dans la configuration du panneau.',
|
|
790
|
-
);
|
|
791
|
-
}
|
|
792
|
-
if (!Boolean(panel.createConfig)) {
|
|
793
|
-
return res
|
|
794
|
-
.status(500)
|
|
795
|
-
.send(
|
|
796
|
-
'"createConfig" doit être défini dans la configuration du panneau.',
|
|
797
|
-
);
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
const records = await panel.parseImportFile(
|
|
801
|
-
files.data[0],
|
|
802
|
-
fields.recordId[0],
|
|
803
|
-
);
|
|
804
|
-
const columns = [];
|
|
805
|
-
if (records.length === 0) {
|
|
806
|
-
return res.send({
|
|
807
|
-
affectedRows: 0,
|
|
808
|
-
});
|
|
809
|
-
}
|
|
810
|
-
|
|
811
|
-
try {
|
|
812
|
-
for (let record of records) {
|
|
813
|
-
for (let key in record) {
|
|
814
|
-
if (!columns.includes(key)) {
|
|
815
|
-
columns.push(key);
|
|
816
|
-
}
|
|
817
|
-
}
|
|
818
|
-
}
|
|
166
|
+
app.post("/recordsExport", recordsExportRoute(getRecords));
|
|
819
167
|
|
|
820
|
-
|
|
821
|
-
"INSERT IGNORE INTO ?? (" +
|
|
822
|
-
quoteIdentifier(panel.createConfig.recordIdKey, db.dbType) +
|
|
823
|
-
", " +
|
|
824
|
-
columns.map((x) => quoteIdentifier(x, db.dbType)).join(", ") +
|
|
825
|
-
") VALUES";
|
|
826
|
-
for (let record of records) {
|
|
827
|
-
queryString +=
|
|
828
|
-
" (" +
|
|
829
|
-
fields.recordId[0] +
|
|
830
|
-
", " +
|
|
831
|
-
columns
|
|
832
|
-
.map((x) => formatValueForInsertion(record[x], db.dbType))
|
|
833
|
-
.join(", ") +
|
|
834
|
-
"),";
|
|
835
|
-
}
|
|
836
|
-
queryString = queryString.slice(0, -1) + " ON DUPLICATE KEY UPDATE";
|
|
837
|
-
for (let column of columns) {
|
|
838
|
-
queryString += ` ${quoteIdentifier(column, db.dbType)} = VALUES(${quoteIdentifier(column, db.dbType)}),`;
|
|
839
|
-
}
|
|
840
|
-
const { err, rows: r } = await db.query(queryString.slice(0, -1), [
|
|
841
|
-
panel.tableName,
|
|
842
|
-
]);
|
|
843
|
-
if (err) {
|
|
844
|
-
console.error(err);
|
|
845
|
-
return res.status(500).send({
|
|
846
|
-
err,
|
|
847
|
-
message: "Une erreur est survenue.",
|
|
848
|
-
});
|
|
849
|
-
}
|
|
850
|
-
res.send({
|
|
851
|
-
affectedRows: r.affectedRows,
|
|
852
|
-
});
|
|
853
|
-
} catch (err) {
|
|
854
|
-
console.error(err);
|
|
855
|
-
res.status(500).send({
|
|
856
|
-
err,
|
|
857
|
-
message: "Une erreur est survenue.",
|
|
858
|
-
});
|
|
859
|
-
}
|
|
860
|
-
});
|
|
861
|
-
});
|
|
168
|
+
app.post("/recordsImport", recordsImportRoute(db));
|
|
862
169
|
|
|
863
|
-
app.
|
|
864
|
-
const view = req.config.views[req.query.viewIndex as string];
|
|
865
|
-
const { err, rows } = await db.query(
|
|
866
|
-
"SELECT " +
|
|
867
|
-
(view.selectExpression || "*") +
|
|
868
|
-
" FROM ?? " +
|
|
869
|
-
(view.join || "") +
|
|
870
|
-
" WHERE ?? = ?",
|
|
871
|
-
[
|
|
872
|
-
view.tableName,
|
|
873
|
-
view.tableName + "." + view.primaryId,
|
|
874
|
-
req.query.recordId,
|
|
875
|
-
],
|
|
876
|
-
);
|
|
877
|
-
if (err) {
|
|
878
|
-
return res.status(500).send(err);
|
|
879
|
-
}
|
|
880
|
-
if (rows.length > 0) {
|
|
881
|
-
const record = rows[0];
|
|
882
|
-
for (const column of view.columns) {
|
|
883
|
-
if (column.formatValue) {
|
|
884
|
-
record[`__formatValue-${column.__id__}__`] =
|
|
885
|
-
await column.formatValue(record);
|
|
886
|
-
}
|
|
887
|
-
}
|
|
888
|
-
res.send(stripHiddenColumns(view, record));
|
|
889
|
-
} else {
|
|
890
|
-
res.sendStatus(404);
|
|
891
|
-
}
|
|
892
|
-
});
|
|
170
|
+
app.post("/subrecordsImport", subrecordsImportRoute(db));
|
|
893
171
|
|
|
894
|
-
app.
|
|
895
|
-
const view = req.config.views[req.body.viewIndex];
|
|
896
|
-
if (!areSafeColumnNames(req.body.data)) {
|
|
897
|
-
return res.status(400).send({ error: "Invalid column name" });
|
|
898
|
-
}
|
|
899
|
-
if (touchesHiddenColumn(view, req.body.data)) {
|
|
900
|
-
return res.sendStatus(403);
|
|
901
|
-
}
|
|
902
|
-
for (let key in req.body.data) {
|
|
903
|
-
if (
|
|
904
|
-
req.body.data[key] == "NULL" ||
|
|
905
|
-
req.body.data[key] === "" ||
|
|
906
|
-
req.body.data[key] === "Invalid date"
|
|
907
|
-
) {
|
|
908
|
-
req.body.data[key] = null;
|
|
909
|
-
}
|
|
910
|
-
}
|
|
911
|
-
if (Object.keys(req.body.data).length === 0) {
|
|
912
|
-
return res.sendStatus(200);
|
|
913
|
-
}
|
|
914
|
-
const queryExpression = Object.keys(req.body.data)
|
|
915
|
-
.map((x) => `${quoteIdentifier(x, db.dbType)} = ?`)
|
|
916
|
-
.join(", ");
|
|
917
|
-
const queryParameters = Object.keys(req.body.data).map(
|
|
918
|
-
(x) => req.body.data[x],
|
|
919
|
-
);
|
|
920
|
-
const { err } = await db.query(
|
|
921
|
-
`UPDATE ?? SET ${queryExpression} WHERE ?? = ?`,
|
|
922
|
-
[
|
|
923
|
-
view.tableName,
|
|
924
|
-
...queryParameters,
|
|
925
|
-
view.primaryId,
|
|
926
|
-
req.body.primaryValue,
|
|
927
|
-
],
|
|
928
|
-
);
|
|
929
|
-
if (err) {
|
|
930
|
-
return res.status(500).send(err);
|
|
931
|
-
}
|
|
932
|
-
if (view.editCallback) {
|
|
933
|
-
try {
|
|
934
|
-
await axios.post(view.editCallback, {
|
|
935
|
-
id: req.body.primaryValue,
|
|
936
|
-
data: req.body.data,
|
|
937
|
-
previousData: req.body.previousData,
|
|
938
|
-
});
|
|
939
|
-
} catch (e) {}
|
|
940
|
-
}
|
|
941
|
-
if (view.editCallbackFunction) {
|
|
942
|
-
await view.editCallbackFunction({
|
|
943
|
-
id: req.body.primaryValue,
|
|
944
|
-
data: req.body.data,
|
|
945
|
-
});
|
|
946
|
-
}
|
|
947
|
-
res.sendStatus(200);
|
|
948
|
-
});
|
|
172
|
+
app.get("/record", recordRoute(db));
|
|
949
173
|
|
|
950
|
-
app.post("/
|
|
951
|
-
const view = req.config.views[req.body.viewIndex];
|
|
952
|
-
const action = view.customActions[req.body.actionIndex];
|
|
953
|
-
for (const recordId of req.body.selectedRecordIds) {
|
|
954
|
-
try {
|
|
955
|
-
const { rows } = await db.query(
|
|
956
|
-
"SELECT " +
|
|
957
|
-
(view.selectExpression || "*") +
|
|
958
|
-
" FROM ?? " +
|
|
959
|
-
(view.join || "") +
|
|
960
|
-
" WHERE ?? = ?",
|
|
961
|
-
[view.tableName, view.tableName + "." + view.primaryId, recordId],
|
|
962
|
-
);
|
|
963
|
-
const record = rows[0];
|
|
964
|
-
const actionResult = await action.action(
|
|
965
|
-
record,
|
|
966
|
-
req.session.user.userId,
|
|
967
|
-
);
|
|
968
|
-
if (actionResult) {
|
|
969
|
-
return res.send({
|
|
970
|
-
success: true,
|
|
971
|
-
message: actionResult,
|
|
972
|
-
});
|
|
973
|
-
}
|
|
974
|
-
} catch (err) {
|
|
975
|
-
console.error(err);
|
|
976
|
-
return res.send({
|
|
977
|
-
success: false,
|
|
978
|
-
error: err,
|
|
979
|
-
});
|
|
980
|
-
}
|
|
981
|
-
}
|
|
982
|
-
res.send({
|
|
983
|
-
success: true,
|
|
984
|
-
message: action.successMessage,
|
|
985
|
-
});
|
|
986
|
-
});
|
|
174
|
+
app.post("/updateRecord", updateRecordRoute(db));
|
|
987
175
|
|
|
988
|
-
app.post("/
|
|
989
|
-
const view = req.config.views[req.body.viewIndex];
|
|
990
|
-
const { rows, err: err2 } = await db.query(
|
|
991
|
-
"SELECT " +
|
|
992
|
-
(view.selectExpression || "*") +
|
|
993
|
-
" FROM ?? " +
|
|
994
|
-
(view.join || "") +
|
|
995
|
-
" WHERE ?? = ?",
|
|
996
|
-
[
|
|
997
|
-
view.tableName,
|
|
998
|
-
view.tableName + "." + view.primaryId,
|
|
999
|
-
req.body.primaryValue,
|
|
1000
|
-
],
|
|
1001
|
-
);
|
|
1002
|
-
if (err2) {
|
|
1003
|
-
return res.status(500).send(err2);
|
|
1004
|
-
}
|
|
1005
|
-
const deletedRecord = rows[0];
|
|
1006
|
-
if (!deletedRecord) {
|
|
1007
|
-
return res.status(404).send("Record not found");
|
|
1008
|
-
}
|
|
1009
|
-
const { err } = await db.query("DELETE FROM ?? WHERE ?? = ?", [
|
|
1010
|
-
view.tableName,
|
|
1011
|
-
view.primaryId,
|
|
1012
|
-
req.body.primaryValue,
|
|
1013
|
-
]);
|
|
1014
|
-
if (err) {
|
|
1015
|
-
return res.status(500).send(err);
|
|
1016
|
-
}
|
|
1017
|
-
if (view.deleteCallback) {
|
|
1018
|
-
try {
|
|
1019
|
-
await axios.post(view.deleteCallback, {
|
|
1020
|
-
id: req.body.primaryValue,
|
|
1021
|
-
});
|
|
1022
|
-
} catch (e) {}
|
|
1023
|
-
}
|
|
1024
|
-
if (view.deleteCallbackFunction) {
|
|
1025
|
-
await view.deleteCallbackFunction({
|
|
1026
|
-
id: req.body.primaryValue,
|
|
1027
|
-
deletedRecord,
|
|
1028
|
-
});
|
|
1029
|
-
}
|
|
1030
|
-
res.sendStatus(200);
|
|
1031
|
-
});
|
|
176
|
+
app.post("/customAction", customActionRoute(db));
|
|
1032
177
|
|
|
1033
|
-
app.post("/
|
|
1034
|
-
const view = req.config.views[req.body.viewIndex];
|
|
1035
|
-
const panel = view.recordViewPanels.find(
|
|
1036
|
-
(x) => x.__id__ === req.body.panelId,
|
|
1037
|
-
);
|
|
1038
|
-
const { rows, err: err2 } = await db.query(
|
|
1039
|
-
"SELECT * FROM ?? WHERE ?? = ?",
|
|
1040
|
-
[panel.tableName, panel.primaryId, req.body.primaryValue],
|
|
1041
|
-
);
|
|
1042
|
-
if (err2) {
|
|
1043
|
-
return res.status(500).send(err2);
|
|
1044
|
-
}
|
|
1045
|
-
const deletedRecord = rows[0];
|
|
1046
|
-
const { err } = await db.query("DELETE FROM ?? WHERE ?? = ?", [
|
|
1047
|
-
panel.tableName,
|
|
1048
|
-
panel.primaryId,
|
|
1049
|
-
req.body.primaryValue,
|
|
1050
|
-
]);
|
|
1051
|
-
if (err) {
|
|
1052
|
-
return res.status(500).send(err);
|
|
1053
|
-
}
|
|
1054
|
-
if (panel.deleteCallback) {
|
|
1055
|
-
try {
|
|
1056
|
-
await axios.post(panel.deleteCallback, {
|
|
1057
|
-
id: req.body.primaryValue,
|
|
1058
|
-
});
|
|
1059
|
-
} catch (e) {}
|
|
1060
|
-
}
|
|
1061
|
-
if (panel.deleteCallbackFunction) {
|
|
1062
|
-
await panel.deleteCallbackFunction({
|
|
1063
|
-
id: req.body.primaryValue,
|
|
1064
|
-
deletedRecord,
|
|
1065
|
-
});
|
|
1066
|
-
}
|
|
1067
|
-
res.sendStatus(200);
|
|
1068
|
-
});
|
|
178
|
+
app.post("/deleteRecord", deleteRecordRoute(db));
|
|
1069
179
|
|
|
1070
|
-
app.post("/
|
|
1071
|
-
const view = req.config.views[req.body.viewIndex];
|
|
1072
|
-
// SECURITY: bind the primary values as parameters instead of interpolating
|
|
1073
|
-
// them into the IN() list (quote-bearing values could break out).
|
|
1074
|
-
const placeholders = req.body.primaryValues.map(() => "?").join(", ");
|
|
1075
|
-
const { err } = await db.query(
|
|
1076
|
-
`DELETE FROM ?? WHERE ?? IN(${placeholders})`,
|
|
1077
|
-
[view.tableName, view.primaryId, ...req.body.primaryValues],
|
|
1078
|
-
);
|
|
1079
|
-
if (err) {
|
|
1080
|
-
return res.status(500).send(err);
|
|
1081
|
-
}
|
|
1082
|
-
res.sendStatus(200);
|
|
1083
|
-
});
|
|
180
|
+
app.post("/deleteRecordFromPanel", deleteRecordFromPanelRoute(db));
|
|
1084
181
|
|
|
1085
|
-
app.
|
|
1086
|
-
const notificationBadges = {};
|
|
182
|
+
app.post("/deleteRecords", deleteRecordsRoute(db));
|
|
1087
183
|
|
|
1088
|
-
|
|
1089
|
-
if (obj.notificationBadge) {
|
|
1090
|
-
const { err, rows } = await db.query(obj.notificationBadge.query);
|
|
1091
|
-
if (err) {
|
|
1092
|
-
console.error(err);
|
|
1093
|
-
} else if (
|
|
1094
|
-
rows.length > 0 &&
|
|
1095
|
-
rows[0][obj.notificationBadge.attribute]
|
|
1096
|
-
) {
|
|
1097
|
-
const notificationBadge = rows[0][obj.notificationBadge.attribute];
|
|
1098
|
-
if (
|
|
1099
|
-
!obj.notificationBadge.displayCondition ||
|
|
1100
|
-
obj.notificationBadge.displayCondition(notificationBadge)
|
|
1101
|
-
) {
|
|
1102
|
-
notificationBadges[obj.__id__] = notificationBadge;
|
|
1103
|
-
}
|
|
1104
|
-
}
|
|
1105
|
-
}
|
|
1106
|
-
}
|
|
184
|
+
app.get("/notificationBadges", notificationBadgesRoute(db));
|
|
1107
185
|
|
|
1108
|
-
|
|
1109
|
-
await checkAndGetNotificationBadge(view);
|
|
1110
|
-
if (view.subviews) {
|
|
1111
|
-
for (const subview of view.subviews) {
|
|
1112
|
-
await checkAndGetNotificationBadge(subview);
|
|
1113
|
-
}
|
|
1114
|
-
}
|
|
1115
|
-
}
|
|
1116
|
-
res.send(notificationBadges);
|
|
1117
|
-
});
|
|
186
|
+
app.post("/duplicateRecord", duplicateRecordRoute(db));
|
|
1118
187
|
|
|
1119
|
-
app.post("/
|
|
1120
|
-
// SECURITY: bound the (client-supplied) target table to tables the role may write.
|
|
1121
|
-
const allowedTables = writableTables(req);
|
|
1122
|
-
const requestedTables = [
|
|
1123
|
-
req.body.table,
|
|
1124
|
-
...(req.body.duplicateConfig?.foreignTables || []).map((t) => t.table),
|
|
1125
|
-
];
|
|
1126
|
-
if (requestedTables.some((t) => !allowedTables.has(t))) {
|
|
1127
|
-
return res.sendStatus(403);
|
|
1128
|
-
}
|
|
1129
|
-
const primaryId = req.body.duplicateConfig.primaryId;
|
|
1130
|
-
const { err, rows } = await db.query("SELECT * FROM ?? WHERE ?? = ?", [
|
|
1131
|
-
req.body.table,
|
|
1132
|
-
primaryId,
|
|
1133
|
-
req.body.primaryValue,
|
|
1134
|
-
]);
|
|
1135
|
-
if (err) {
|
|
1136
|
-
return res.status(500).send(err);
|
|
1137
|
-
}
|
|
1138
|
-
const record = rows[0];
|
|
1139
|
-
const data = {};
|
|
1140
|
-
for (let field of req.body.duplicateConfig.fields) {
|
|
1141
|
-
data[field] = record[field];
|
|
1142
|
-
}
|
|
1143
|
-
const { err: err2, rows: r } = await db.query(
|
|
1144
|
-
"INSERT IGNORE INTO ?? SET ?",
|
|
1145
|
-
[req.body.table, data],
|
|
1146
|
-
);
|
|
1147
|
-
if (err2) {
|
|
1148
|
-
return res.status(500).send(err);
|
|
1149
|
-
}
|
|
1150
|
-
const newRecordId = r.insertId;
|
|
1151
|
-
for (let foreignTable of req.body.duplicateConfig.foreignTables) {
|
|
1152
|
-
const { err, rows } = await db.query("SELECT * FROM ?? WHERE ?? = ?", [
|
|
1153
|
-
foreignTable.table,
|
|
1154
|
-
primaryId,
|
|
1155
|
-
req.body.primaryValue,
|
|
1156
|
-
]);
|
|
1157
|
-
if (err) console.log(err);
|
|
1158
|
-
for (let record of rows) {
|
|
1159
|
-
const data = {};
|
|
1160
|
-
data[primaryId] = newRecordId;
|
|
1161
|
-
for (let field of foreignTable.fields) {
|
|
1162
|
-
data[field] = record[field];
|
|
1163
|
-
}
|
|
1164
|
-
await db.query("INSERT IGNORE INTO ?? SET ?", [
|
|
1165
|
-
foreignTable.table,
|
|
1166
|
-
data,
|
|
1167
|
-
]);
|
|
1168
|
-
}
|
|
1169
|
-
}
|
|
1170
|
-
res.sendStatus(200);
|
|
1171
|
-
});
|
|
1172
|
-
|
|
1173
|
-
app.post("/createRecord", async function (req, res, next) {
|
|
1174
|
-
const view = req.config.views[req.body.viewIndex];
|
|
1175
|
-
// panelIndex arrives as a string from the front-end but a numeric caller
|
|
1176
|
-
// may send 0 — only null/undefined mean "no panel".
|
|
1177
|
-
const hasPanel =
|
|
1178
|
-
req.body.panelIndex !== undefined && req.body.panelIndex !== null;
|
|
1179
|
-
const panel = hasPanel ? view.recordViewPanels[req.body.panelIndex] : null;
|
|
1180
|
-
if (!hasPanel && touchesHiddenColumn(view, req.body.data)) {
|
|
1181
|
-
return res.sendStatus(403);
|
|
1182
|
-
}
|
|
1183
|
-
try {
|
|
1184
|
-
for (let key in req.body.data) {
|
|
1185
|
-
if (
|
|
1186
|
-
req.body.data[key] == "NULL" ||
|
|
1187
|
-
req.body.data[key] === "Invalid date"
|
|
1188
|
-
) {
|
|
1189
|
-
req.body.data[key] = null;
|
|
1190
|
-
}
|
|
1191
|
-
}
|
|
1192
|
-
if (panel) {
|
|
1193
|
-
req.body.data[panel.createConfig.recordIdKey] = req.body.recordId;
|
|
1194
|
-
}
|
|
1195
|
-
const primaryColumn = (view || panel)?.columns?.find(
|
|
1196
|
-
(x) => x.name === (view || panel).primaryId,
|
|
1197
|
-
);
|
|
1198
|
-
if (primaryColumn?.dataType == "uuid") {
|
|
1199
|
-
req.body.data[primaryColumn.name] = uuidv4();
|
|
1200
|
-
}
|
|
1201
|
-
const createPayload = JSON.parse(JSON.stringify(req.body.data));
|
|
1202
|
-
const columns = panel ? panel.createConfig.columns : view.columns;
|
|
1203
|
-
for (let key in createPayload) {
|
|
1204
|
-
if (
|
|
1205
|
-
columns?.find((x) => x.name === key)?.includedInCreatePayload ===
|
|
1206
|
-
false
|
|
1207
|
-
) {
|
|
1208
|
-
delete createPayload[key];
|
|
1209
|
-
}
|
|
1210
|
-
}
|
|
1211
|
-
if (!areSafeColumnNames(createPayload)) {
|
|
1212
|
-
return res.status(400).send({ error: "Invalid column name" });
|
|
1213
|
-
}
|
|
1214
|
-
const queryExpression = Object.keys(createPayload)
|
|
1215
|
-
.map((x) => quoteIdentifier(x, db.dbType))
|
|
1216
|
-
.join(", ");
|
|
1217
|
-
const queryPlaceholders = Object.keys(createPayload)
|
|
1218
|
-
.map((x) => "?")
|
|
1219
|
-
.join(", ");
|
|
1220
|
-
const queryParameters = Object.keys(createPayload).map(
|
|
1221
|
-
(x) => createPayload[x],
|
|
1222
|
-
);
|
|
1223
|
-
const { err, rows: r } = await db.query(
|
|
1224
|
-
`INSERT INTO ?? (${queryExpression}) VALUES(${queryPlaceholders})`,
|
|
1225
|
-
[panel ? panel.tableName : view.tableName, ...queryParameters],
|
|
1226
|
-
);
|
|
1227
|
-
if (err) {
|
|
1228
|
-
console.error(err);
|
|
1229
|
-
return res.status(500).send({
|
|
1230
|
-
err,
|
|
1231
|
-
message:
|
|
1232
|
-
err.code === "ER_DUP_ENTRY"
|
|
1233
|
-
? "Cet élément existe déjà."
|
|
1234
|
-
: "Une erreur est survenue.",
|
|
1235
|
-
});
|
|
1236
|
-
}
|
|
1237
|
-
if (panel ? panel.createCallback : view.createCallback) {
|
|
1238
|
-
try {
|
|
1239
|
-
await axios.post(panel ? panel.createCallback : view.createCallback, {
|
|
1240
|
-
id: r.insertId,
|
|
1241
|
-
data: req.body.data,
|
|
1242
|
-
});
|
|
1243
|
-
} catch (e) {}
|
|
1244
|
-
}
|
|
1245
|
-
if (panel ? panel.createCallbackFunction : view.createCallbackFunction) {
|
|
1246
|
-
// The insert is already committed: a failing callback must not turn
|
|
1247
|
-
// into a 500 that invites a client retry (which would duplicate the
|
|
1248
|
-
// row). Log and answer 200.
|
|
1249
|
-
try {
|
|
1250
|
-
await (
|
|
1251
|
-
panel ? panel.createCallbackFunction : view.createCallbackFunction
|
|
1252
|
-
)({
|
|
1253
|
-
id: r.insertId,
|
|
1254
|
-
data: req.body.data,
|
|
1255
|
-
});
|
|
1256
|
-
} catch (callbackErr) {
|
|
1257
|
-
console.error(callbackErr);
|
|
1258
|
-
}
|
|
1259
|
-
}
|
|
1260
|
-
res.send({
|
|
1261
|
-
id: r.insertId,
|
|
1262
|
-
});
|
|
1263
|
-
} catch (err) {
|
|
1264
|
-
console.error(err);
|
|
1265
|
-
res.status(500).send({
|
|
1266
|
-
err,
|
|
1267
|
-
message: "Une erreur est survenue.",
|
|
1268
|
-
});
|
|
1269
|
-
}
|
|
1270
|
-
});
|
|
188
|
+
app.post("/createRecord", createRecordRoute(db));
|
|
1271
189
|
|
|
1272
190
|
// Attack-surface reduction: only expose /customQuery at all when the config
|
|
1273
191
|
// actually declares SQL templates (panels, autocomplete lists, …). If nothing
|
|
@@ -1278,421 +196,17 @@ export default (db, config) => {
|
|
|
1278
196
|
if (!CONFIG_USES_CUSTOM_QUERY) {
|
|
1279
197
|
app.get("/customQuery", (req, res) => res.sendStatus(404));
|
|
1280
198
|
} else {
|
|
1281
|
-
app.get("/customQuery",
|
|
1282
|
-
const template =
|
|
1283
|
-
typeof req.query.query === "string" ? req.query.query : "";
|
|
1284
|
-
// SECURITY: only run whitelisted, read-only templates, and bind the record id
|
|
1285
|
-
// as a parameter instead of substituting it into the SQL string.
|
|
1286
|
-
const allowed = collectAllowedQueries(req.config.views);
|
|
1287
|
-
if (!allowed.has(template.trim())) {
|
|
1288
|
-
return res.status(403).send({ error: "Query not allowed" });
|
|
1289
|
-
}
|
|
1290
|
-
if (!/^\s*\(?\s*(select|with)\b/i.test(template) || /;/.test(template)) {
|
|
1291
|
-
return res
|
|
1292
|
-
.status(400)
|
|
1293
|
-
.send({ error: "Only read-only queries are allowed" });
|
|
1294
|
-
}
|
|
1295
|
-
const placeholderCount = (template.match(/\?/g) || []).length;
|
|
1296
|
-
const params = new Array(placeholderCount).fill(req.query.primaryValue);
|
|
1297
|
-
const { err, rows } = await db.query(template, params);
|
|
1298
|
-
if (err) {
|
|
1299
|
-
return res.status(500).send(err);
|
|
1300
|
-
}
|
|
1301
|
-
if (req.query.viewIndex && req.query.panelIndex) {
|
|
1302
|
-
const panel =
|
|
1303
|
-
req.config.views[req.query.viewIndex as string].recordViewPanels[
|
|
1304
|
-
req.query.panelIndex as string
|
|
1305
|
-
];
|
|
1306
|
-
if (panel.formatValue) {
|
|
1307
|
-
for (const row of rows) {
|
|
1308
|
-
for (const key in panel.formatValue) {
|
|
1309
|
-
row[key] = await panel.formatValue[key](row);
|
|
1310
|
-
}
|
|
1311
|
-
}
|
|
1312
|
-
}
|
|
1313
|
-
}
|
|
1314
|
-
res.send(rows);
|
|
1315
|
-
});
|
|
1316
|
-
}
|
|
1317
|
-
|
|
1318
|
-
function uploadFile(column, fileParamName, req, res) {
|
|
1319
|
-
if (column.s3Config || column.gcpConfig) {
|
|
1320
|
-
const form = new multiparty.Form();
|
|
1321
|
-
form.parse(req, async function (err, fields, files) {
|
|
1322
|
-
if (err) {
|
|
1323
|
-
return res.status(500).send(err);
|
|
1324
|
-
}
|
|
1325
|
-
const originalFilename = files[fileParamName][0].originalFilename;
|
|
1326
|
-
const contentType =
|
|
1327
|
-
mime.getType(originalFilename) || "application/octet-stream";
|
|
1328
|
-
|
|
1329
|
-
const extension = originalFilename.match(/(?:\.([^.]+))?$/)[1];
|
|
1330
|
-
let aspectRatio = null;
|
|
1331
|
-
if (["png", "jpeg", "jpg", "gif"].includes(extension)) {
|
|
1332
|
-
try {
|
|
1333
|
-
const dimensions = await getImageDimensions(
|
|
1334
|
-
files[fileParamName][0].path,
|
|
1335
|
-
);
|
|
1336
|
-
if (dimensions) {
|
|
1337
|
-
aspectRatio = dimensions.height / dimensions.width;
|
|
1338
|
-
}
|
|
1339
|
-
} catch {
|
|
1340
|
-
// malformed image data: leave aspectRatio null rather than crash
|
|
1341
|
-
}
|
|
1342
|
-
}
|
|
1343
|
-
const originalFilenameWithoutExtension = originalFilename.replace(
|
|
1344
|
-
"." + extension,
|
|
1345
|
-
"",
|
|
1346
|
-
);
|
|
1347
|
-
|
|
1348
|
-
if (column.gcpConfig) {
|
|
1349
|
-
const gcpConfig = column.gcpConfig;
|
|
1350
|
-
const path =
|
|
1351
|
-
(gcpConfig.subfolder ? gcpConfig.subfolder + "/" : "") +
|
|
1352
|
-
originalFilenameWithoutExtension +
|
|
1353
|
-
"-" +
|
|
1354
|
-
Math.random().toString().substr(2) +
|
|
1355
|
-
"." +
|
|
1356
|
-
extension;
|
|
1357
|
-
const storage = new Storage();
|
|
1358
|
-
|
|
1359
|
-
const bucketName = gcpConfig.bucket;
|
|
1360
|
-
storage
|
|
1361
|
-
.bucket(bucketName)
|
|
1362
|
-
.upload(files[fileParamName][0].path, {
|
|
1363
|
-
destination: path,
|
|
1364
|
-
})
|
|
1365
|
-
.then(() => {
|
|
1366
|
-
res.status(200).send({
|
|
1367
|
-
path: gcpConfig.pathTransform
|
|
1368
|
-
? gcpConfig.pathTransform(path)
|
|
1369
|
-
: path,
|
|
1370
|
-
aspectRatio,
|
|
1371
|
-
originalFilename,
|
|
1372
|
-
});
|
|
1373
|
-
})
|
|
1374
|
-
.catch((e) => {
|
|
1375
|
-
res.status(500).send(e);
|
|
1376
|
-
});
|
|
1377
|
-
} else {
|
|
1378
|
-
const s3Config = column.s3Config;
|
|
1379
|
-
const path =
|
|
1380
|
-
(s3Config.subfolder ? s3Config.subfolder + "/" : "") +
|
|
1381
|
-
originalFilenameWithoutExtension +
|
|
1382
|
-
"-" +
|
|
1383
|
-
Math.random().toString().substr(2) +
|
|
1384
|
-
"." +
|
|
1385
|
-
extension;
|
|
1386
|
-
const rawHost = String(s3Config.host || "");
|
|
1387
|
-
const endpoint = /^https?:\/\//i.test(rawHost)
|
|
1388
|
-
? rawHost
|
|
1389
|
-
: `https://${rawHost}`;
|
|
1390
|
-
const s3 = new S3Client({
|
|
1391
|
-
endpoint,
|
|
1392
|
-
region: s3Config.region,
|
|
1393
|
-
...(s3Config.accessKeyId && {
|
|
1394
|
-
credentials: {
|
|
1395
|
-
accessKeyId: s3Config.accessKeyId,
|
|
1396
|
-
secretAccessKey: s3Config.secretAccessKey ?? "",
|
|
1397
|
-
},
|
|
1398
|
-
}),
|
|
1399
|
-
forcePathStyle: false,
|
|
1400
|
-
});
|
|
1401
|
-
if (s3Config.signedUrlNeeded) {
|
|
1402
|
-
getSignedUrl(
|
|
1403
|
-
s3,
|
|
1404
|
-
new PutObjectCommand({
|
|
1405
|
-
Bucket: s3Config.bucket,
|
|
1406
|
-
Key: path,
|
|
1407
|
-
ContentType: contentType,
|
|
1408
|
-
}),
|
|
1409
|
-
{ expiresIn: 150 },
|
|
1410
|
-
)
|
|
1411
|
-
.then((signedUrl) =>
|
|
1412
|
-
axios.put(
|
|
1413
|
-
signedUrl,
|
|
1414
|
-
fs.readFileSync(files[fileParamName][0].path),
|
|
1415
|
-
{
|
|
1416
|
-
maxContentLength: 100000000000,
|
|
1417
|
-
maxBodyLength: 100000000000,
|
|
1418
|
-
headers: {
|
|
1419
|
-
"Content-Type": contentType,
|
|
1420
|
-
},
|
|
1421
|
-
},
|
|
1422
|
-
),
|
|
1423
|
-
)
|
|
1424
|
-
.then(() => {
|
|
1425
|
-
res.status(200).send({
|
|
1426
|
-
path: s3Config.pathTransform
|
|
1427
|
-
? s3Config.pathTransform(path)
|
|
1428
|
-
: path,
|
|
1429
|
-
aspectRatio,
|
|
1430
|
-
originalFilename,
|
|
1431
|
-
});
|
|
1432
|
-
})
|
|
1433
|
-
.catch((e) => {
|
|
1434
|
-
res.status(500).send(e);
|
|
1435
|
-
});
|
|
1436
|
-
} else {
|
|
1437
|
-
s3.send(
|
|
1438
|
-
new PutObjectCommand({
|
|
1439
|
-
Body: fs.readFileSync(files[fileParamName][0].path),
|
|
1440
|
-
Bucket: s3Config.bucket,
|
|
1441
|
-
ContentType: contentType,
|
|
1442
|
-
ACL: "public-read",
|
|
1443
|
-
Key: path,
|
|
1444
|
-
}),
|
|
1445
|
-
)
|
|
1446
|
-
.then(() => {
|
|
1447
|
-
res.send({
|
|
1448
|
-
url: `https://${s3Config.host}/${s3Config.bucket}/${path}`,
|
|
1449
|
-
path: s3Config.pathTransform
|
|
1450
|
-
? s3Config.pathTransform(path)
|
|
1451
|
-
: path,
|
|
1452
|
-
aspectRatio,
|
|
1453
|
-
originalFilename,
|
|
1454
|
-
});
|
|
1455
|
-
})
|
|
1456
|
-
.catch((err) => res.status(500).send(err));
|
|
1457
|
-
}
|
|
1458
|
-
}
|
|
1459
|
-
});
|
|
1460
|
-
} else if (column.sftpConfig) {
|
|
1461
|
-
if (!column.publicPath) {
|
|
1462
|
-
return res
|
|
1463
|
-
.status(500)
|
|
1464
|
-
.send("You need to pass publicPath parameter when using sftpConfig");
|
|
1465
|
-
}
|
|
1466
|
-
const form = new multiparty.Form();
|
|
1467
|
-
form.parse(req, function (err, fields, files) {
|
|
1468
|
-
if (err) {
|
|
1469
|
-
return res.status(500).send(err);
|
|
1470
|
-
}
|
|
1471
|
-
const pathSplitted = files[fileParamName][0].path.split(/[\/\\]+/);
|
|
1472
|
-
const filename = pathSplitted[pathSplitted.length - 1];
|
|
1473
|
-
(async () => {
|
|
1474
|
-
const sftp = new SftpClient();
|
|
1475
|
-
await sftp.connect({
|
|
1476
|
-
host: column.sftpConfig.host,
|
|
1477
|
-
port: 22,
|
|
1478
|
-
username: column.sftpConfig.user,
|
|
1479
|
-
password: column.sftpConfig.password,
|
|
1480
|
-
});
|
|
1481
|
-
await sftp.fastPut(
|
|
1482
|
-
files[fileParamName][0].path,
|
|
1483
|
-
column.publicPath +
|
|
1484
|
-
(column.subdirectory ? "/" + column.subdirectory : "") +
|
|
1485
|
-
"/" +
|
|
1486
|
-
filename,
|
|
1487
|
-
);
|
|
1488
|
-
await sftp.end();
|
|
1489
|
-
res.send({
|
|
1490
|
-
path: filename,
|
|
1491
|
-
originalFilename: files[fileParamName][0].originalFilename,
|
|
1492
|
-
});
|
|
1493
|
-
fs.unlinkSync(files[fileParamName][0].path);
|
|
1494
|
-
})();
|
|
1495
|
-
});
|
|
1496
|
-
} else {
|
|
1497
|
-
const form = new multiparty.Form({
|
|
1498
|
-
uploadDir:
|
|
1499
|
-
(column.publicPath as string) + (column.subdirectory as string),
|
|
1500
|
-
});
|
|
1501
|
-
form.parse(req, function (err, fields, files) {
|
|
1502
|
-
if (err) {
|
|
1503
|
-
return res.status(500).send(err);
|
|
1504
|
-
}
|
|
1505
|
-
res.send({
|
|
1506
|
-
path: files[fileParamName][0].path.replace(column.publicPath, ""),
|
|
1507
|
-
originalFilename: files[fileParamName][0].originalFilename,
|
|
1508
|
-
});
|
|
1509
|
-
});
|
|
1510
|
-
}
|
|
199
|
+
app.get("/customQuery", customQueryRoute(db));
|
|
1511
200
|
}
|
|
1512
201
|
|
|
1513
202
|
app.post(
|
|
1514
203
|
"/uploadFileFromBetterRichTextInput",
|
|
1515
|
-
|
|
1516
|
-
// Resolve settings on the server so function-valued pathTransform survives.
|
|
1517
|
-
// Validate the target before parsing multipart data or contacting storage.
|
|
1518
|
-
const role = roleOf(req);
|
|
1519
|
-
const { viewIndex, columnId } = req.query;
|
|
1520
|
-
if (
|
|
1521
|
-
typeof viewIndex !== "string" ||
|
|
1522
|
-
!/^\d+$/.test(viewIndex) ||
|
|
1523
|
-
typeof columnId !== "string"
|
|
1524
|
-
) {
|
|
1525
|
-
return res.sendStatus(403);
|
|
1526
|
-
}
|
|
1527
|
-
const view = req.config.views[viewIndex];
|
|
1528
|
-
if (!view || !canWriteView(view, role)) {
|
|
1529
|
-
return res.sendStatus(403);
|
|
1530
|
-
}
|
|
1531
|
-
const column = [
|
|
1532
|
-
...(view.columns || []),
|
|
1533
|
-
...(view.recordViewPanels?.flatMap(
|
|
1534
|
-
(panel) => panel.createConfig?.columns || [],
|
|
1535
|
-
) || []),
|
|
1536
|
-
].find((column) => column.__id__ === columnId);
|
|
1537
|
-
if (!column || (column.roles && !column.roles.includes(role))) {
|
|
1538
|
-
return res.sendStatus(403);
|
|
1539
|
-
}
|
|
1540
|
-
if (column.dataType !== "betterRichText" || !column.fileUploadProperties) {
|
|
1541
|
-
return res.status(400).send({ error: "Invalid rich-text upload column" });
|
|
1542
|
-
}
|
|
1543
|
-
uploadFile(column.fileUploadProperties, "upload", req, res);
|
|
1544
|
-
},
|
|
204
|
+
uploadFileFromBetterRichTextInputRoute(),
|
|
1545
205
|
);
|
|
1546
206
|
|
|
1547
|
-
app.post("/uploadFile",
|
|
1548
|
-
const view = req.config.views[req.query.viewIndex as string];
|
|
1549
|
-
const column = [
|
|
1550
|
-
...view.columns,
|
|
1551
|
-
...(view.recordViewPanels
|
|
1552
|
-
?.map((x) => x?.createConfig?.columns)
|
|
1553
|
-
?.filter((x) => x)
|
|
1554
|
-
?.flat() || []),
|
|
1555
|
-
].find((x) => x.__id__ == req.query.columnId);
|
|
1556
|
-
uploadFile(column, "file", req, res);
|
|
1557
|
-
});
|
|
207
|
+
app.post("/uploadFile", uploadFileRoute());
|
|
1558
208
|
|
|
1559
|
-
app.get("/searchCompanies",
|
|
1560
|
-
const processedQuery = (req.query.query as any).replaceAll(" ", "-");
|
|
1561
|
-
try {
|
|
1562
|
-
const response = await axios.get(
|
|
1563
|
-
`https://public.opendatasoft.com/api/explore/v2.1/catalog/datasets/economicref-france-sirene-v3/records`,
|
|
1564
|
-
{
|
|
1565
|
-
params: {
|
|
1566
|
-
where: `denominationunitelegale LIKE '${processedQuery}*' OR siret LIKE '${processedQuery}*'`,
|
|
1567
|
-
limit: 100,
|
|
1568
|
-
},
|
|
1569
|
-
},
|
|
1570
|
-
);
|
|
1571
|
-
const results = response.data.results
|
|
1572
|
-
.filter(
|
|
1573
|
-
(etablissement) =>
|
|
1574
|
-
etablissement.etatadministratifetablissement === "Actif",
|
|
1575
|
-
)
|
|
1576
|
-
.map((etablissement) => ({
|
|
1577
|
-
id: Math.random().toString().slice(2),
|
|
1578
|
-
companyName: etablissement.denominationunitelegale,
|
|
1579
|
-
siret: etablissement.siret,
|
|
1580
|
-
siren: etablissement.siren,
|
|
1581
|
-
address: `${etablissement.numerovoieetablissement || ""} ${etablissement.typevoieetablissement || ""} ${etablissement.libellevoieetablissement || ""}`,
|
|
1582
|
-
city: etablissement.libellecommuneetablissement,
|
|
1583
|
-
zipCode: etablissement.codepostaletablissement,
|
|
1584
|
-
activityNumber: etablissement.activiteprincipaleunitelegale,
|
|
1585
|
-
}))
|
|
1586
|
-
.slice(0, 30);
|
|
1587
|
-
res.send(results);
|
|
1588
|
-
} catch (e) {
|
|
1589
|
-
res.send([]);
|
|
1590
|
-
}
|
|
1591
|
-
});
|
|
209
|
+
app.get("/searchCompanies", searchCompaniesRoute());
|
|
1592
210
|
|
|
1593
211
|
return app;
|
|
1594
212
|
};
|
|
1595
|
-
|
|
1596
|
-
// Collect every SQL template declared anywhere in the (role-filtered) config:
|
|
1597
|
-
// panel `query`, `autocompleteListQuery`, etc. customQuery may only run a
|
|
1598
|
-
// template that already exists in the config the caller is allowed to see —
|
|
1599
|
-
// so it can never be turned into an arbitrary read/write primitive.
|
|
1600
|
-
export function collectAllowedQueries(node, acc = new Set<string>()) {
|
|
1601
|
-
if (!node || typeof node !== "object") return acc;
|
|
1602
|
-
if (Array.isArray(node)) {
|
|
1603
|
-
for (const item of node) collectAllowedQueries(item, acc);
|
|
1604
|
-
return acc;
|
|
1605
|
-
}
|
|
1606
|
-
for (const key of Object.keys(node)) {
|
|
1607
|
-
const value = node[key];
|
|
1608
|
-
if (typeof value === "string" && (key === "query" || /Query$/.test(key))) {
|
|
1609
|
-
acc.add(value.trim());
|
|
1610
|
-
} else if (value && typeof value === "object") {
|
|
1611
|
-
collectAllowedQueries(value, acc);
|
|
1612
|
-
}
|
|
1613
|
-
}
|
|
1614
|
-
return acc;
|
|
1615
|
-
}
|
|
1616
|
-
|
|
1617
|
-
export async function enrichConfig(config, db) {
|
|
1618
|
-
const { rows: info, err } = await db.query(
|
|
1619
|
-
"SELECT * FROM information_schema.columns WHERE table_schema = ?",
|
|
1620
|
-
[config.mysql?.database || config.postgresql?.database],
|
|
1621
|
-
);
|
|
1622
|
-
if (err) {
|
|
1623
|
-
console.error(err);
|
|
1624
|
-
return;
|
|
1625
|
-
}
|
|
1626
|
-
for (const view of config.views) {
|
|
1627
|
-
if (view.type == "table") {
|
|
1628
|
-
const tableName = view.tableName;
|
|
1629
|
-
for (const infoEl of info) {
|
|
1630
|
-
if (infoEl.TABLE_NAME == tableName && infoEl.COLUMN_KEY == "PRI") {
|
|
1631
|
-
view.primaryId = infoEl.COLUMN_NAME;
|
|
1632
|
-
}
|
|
1633
|
-
}
|
|
1634
|
-
for (const column of view.columns) {
|
|
1635
|
-
enrichColumn(column, tableName, info);
|
|
1636
|
-
}
|
|
1637
|
-
if (view.recordViewPanels) {
|
|
1638
|
-
for (const panel of view.recordViewPanels) {
|
|
1639
|
-
if (panel.createConfig) {
|
|
1640
|
-
for (const column of panel.createConfig.columns) {
|
|
1641
|
-
enrichColumn(column, tableName, info);
|
|
1642
|
-
}
|
|
1643
|
-
}
|
|
1644
|
-
if (!panel.__id__) {
|
|
1645
|
-
panel.__id__ = generateUniqueIdForObject(panel);
|
|
1646
|
-
}
|
|
1647
|
-
}
|
|
1648
|
-
}
|
|
1649
|
-
}
|
|
1650
|
-
if (view.subviews) {
|
|
1651
|
-
for (const subview of view.subviews) {
|
|
1652
|
-
if (!subview.__id__) {
|
|
1653
|
-
subview.__id__ = generateUniqueIdForObject(subview);
|
|
1654
|
-
}
|
|
1655
|
-
}
|
|
1656
|
-
}
|
|
1657
|
-
if (!view.__id__) {
|
|
1658
|
-
view.__id__ = generateUniqueIdForObject(view);
|
|
1659
|
-
}
|
|
1660
|
-
}
|
|
1661
|
-
config.enriched = true;
|
|
1662
|
-
}
|
|
1663
|
-
|
|
1664
|
-
export function enrichColumn(column, tableName, info) {
|
|
1665
|
-
if (!column.__id__) {
|
|
1666
|
-
column.__id__ = generateUniqueIdForObject(column);
|
|
1667
|
-
}
|
|
1668
|
-
const infoEl = info.find(
|
|
1669
|
-
(x) => x.TABLE_NAME == tableName && x.COLUMN_NAME == column.name,
|
|
1670
|
-
);
|
|
1671
|
-
if (
|
|
1672
|
-
infoEl?.IS_NULLABLE == "NO" &&
|
|
1673
|
-
infoEl?.DATA_TYPE != "text" &&
|
|
1674
|
-
column.required !== true &&
|
|
1675
|
-
column.required !== false
|
|
1676
|
-
) {
|
|
1677
|
-
column.required = true;
|
|
1678
|
-
}
|
|
1679
|
-
if (!column.dataType && infoEl) {
|
|
1680
|
-
column.dataType = infoEl.DATA_TYPE;
|
|
1681
|
-
if (column.dataType == "enum") {
|
|
1682
|
-
column.dataType = "select";
|
|
1683
|
-
column.options = JSON.parse(
|
|
1684
|
-
infoEl.COLUMN_TYPE.replace("enum(", "[")
|
|
1685
|
-
.replace(")", "]")
|
|
1686
|
-
.replace(/'/g, '"'),
|
|
1687
|
-
);
|
|
1688
|
-
}
|
|
1689
|
-
}
|
|
1690
|
-
if (column.dataType == "richText") {
|
|
1691
|
-
column.name = column.contentField;
|
|
1692
|
-
}
|
|
1693
|
-
if (column.dataType == "file" && (column.s3Config || column.gcpConfig)) {
|
|
1694
|
-
const host = column.s3Config?.host || column.gcpConfig?.host;
|
|
1695
|
-
const bucket = column.s3Config?.bucket || column.gcpConfig?.bucket;
|
|
1696
|
-
column.baseUrl = `https://${host}/${bucket}/`;
|
|
1697
|
-
}
|
|
1698
|
-
}
|