@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.
Files changed (225) hide show
  1. package/.agents/report/blockers.json +1 -0
  2. package/.agents/report/failures/2026-09-23-api-user-filter-contract.md +31 -0
  3. package/.agents/report/failures/2026-09-23-api-values-contract.md +25 -0
  4. package/.agents/report/failures/2026-10-01-cycle-02-create-callback-contract.md +17 -0
  5. package/.agents/report/failures/2026-10-01-cycle-06-delete-callback-id-contract.md +17 -0
  6. package/.agents/report/failures/2026-10-01-cycle-07-panel-delete-callback-id-contract.md +24 -0
  7. package/.agents/report/failures/2026-10-01-cycle-13-update-callback-id-contract.md +17 -0
  8. package/.agents/report/failures/2026-10-01-cycle-15-rich-text-upload-boundary-contract.md +24 -0
  9. package/.agents/report/failures/2026-10-03-cycle-06-record-import-parser-contract.md +11 -0
  10. package/.agents/report/failures/2026-10-03-cycle-07-panel-import-parser-contract.md +24 -0
  11. package/.agents/report/failures/2026-10-07-resume-cycle-02-api-assembly-contract.md +46 -0
  12. package/.agents/report/port/2026-09-23-api-config-enrichment.md +13 -0
  13. package/.agents/report/port/2026-09-23-api-file-upload.md +11 -0
  14. package/.agents/report/port/2026-09-23-api-query-authorization.md +14 -0
  15. package/.agents/report/port/2026-09-23-api-request-configuration.md +11 -0
  16. package/.agents/report/port/2026-09-23-database-manager.md +13 -0
  17. package/.agents/report/port/2026-10-01-api-adjacent-record-id.md +9 -0
  18. package/.agents/report/port/2026-10-01-cycle-01-api-config-route.md +9 -0
  19. package/.agents/report/port/2026-10-01-cycle-03-custom-action.md +9 -0
  20. package/.agents/report/port/2026-10-01-cycle-04-custom-component.md +7 -0
  21. package/.agents/report/port/2026-10-01-cycle-05-custom-query.md +7 -0
  22. package/.agents/report/port/2026-10-01-cycle-08-bulk-delete.md +9 -0
  23. package/.agents/report/port/2026-10-01-cycle-09-duplicate-record.md +9 -0
  24. package/.agents/report/port/2026-10-01-cycle-10-notification-badges.md +9 -0
  25. package/.agents/report/port/2026-10-01-cycle-11-record.md +7 -0
  26. package/.agents/report/port/2026-10-01-cycle-12-search-companies.md +11 -0
  27. package/.agents/report/port/2026-10-01-cycle-14-upload-file.md +7 -0
  28. package/.agents/report/port/2026-10-03-cycle-01-create-record.md +11 -0
  29. package/.agents/report/port/2026-10-03-cycle-02-delete-record.md +11 -0
  30. package/.agents/report/port/2026-10-03-cycle-03-panel-delete-record.md +11 -0
  31. package/.agents/report/port/2026-10-03-cycle-04-update-record.md +11 -0
  32. package/.agents/report/port/2026-10-03-cycle-05-api-values.md +11 -0
  33. package/.agents/report/port/2026-10-06-cycle-01-record-import.md +9 -0
  34. package/.agents/report/port/2026-10-06-cycle-03-rich-text-upload.md +7 -0
  35. package/.agents/report/port/2026-10-06-cycle-04-user-filters.md +11 -0
  36. package/.agents/report/port/2026-10-06-resume-cycle-01-panel-import.md +9 -0
  37. package/.agents/report/port/2026-10-07-cycle-01-api-assembly.md +16 -0
  38. package/.agents/report/port/2026-10-07-cycle-02-index-entry-point.md +13 -0
  39. package/.agents/report/remediation/2026-10-02-api-user-filter-remediation.md +71 -0
  40. package/.agents/report/remediation/2026-10-02-api-values-remediation.md +17 -0
  41. package/.agents/report/remediation/2026-10-03-callback-id-contract-remediation.md +36 -0
  42. package/.agents/report/remediation/2026-10-06-import-parser-remediation.md +41 -0
  43. package/.agents/report/remediation/2026-10-06-rich-text-upload-remediation.md +33 -0
  44. package/.agents/report/remediation/2026-10-07-api-assembly-remediation.md +32 -0
  45. package/.agents/skills/port-javascript-to-typescript/SKILL.md +42 -0
  46. package/.agents/skills/port-javascript-to-typescript/agents/openai.yaml +3 -0
  47. package/.agents/skills/port-javascript-to-typescript/references/blockers.md +31 -0
  48. package/.agents/skills/port-javascript-to-typescript/references/planning.md +43 -0
  49. package/.agents/skills/port-javascript-to-typescript/references/porting.md +9 -0
  50. package/.agents/skills/port-javascript-to-typescript/references/reporting.md +24 -0
  51. package/.agents/skills/port-javascript-to-typescript/references/rules.md +8 -0
  52. package/.agents/skills/port-javascript-to-typescript/references/selection.md +16 -0
  53. package/.agents/skills/port-javascript-to-typescript/references/tools.md +20 -0
  54. package/.agents/skills/port-javascript-to-typescript/scripts/check.sh +17 -0
  55. package/.agents/skills/port-javascript-to-typescript/scripts/graph.sh +30 -0
  56. package/.agents/skills/port-javascript-to-typescript/scripts/madge.json +12 -0
  57. package/.agents/skills/port-javascript-to-typescript/scripts/oxlint.json +14 -0
  58. package/.agents/skills/port-javascript-to-typescript/scripts/test-tools.cjs +76 -0
  59. package/AGENTS.md +5 -0
  60. package/dist/api/authorization.d.ts +38 -0
  61. package/dist/api/authorization.js +74 -0
  62. package/dist/api/authorization.js.map +1 -0
  63. package/dist/api/config.d.ts +42 -0
  64. package/dist/api/config.js +105 -0
  65. package/dist/api/config.js.map +1 -0
  66. package/dist/api/filterUser.d.ts +2 -0
  67. package/dist/api/filterUser.js +24 -0
  68. package/dist/api/filterUser.js.map +1 -0
  69. package/dist/api/importRecords.d.ts +1 -0
  70. package/dist/api/importRecords.js +16 -0
  71. package/dist/api/importRecords.js.map +1 -0
  72. package/dist/api/queries.d.ts +48 -0
  73. package/dist/api/queries.js +139 -0
  74. package/dist/api/queries.js.map +1 -0
  75. package/dist/api/request.d.ts +29 -0
  76. package/dist/api/request.js +3 -0
  77. package/dist/api/request.js.map +1 -0
  78. package/dist/api/routes/adjacentRecordId.d.ts +10 -0
  79. package/dist/api/routes/adjacentRecordId.js +13 -0
  80. package/dist/api/routes/adjacentRecordId.js.map +1 -0
  81. package/dist/api/routes/config.d.ts +3 -0
  82. package/dist/api/routes/config.js +27 -0
  83. package/dist/api/routes/config.js.map +1 -0
  84. package/dist/api/routes/createRecord.d.ts +19 -0
  85. package/dist/api/routes/createRecord.js +93 -0
  86. package/dist/api/routes/createRecord.js.map +1 -0
  87. package/dist/api/routes/customAction.d.ts +17 -0
  88. package/dist/api/routes/customAction.js +35 -0
  89. package/dist/api/routes/customAction.js.map +1 -0
  90. package/dist/api/routes/customComponent.d.ts +3 -0
  91. package/dist/api/routes/customComponent.js +34 -0
  92. package/dist/api/routes/customComponent.js.map +1 -0
  93. package/dist/api/routes/customQuery.d.ts +7 -0
  94. package/dist/api/routes/customQuery.js +33 -0
  95. package/dist/api/routes/customQuery.js.map +1 -0
  96. package/dist/api/routes/deleteRecord.d.ts +9 -0
  97. package/dist/api/routes/deleteRecord.js +46 -0
  98. package/dist/api/routes/deleteRecord.js.map +1 -0
  99. package/dist/api/routes/deleteRecordFromPanel.d.ts +10 -0
  100. package/dist/api/routes/deleteRecordFromPanel.js +36 -0
  101. package/dist/api/routes/deleteRecordFromPanel.js.map +1 -0
  102. package/dist/api/routes/deleteRecords.d.ts +9 -0
  103. package/dist/api/routes/deleteRecords.js +12 -0
  104. package/dist/api/routes/deleteRecords.js.map +1 -0
  105. package/dist/api/routes/duplicateRecord.d.ts +17 -0
  106. package/dist/api/routes/duplicateRecord.js +55 -0
  107. package/dist/api/routes/duplicateRecord.js.map +1 -0
  108. package/dist/api/routes/foreignSelectRecords.d.ts +10 -0
  109. package/dist/api/routes/foreignSelectRecords.js +61 -0
  110. package/dist/api/routes/foreignSelectRecords.js.map +1 -0
  111. package/dist/api/routes/notificationBadges.d.ts +6 -0
  112. package/dist/api/routes/notificationBadges.js +31 -0
  113. package/dist/api/routes/notificationBadges.js.map +1 -0
  114. package/dist/api/routes/record.d.ts +7 -0
  115. package/dist/api/routes/record.js +32 -0
  116. package/dist/api/routes/record.js.map +1 -0
  117. package/dist/api/routes/records.d.ts +14 -0
  118. package/dist/api/routes/records.js +57 -0
  119. package/dist/api/routes/records.js.map +1 -0
  120. package/dist/api/routes/recordsExport.d.ts +13 -0
  121. package/dist/api/routes/recordsExport.js +67 -0
  122. package/dist/api/routes/recordsExport.js.map +1 -0
  123. package/dist/api/routes/recordsImport.d.ts +4 -0
  124. package/dist/api/routes/recordsImport.js +100 -0
  125. package/dist/api/routes/recordsImport.js.map +1 -0
  126. package/dist/api/routes/searchCompanies.d.ts +17 -0
  127. package/dist/api/routes/searchCompanies.js +72 -0
  128. package/dist/api/routes/searchCompanies.js.map +1 -0
  129. package/dist/api/routes/subrecordsImport.d.ts +4 -0
  130. package/dist/api/routes/subrecordsImport.js +94 -0
  131. package/dist/api/routes/subrecordsImport.js.map +1 -0
  132. package/dist/api/routes/updateRecord.d.ts +13 -0
  133. package/dist/api/routes/updateRecord.js +55 -0
  134. package/dist/api/routes/updateRecord.js.map +1 -0
  135. package/dist/api/routes/uploadFile.d.ts +3 -0
  136. package/dist/api/routes/uploadFile.js +13 -0
  137. package/dist/api/routes/uploadFile.js.map +1 -0
  138. package/dist/api/routes/uploadFileFromBetterRichTextInput.d.ts +3 -0
  139. package/dist/api/routes/uploadFileFromBetterRichTextInput.js +33 -0
  140. package/dist/api/routes/uploadFileFromBetterRichTextInput.js.map +1 -0
  141. package/dist/api/uploadFile.d.ts +4 -0
  142. package/dist/api/uploadFile.js +209 -0
  143. package/dist/api/uploadFile.js.map +1 -0
  144. package/dist/api/values.d.ts +6 -0
  145. package/dist/api/values.js +88 -0
  146. package/dist/api/values.js.map +1 -0
  147. package/dist/api.d.ts +8 -27
  148. package/dist/api.js +80 -1387
  149. package/dist/api.js.map +1 -1
  150. package/dist/index.d.ts +2 -2
  151. package/dist/index.js +8 -2
  152. package/dist/index.js.map +1 -1
  153. package/dist/tsconfig.strict.tsbuildinfo +1 -0
  154. package/dist/types/config.d.ts +15 -5
  155. package/dist/types/config.js +4 -0
  156. package/dist/types/config.js.map +1 -1
  157. package/dist/types/database.d.ts +1 -0
  158. package/dist/types/database.js +3 -0
  159. package/dist/types/database.js.map +1 -0
  160. package/dist/utils/dbManager.d.ts +32 -7
  161. package/dist/utils/dbManager.js +59 -7
  162. package/dist/utils/dbManager.js.map +1 -1
  163. package/package.json +12 -3
  164. package/src/api/authorization.ts +129 -0
  165. package/src/api/config.ts +152 -0
  166. package/src/api/filterUser.ts +28 -0
  167. package/src/api/importRecords.ts +16 -0
  168. package/src/api/queries.ts +194 -0
  169. package/src/api/request.ts +31 -0
  170. package/src/api/routes/adjacentRecordId.ts +20 -0
  171. package/src/api/routes/config.ts +15 -0
  172. package/src/api/routes/createRecord.ts +131 -0
  173. package/src/api/routes/customAction.ts +60 -0
  174. package/src/api/routes/customComponent.ts +44 -0
  175. package/src/api/routes/customQuery.ts +53 -0
  176. package/src/api/routes/deleteRecord.ts +59 -0
  177. package/src/api/routes/deleteRecordFromPanel.ts +58 -0
  178. package/src/api/routes/deleteRecords.ts +28 -0
  179. package/src/api/routes/duplicateRecord.ts +73 -0
  180. package/src/api/routes/foreignSelectRecords.ts +80 -0
  181. package/src/api/routes/notificationBadges.ts +47 -0
  182. package/src/api/routes/record.ts +48 -0
  183. package/src/api/routes/records.ts +91 -0
  184. package/src/api/routes/recordsExport.ts +92 -0
  185. package/src/api/routes/recordsImport.ts +115 -0
  186. package/src/api/routes/searchCompanies.ts +96 -0
  187. package/src/api/routes/subrecordsImport.ts +114 -0
  188. package/src/api/routes/updateRecord.ts +74 -0
  189. package/src/api/routes/uploadFile.ts +16 -0
  190. package/src/api/routes/uploadFileFromBetterRichTextInput.ts +41 -0
  191. package/src/api/uploadFile.ts +249 -0
  192. package/src/api/values.ts +96 -0
  193. package/src/api.ts +94 -1580
  194. package/src/index.ts +42 -25
  195. package/src/types/config.ts +15 -5
  196. package/src/types/database.ts +2 -0
  197. package/src/utils/dbManager.ts +99 -14
  198. package/tests/api-helpers.test.ts +59 -0
  199. package/tests/dbManager.test.ts +16 -1
  200. package/tests/helpers/fakeDb.ts +9 -7
  201. package/tests/integration/authorizationBoundary.integration.test.ts +57 -0
  202. package/tests/integration/configEnrichment.integration.test.ts +79 -0
  203. package/tests/integration/customComponentBoundary.integration.test.ts +58 -0
  204. package/tests/integration/dbManager.integration.test.ts +105 -3
  205. package/tests/integration/importParserChild.cjs +55 -0
  206. package/tests/integration/indexRuntime.integration.test.ts +22 -0
  207. package/tests/integration/indexRuntimeChild.cjs +110 -0
  208. package/tests/integration/queries.integration.test.ts +88 -0
  209. package/tests/integration/requestConfig.integration.test.ts +118 -0
  210. package/tests/integration/routes.authorization.integration.test.ts +1 -0
  211. package/tests/integration/routes.config.integration.test.ts +39 -1
  212. package/tests/integration/routes.crud.integration.test.ts +233 -1
  213. package/tests/integration/routes.custom.integration.test.ts +119 -0
  214. package/tests/integration/routes.foreign.integration.test.ts +3 -2
  215. package/tests/integration/routes.importExport.integration.test.ts +308 -6
  216. package/tests/integration/routes.integration.test.ts +4 -0
  217. package/tests/integration/routes.records.integration.test.ts +82 -6
  218. package/tests/integration/routes.upload.integration.test.ts +123 -0
  219. package/tests/integration/userFilters.integration.test.ts +225 -0
  220. package/tsconfig.json +1 -1
  221. package/tsconfig.strict.json +45 -0
  222. package/dist/front/ckeditor5-build/src/ckeditor.d.ts +0 -43
  223. package/dist/front/ckeditor5-build/src/ckeditor.js +0 -130
  224. package/dist/front/ckeditor5-build/src/ckeditor.js.map +0 -1
  225. package/dist/tsconfig.tsbuildinfo +0 -1
package/src/api.ts CHANGED
@@ -1,230 +1,60 @@
1
1
  import * as express from "express";
2
- import * as multiparty from "multiparty";
3
- import * as fs from "fs";
4
- import * as path from "path";
5
- import * as crypto from "crypto";
6
- import * as mime from "mime";
7
-
8
- import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
9
- import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
10
- import { Storage } from "@google-cloud/storage";
11
- import { getImageDimensions } from "./utils/imageDimensions";
12
- import * as serializeJs from "serialize-javascript";
13
- import * as SftpClient from "ssh2-sftp-client";
14
-
15
- import * as rollup from "rollup";
16
- import babel from "@rollup/plugin-babel";
17
- import resolve from "@rollup/plugin-node-resolve";
18
- import axios from "axios";
19
- import { v4 as uuidv4 } from "uuid";
20
-
21
- import { quoteIdentifier } from "./utils/dbManager";
22
-
23
- export function generateUniqueIdForObject(obj) {
24
- const str = JSON.stringify(obj);
25
- const hash = crypto.createHash("sha256").update(str).digest("hex");
26
- return hash;
27
- }
28
-
29
- // ---------------------------------------------------------------------------
30
- // SECURITY: server-side authorization.
31
- //
32
- // The config-filtering middleware below only decides which views the *client*
33
- // is shown, and sets canEdit/canCreate/canDelete flags on that served config.
34
- // Nothing enforced those decisions server-side, so a session with the lowest
35
- // role could POST directly to /updateRecord, /deleteRecord, /customQuery, …
36
- // and act on any view — including read-only ones — as the DB user (root).
37
- // This block re-checks every sensitive route against the view's declared roles.
38
- // ---------------------------------------------------------------------------
39
- export const ROOT_ROLE = "root";
40
- export const roleOf = (req) =>
41
- req.session && req.session.user && req.session.user.role;
42
- // Write access = the view's `roles` (root always allowed).
43
- export const canWriteView = (view, role) =>
44
- role === ROOT_ROLE || !view || !view.roles || view.roles.includes(role);
45
-
46
- export const viewFromRequest = (req) => {
47
- const idx =
48
- req.body && req.body.viewIndex !== undefined
49
- ? req.body.viewIndex
50
- : req.query && req.query.viewIndex !== undefined
51
- ? req.query.viewIndex
52
- : undefined;
53
- if (idx === undefined || !req.config || !req.config.views) return undefined;
54
- return req.config.views[idx];
55
- };
56
-
57
- // Routes that mutate data and reference their target view via viewIndex
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.user &&
67
+ (req.session!.user &&
238
68
  [...(x.roles || []), ...(x.readRoles || [])].includes(
239
- req.session.user.role,
69
+ req.session!.user.role,
240
70
  )),
241
71
  )
242
72
  .map((view) => {
243
73
  const readOnly =
244
- req.session.user &&
74
+ req.session!.user &&
245
75
  view.roles &&
246
- !view.roles.includes(req.session.user.role);
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.user &&
264
- column.roles.includes(req.session.user.role));
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.enriched) {
285
- await enrichConfig(req.config, db);
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
- async function getRecords(params, computeCount = false) {
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("/customComponent", async (req, res) => {
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.post("/adjacentRecordId", async function (req, res, next) {
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("/foreignSelectRecords", async (req, res, next) => {
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("/records", async function (req, res, next) {
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
- for (const column of view.columns) {
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", async function (req, res) {
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
- let queryString =
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.get("/record", async function (req, res) {
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.post("/updateRecord", async function (req, res, next) {
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("/customAction", async function (req, res, next) {
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("/deleteRecord", async function (req, res) {
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("/deleteRecordFromPanel", async function (req, res) {
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("/deleteRecords", async function (req, res, next) {
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.get("/notificationBadges", async function (req, res) {
1086
- const notificationBadges = {};
182
+ app.post("/deleteRecords", deleteRecordsRoute(db));
1087
183
 
1088
- async function checkAndGetNotificationBadge(obj) {
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
- for (const view of req.config.views) {
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("/duplicateRecord", async function (req, res, next) {
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", async function (req, res, next) {
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
- async function (req, res, next) {
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", async function (req, res, next) {
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", async (req, res) => {
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
- }