@galadrim/galadmin 0.2.11 → 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 (233) 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/.github/actions/version-bump/action.yml +41 -0
  60. package/.github/workflows/alpha.yml +53 -0
  61. package/.github/workflows/ci.yml +8 -9
  62. package/.github/workflows/main.yml +13 -14
  63. package/AGENTS.md +5 -0
  64. package/dist/api/authorization.d.ts +38 -0
  65. package/dist/api/authorization.js +74 -0
  66. package/dist/api/authorization.js.map +1 -0
  67. package/dist/api/config.d.ts +42 -0
  68. package/dist/api/config.js +105 -0
  69. package/dist/api/config.js.map +1 -0
  70. package/dist/api/filterUser.d.ts +2 -0
  71. package/dist/api/filterUser.js +24 -0
  72. package/dist/api/filterUser.js.map +1 -0
  73. package/dist/api/importRecords.d.ts +1 -0
  74. package/dist/api/importRecords.js +16 -0
  75. package/dist/api/importRecords.js.map +1 -0
  76. package/dist/api/queries.d.ts +48 -0
  77. package/dist/api/queries.js +139 -0
  78. package/dist/api/queries.js.map +1 -0
  79. package/dist/api/request.d.ts +29 -0
  80. package/dist/api/request.js +3 -0
  81. package/dist/api/request.js.map +1 -0
  82. package/dist/api/routes/adjacentRecordId.d.ts +10 -0
  83. package/dist/api/routes/adjacentRecordId.js +13 -0
  84. package/dist/api/routes/adjacentRecordId.js.map +1 -0
  85. package/dist/api/routes/config.d.ts +3 -0
  86. package/dist/api/routes/config.js +27 -0
  87. package/dist/api/routes/config.js.map +1 -0
  88. package/dist/api/routes/createRecord.d.ts +19 -0
  89. package/dist/api/routes/createRecord.js +93 -0
  90. package/dist/api/routes/createRecord.js.map +1 -0
  91. package/dist/api/routes/customAction.d.ts +17 -0
  92. package/dist/api/routes/customAction.js +35 -0
  93. package/dist/api/routes/customAction.js.map +1 -0
  94. package/dist/api/routes/customComponent.d.ts +3 -0
  95. package/dist/api/routes/customComponent.js +34 -0
  96. package/dist/api/routes/customComponent.js.map +1 -0
  97. package/dist/api/routes/customQuery.d.ts +7 -0
  98. package/dist/api/routes/customQuery.js +33 -0
  99. package/dist/api/routes/customQuery.js.map +1 -0
  100. package/dist/api/routes/deleteRecord.d.ts +9 -0
  101. package/dist/api/routes/deleteRecord.js +46 -0
  102. package/dist/api/routes/deleteRecord.js.map +1 -0
  103. package/dist/api/routes/deleteRecordFromPanel.d.ts +10 -0
  104. package/dist/api/routes/deleteRecordFromPanel.js +36 -0
  105. package/dist/api/routes/deleteRecordFromPanel.js.map +1 -0
  106. package/dist/api/routes/deleteRecords.d.ts +9 -0
  107. package/dist/api/routes/deleteRecords.js +12 -0
  108. package/dist/api/routes/deleteRecords.js.map +1 -0
  109. package/dist/api/routes/duplicateRecord.d.ts +17 -0
  110. package/dist/api/routes/duplicateRecord.js +55 -0
  111. package/dist/api/routes/duplicateRecord.js.map +1 -0
  112. package/dist/api/routes/foreignSelectRecords.d.ts +10 -0
  113. package/dist/api/routes/foreignSelectRecords.js +61 -0
  114. package/dist/api/routes/foreignSelectRecords.js.map +1 -0
  115. package/dist/api/routes/notificationBadges.d.ts +6 -0
  116. package/dist/api/routes/notificationBadges.js +31 -0
  117. package/dist/api/routes/notificationBadges.js.map +1 -0
  118. package/dist/api/routes/record.d.ts +7 -0
  119. package/dist/api/routes/record.js +32 -0
  120. package/dist/api/routes/record.js.map +1 -0
  121. package/dist/api/routes/records.d.ts +14 -0
  122. package/dist/api/routes/records.js +57 -0
  123. package/dist/api/routes/records.js.map +1 -0
  124. package/dist/api/routes/recordsExport.d.ts +13 -0
  125. package/dist/api/routes/recordsExport.js +67 -0
  126. package/dist/api/routes/recordsExport.js.map +1 -0
  127. package/dist/api/routes/recordsImport.d.ts +4 -0
  128. package/dist/api/routes/recordsImport.js +100 -0
  129. package/dist/api/routes/recordsImport.js.map +1 -0
  130. package/dist/api/routes/searchCompanies.d.ts +17 -0
  131. package/dist/api/routes/searchCompanies.js +72 -0
  132. package/dist/api/routes/searchCompanies.js.map +1 -0
  133. package/dist/api/routes/subrecordsImport.d.ts +4 -0
  134. package/dist/api/routes/subrecordsImport.js +94 -0
  135. package/dist/api/routes/subrecordsImport.js.map +1 -0
  136. package/dist/api/routes/updateRecord.d.ts +13 -0
  137. package/dist/api/routes/updateRecord.js +55 -0
  138. package/dist/api/routes/updateRecord.js.map +1 -0
  139. package/dist/api/routes/uploadFile.d.ts +3 -0
  140. package/dist/api/routes/uploadFile.js +13 -0
  141. package/dist/api/routes/uploadFile.js.map +1 -0
  142. package/dist/api/routes/uploadFileFromBetterRichTextInput.d.ts +3 -0
  143. package/dist/api/routes/uploadFileFromBetterRichTextInput.js +33 -0
  144. package/dist/api/routes/uploadFileFromBetterRichTextInput.js.map +1 -0
  145. package/dist/api/uploadFile.d.ts +4 -0
  146. package/dist/api/uploadFile.js +209 -0
  147. package/dist/api/uploadFile.js.map +1 -0
  148. package/dist/api/values.d.ts +6 -0
  149. package/dist/api/values.js +88 -0
  150. package/dist/api/values.js.map +1 -0
  151. package/dist/api.d.ts +8 -27
  152. package/dist/api.js +80 -1387
  153. package/dist/api.js.map +1 -1
  154. package/dist/index.d.ts +2 -2
  155. package/dist/index.js +8 -2
  156. package/dist/index.js.map +1 -1
  157. package/dist/tsconfig.strict.tsbuildinfo +1 -0
  158. package/dist/types/config.d.ts +15 -5
  159. package/dist/types/config.js +4 -0
  160. package/dist/types/config.js.map +1 -1
  161. package/dist/types/database.d.ts +1 -0
  162. package/dist/types/database.js +3 -0
  163. package/dist/types/database.js.map +1 -0
  164. package/dist/utils/dbManager.d.ts +32 -7
  165. package/dist/utils/dbManager.js +59 -7
  166. package/dist/utils/dbManager.js.map +1 -1
  167. package/package.json +12 -3
  168. package/src/api/authorization.ts +129 -0
  169. package/src/api/config.ts +152 -0
  170. package/src/api/filterUser.ts +28 -0
  171. package/src/api/importRecords.ts +16 -0
  172. package/src/api/queries.ts +194 -0
  173. package/src/api/request.ts +31 -0
  174. package/src/api/routes/adjacentRecordId.ts +20 -0
  175. package/src/api/routes/config.ts +15 -0
  176. package/src/api/routes/createRecord.ts +131 -0
  177. package/src/api/routes/customAction.ts +60 -0
  178. package/src/api/routes/customComponent.ts +44 -0
  179. package/src/api/routes/customQuery.ts +53 -0
  180. package/src/api/routes/deleteRecord.ts +59 -0
  181. package/src/api/routes/deleteRecordFromPanel.ts +58 -0
  182. package/src/api/routes/deleteRecords.ts +28 -0
  183. package/src/api/routes/duplicateRecord.ts +73 -0
  184. package/src/api/routes/foreignSelectRecords.ts +80 -0
  185. package/src/api/routes/notificationBadges.ts +47 -0
  186. package/src/api/routes/record.ts +48 -0
  187. package/src/api/routes/records.ts +91 -0
  188. package/src/api/routes/recordsExport.ts +92 -0
  189. package/src/api/routes/recordsImport.ts +115 -0
  190. package/src/api/routes/searchCompanies.ts +96 -0
  191. package/src/api/routes/subrecordsImport.ts +114 -0
  192. package/src/api/routes/updateRecord.ts +74 -0
  193. package/src/api/routes/uploadFile.ts +16 -0
  194. package/src/api/routes/uploadFileFromBetterRichTextInput.ts +41 -0
  195. package/src/api/uploadFile.ts +249 -0
  196. package/src/api/values.ts +96 -0
  197. package/src/api.ts +94 -1580
  198. package/src/front/ckeditor5-build/node_modules/semver/bin/semver.js +0 -0
  199. package/src/index.ts +42 -25
  200. package/src/types/config.ts +15 -5
  201. package/src/types/database.ts +2 -0
  202. package/src/utils/dbManager.ts +99 -14
  203. package/tests/api-helpers.test.ts +59 -0
  204. package/tests/dbManager.test.ts +16 -1
  205. package/tests/helpers/fakeDb.ts +9 -7
  206. package/tests/integration/authorizationBoundary.integration.test.ts +57 -0
  207. package/tests/integration/configEnrichment.integration.test.ts +79 -0
  208. package/tests/integration/customComponentBoundary.integration.test.ts +58 -0
  209. package/tests/integration/dbManager.integration.test.ts +105 -3
  210. package/tests/integration/importParserChild.cjs +55 -0
  211. package/tests/integration/indexRuntime.integration.test.ts +22 -0
  212. package/tests/integration/indexRuntimeChild.cjs +110 -0
  213. package/tests/integration/queries.integration.test.ts +88 -0
  214. package/tests/integration/requestConfig.integration.test.ts +118 -0
  215. package/tests/integration/routes.authorization.integration.test.ts +1 -0
  216. package/tests/integration/routes.config.integration.test.ts +39 -1
  217. package/tests/integration/routes.crud.integration.test.ts +233 -1
  218. package/tests/integration/routes.custom.integration.test.ts +119 -0
  219. package/tests/integration/routes.foreign.integration.test.ts +3 -2
  220. package/tests/integration/routes.importExport.integration.test.ts +308 -6
  221. package/tests/integration/routes.integration.test.ts +4 -0
  222. package/tests/integration/routes.records.integration.test.ts +82 -6
  223. package/tests/integration/routes.upload.integration.test.ts +123 -0
  224. package/tests/integration/userFilters.integration.test.ts +225 -0
  225. package/tsconfig.json +1 -1
  226. package/tsconfig.strict.json +45 -0
  227. package/dist/front/ckeditor5-build/src/ckeditor.d.ts +0 -43
  228. package/dist/front/ckeditor5-build/src/ckeditor.js +0 -130
  229. package/dist/front/ckeditor5-build/src/ckeditor.js.map +0 -1
  230. package/dist/tsconfig.tsbuildinfo +0 -1
  231. package/src/front/ckeditor5-build/node_modules/.bin/semver +0 -12
  232. package/src/front/ckeditor5-build/node_modules/.bin/semver.cmd +0 -17
  233. package/src/front/ckeditor5-build/node_modules/.bin/semver.ps1 +0 -28
@@ -0,0 +1,114 @@
1
+ import type { RequestHandler } from "../request";
2
+ import type DbManager from "../../utils/dbManager";
3
+ import * as multiparty from "multiparty";
4
+ import { quoteIdentifier } from "../../utils/dbManager";
5
+ import { canWriteView, roleOf } from "../authorization";
6
+ import { formatValueForInsertion } from "../values";
7
+ import { isImportRecords } from "../importRecords";
8
+
9
+ export default (db: DbManager): RequestHandler =>
10
+ async function (req, res) {
11
+ const form = new multiparty.Form();
12
+ form.parse(req, async function (err, fields, files) {
13
+ if (err) {
14
+ return res.status(500).send(err);
15
+ }
16
+ // Get viewIndex and panelIndex in fields
17
+ const view = req.config!.views[fields.viewIndex![0] as `${number}`];
18
+ const panel = view
19
+ ? view.recordViewPanels![fields.panelIndex![0] as `${number}`]
20
+ : undefined;
21
+ // SECURITY: same write-guard as /recordsImport, plus the panel's own
22
+ // roles when it declares any.
23
+ if (
24
+ !view ||
25
+ !canWriteView(view, roleOf(req)) ||
26
+ !panel ||
27
+ !canWriteView(panel, roleOf(req))
28
+ ) {
29
+ return res.sendStatus(403);
30
+ }
31
+ if (typeof panel.parseImportFile !== "function") {
32
+ return res
33
+ .status(500)
34
+ .send(
35
+ '"parseImportFile" doit être défini dans la configuration du panneau.',
36
+ );
37
+ }
38
+ if (!Boolean(panel.createConfig)) {
39
+ return res
40
+ .status(500)
41
+ .send(
42
+ '"createConfig" doit être défini dans la configuration du panneau.',
43
+ );
44
+ }
45
+
46
+ const records: unknown = await panel.parseImportFile(
47
+ files.data![0],
48
+ fields.recordId![0],
49
+ );
50
+ /*
51
+ #PORTING-EDIT
52
+ The approved object-row shape check rejects invalid parser output before SQL insertion.
53
+ */
54
+ if (!isImportRecords(records)) {
55
+ return res.sendStatus(400);
56
+ }
57
+ const columns: string[] = [];
58
+ if (records.length === 0) {
59
+ return res.send({
60
+ affectedRows: 0,
61
+ });
62
+ }
63
+
64
+ try {
65
+ for (let record of records) {
66
+ for (let key in record) {
67
+ if (!columns.includes(key)) {
68
+ columns.push(key);
69
+ }
70
+ }
71
+ }
72
+
73
+ let queryString =
74
+ "INSERT IGNORE INTO ?? (" +
75
+ quoteIdentifier(panel.createConfig!.recordIdKey, db.dbType!) +
76
+ ", " +
77
+ columns.map((x) => quoteIdentifier(x, db.dbType!)).join(", ") +
78
+ ") VALUES";
79
+ for (let record of records) {
80
+ queryString +=
81
+ " (" +
82
+ fields.recordId![0] +
83
+ ", " +
84
+ columns
85
+ .map((x) => formatValueForInsertion(record[x], db.dbType!))
86
+ .join(", ") +
87
+ "),";
88
+ }
89
+ queryString = queryString.slice(0, -1) + " ON DUPLICATE KEY UPDATE";
90
+ for (let column of columns) {
91
+ queryString += ` ${quoteIdentifier(column, db.dbType!)} = VALUES(${quoteIdentifier(column, db.dbType!)}),`;
92
+ }
93
+ const { err, rows: r } = await db.query(queryString.slice(0, -1), [
94
+ panel.tableName,
95
+ ]);
96
+ if (err) {
97
+ console.error(err);
98
+ return res.status(500).send({
99
+ err,
100
+ message: "Une erreur est survenue.",
101
+ });
102
+ }
103
+ res.send({
104
+ affectedRows: (r as { affectedRows?: number }).affectedRows,
105
+ });
106
+ } catch (err) {
107
+ console.error(err);
108
+ res.status(500).send({
109
+ err,
110
+ message: "Une erreur est survenue.",
111
+ });
112
+ }
113
+ });
114
+ };
@@ -0,0 +1,74 @@
1
+ import type { RequestHandler } from "../request";
2
+ import type DbManager from "../../utils/dbManager";
3
+ import type { DatabaseRow } from "../../utils/dbManager";
4
+ import axios from "axios";
5
+ import { quoteIdentifier } from "../../utils/dbManager";
6
+ import { areSafeColumnNames, touchesHiddenColumn } from "../authorization";
7
+
8
+ type RequestBody = {
9
+ viewIndex: string | number;
10
+ primaryValue: DatabaseRow[string];
11
+ data: Partial<DatabaseRow>;
12
+ previousData?: unknown;
13
+ };
14
+
15
+ export default (db: DbManager): RequestHandler<
16
+ Record<string, string>,
17
+ string | Error | { error: string },
18
+ RequestBody
19
+ > =>
20
+ async function (req, res, next) {
21
+ const view = req.config!.views[req.body.viewIndex as number];
22
+ if (!areSafeColumnNames(req.body.data)) {
23
+ return res.status(400).send({ error: "Invalid column name" });
24
+ }
25
+ if (touchesHiddenColumn(view, req.body.data)) {
26
+ return res.sendStatus(403);
27
+ }
28
+ for (let key in req.body.data) {
29
+ if (
30
+ req.body.data[key] == "NULL" ||
31
+ req.body.data[key] === "" ||
32
+ req.body.data[key] === "Invalid date"
33
+ ) {
34
+ req.body.data[key] = null;
35
+ }
36
+ }
37
+ if (Object.keys(req.body.data).length === 0) {
38
+ return res.sendStatus(200);
39
+ }
40
+ const queryExpression = Object.keys(req.body.data)
41
+ .map((x) => `${quoteIdentifier(x, db.dbType!)} = ?`)
42
+ .join(", ");
43
+ const queryParameters = Object.keys(req.body.data).map(
44
+ (x) => req.body.data[x],
45
+ );
46
+ const { err } = await db.query(
47
+ `UPDATE ?? SET ${queryExpression} WHERE ?? = ?`,
48
+ [
49
+ view.tableName,
50
+ ...queryParameters,
51
+ view.primaryId,
52
+ req.body.primaryValue,
53
+ ],
54
+ );
55
+ if (err) {
56
+ return res.status(500).send(err);
57
+ }
58
+ if (view.editCallback) {
59
+ try {
60
+ await axios.post(view.editCallback, {
61
+ id: req.body.primaryValue,
62
+ data: req.body.data,
63
+ previousData: req.body.previousData,
64
+ });
65
+ } catch (e) {}
66
+ }
67
+ if (view.editCallbackFunction) {
68
+ await view.editCallbackFunction({
69
+ id: req.body.primaryValue as number, // TODO https://github.com/galadrimteam/galadmin/issues/45: route parameters and UUID keys are also strings.
70
+ data: req.body.data,
71
+ });
72
+ }
73
+ res.sendStatus(200);
74
+ };
@@ -0,0 +1,16 @@
1
+ import type { FileUploadProperties } from "../../types/config";
2
+ import type { RequestHandler } from "../request";
3
+ import { uploadFile } from "../uploadFile";
4
+
5
+ export default (): RequestHandler =>
6
+ async function (req, res, next) {
7
+ const view = req.config!.views[req.query.viewIndex as `${number}`];
8
+ const column = [
9
+ ...view.columns!,
10
+ ...(view.recordViewPanels
11
+ ?.map((x) => x?.createConfig?.columns)
12
+ ?.filter((x) => x)
13
+ ?.flat() || []),
14
+ ].find((x) => x!.__id__ == req.query.columnId);
15
+ uploadFile(column as FileUploadProperties, "file", req, res);
16
+ };
@@ -0,0 +1,41 @@
1
+ import type { RequestHandler } from "../request";
2
+ import type { BetterRichTextColumn } from "../../types/config";
3
+ import { uploadFile } from "../uploadFile";
4
+ import { canWriteView, roleOf } from "../authorization";
5
+
6
+ export default (): RequestHandler =>
7
+ async function (req, res, next) {
8
+ // Resolve settings on the server so function-valued pathTransform survives.
9
+ // Validate the target before parsing multipart data or contacting storage.
10
+ const role = roleOf(req);
11
+ const { viewIndex, columnId } = req.query;
12
+ if (
13
+ typeof viewIndex !== "string" ||
14
+ !/^\d+$/.test(viewIndex) ||
15
+ typeof columnId !== "string"
16
+ ) {
17
+ return res.sendStatus(403);
18
+ }
19
+ const view = req.config!.views[viewIndex as `${number}`];
20
+ if (!view || !canWriteView(view, role)) {
21
+ return res.sendStatus(403);
22
+ }
23
+ const column = [
24
+ ...(view.columns || []),
25
+ ...(view.recordViewPanels?.flatMap(
26
+ (panel) => panel.createConfig?.columns || [],
27
+ ) || []),
28
+ ].find((column) => column.__id__ === columnId);
29
+ if (
30
+ !column ||
31
+ (column.roles &&
32
+ !(column.roles as readonly (string | undefined)[]).includes(role))
33
+ ) {
34
+ return res.sendStatus(403);
35
+ }
36
+ const uploadColumn = column as typeof column & Pick<BetterRichTextColumn, "fileUploadProperties">;
37
+ if (column.dataType !== "betterRichText" || !uploadColumn.fileUploadProperties) {
38
+ return res.status(400).send({ error: "Invalid rich-text upload column" });
39
+ }
40
+ uploadFile(uploadColumn.fileUploadProperties, "upload", req, res);
41
+ };
@@ -0,0 +1,249 @@
1
+ import * as multiparty from "multiparty";
2
+ import * as fs from "fs";
3
+ import * as mime from "mime";
4
+ import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
5
+ import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
6
+ import { Storage } from "@google-cloud/storage";
7
+ import { getImageDimensions } from "../utils/imageDimensions";
8
+ import * as SftpClient from "ssh2-sftp-client";
9
+ import axios from "axios";
10
+ import type { IncomingMessage } from "http";
11
+ import type { Response } from "express";
12
+ import type { FileUploadProperties } from "../types/config";
13
+
14
+ function firstUploadedFile(
15
+ files: Record<string, multiparty.File[] | undefined>,
16
+ field: string,
17
+ res: Response,
18
+ ) {
19
+ const file = files[field]?.[0];
20
+ /*
21
+ #PORTING-EDIT
22
+ Reject a missing upload field with HTTP 400 before reading file metadata.
23
+ The multipart parser permits requests without the requested file.
24
+ */
25
+ if (!file) res.status(400).send("Missing upload file");
26
+ return file;
27
+ }
28
+
29
+ function sftpProperty(
30
+ config: FileUploadProperties["sftpConfig"],
31
+ key: keyof NonNullable<FileUploadProperties["sftpConfig"]>,
32
+ ) {
33
+ if (config === undefined) {
34
+ throw new TypeError(`Cannot read properties of undefined (reading '${key}')`);
35
+ }
36
+ return config[key];
37
+ }
38
+
39
+ export function uploadFile(
40
+ column: FileUploadProperties,
41
+ fileParamName: string,
42
+ req: IncomingMessage,
43
+ res: Response,
44
+ ) {
45
+ if (column.s3Config || column.gcpConfig) {
46
+ const form = new multiparty.Form();
47
+ form.parse(req, async function (err, fields, files) {
48
+ if (err) {
49
+ return res.status(500).send(err);
50
+ }
51
+ const file = firstUploadedFile(files, fileParamName, res);
52
+ if (!file) return;
53
+ const originalFilename = file.originalFilename;
54
+ const contentType =
55
+ mime.getType(originalFilename) || "application/octet-stream";
56
+
57
+ const extension = originalFilename.match(/(?:\.([^.]+))?$/)?.[1];
58
+ let aspectRatio: number | null = null;
59
+ if (extension !== undefined && ["png", "jpeg", "jpg", "gif"].includes(extension)) {
60
+ try {
61
+ const dimensions = await getImageDimensions(
62
+ file.path,
63
+ );
64
+ if (dimensions) {
65
+ aspectRatio = dimensions.height / dimensions.width;
66
+ }
67
+ } catch {
68
+ // malformed image data: leave aspectRatio null rather than crash
69
+ }
70
+ }
71
+ const originalFilenameWithoutExtension = originalFilename.replace(
72
+ "." + extension,
73
+ "",
74
+ );
75
+
76
+ if (column.gcpConfig) {
77
+ const gcpConfig = column.gcpConfig;
78
+ const path =
79
+ (gcpConfig.subfolder ? gcpConfig.subfolder + "/" : "") +
80
+ originalFilenameWithoutExtension +
81
+ "-" +
82
+ Math.random().toString().substr(2) +
83
+ "." +
84
+ extension;
85
+ const storage = new Storage();
86
+
87
+ const bucketName = gcpConfig.bucket;
88
+ storage
89
+ .bucket(bucketName)
90
+ .upload(file.path, {
91
+ destination: path,
92
+ })
93
+ .then(() => {
94
+ res.status(200).send({
95
+ path: gcpConfig.pathTransform
96
+ ? gcpConfig.pathTransform(path)
97
+ : path,
98
+ aspectRatio,
99
+ originalFilename,
100
+ });
101
+ })
102
+ .catch((e: unknown) => {
103
+ res.status(500).send(e);
104
+ });
105
+ } else {
106
+ const s3Config = column.s3Config;
107
+ if (!s3Config) {
108
+ throw new TypeError("Cannot read properties of undefined (reading 'subfolder')");
109
+ }
110
+ const path =
111
+ (s3Config.subfolder ? s3Config.subfolder + "/" : "") +
112
+ originalFilenameWithoutExtension +
113
+ "-" +
114
+ Math.random().toString().substr(2) +
115
+ "." +
116
+ extension;
117
+ const rawHost = String(s3Config.host || "");
118
+ const endpoint = /^https?:\/\//i.test(rawHost)
119
+ ? rawHost
120
+ : `https://${rawHost}`;
121
+ const s3 = new S3Client({
122
+ endpoint,
123
+ region: s3Config.region,
124
+ ...(s3Config.accessKeyId && {
125
+ credentials: {
126
+ accessKeyId: s3Config.accessKeyId,
127
+ secretAccessKey: s3Config.secretAccessKey ?? "",
128
+ },
129
+ }),
130
+ forcePathStyle: false,
131
+ });
132
+ if (s3Config.signedUrlNeeded) {
133
+ getSignedUrl(
134
+ s3,
135
+ new PutObjectCommand({
136
+ Bucket: s3Config.bucket,
137
+ Key: path,
138
+ ContentType: contentType,
139
+ }),
140
+ { expiresIn: 150 },
141
+ )
142
+ .then((signedUrl) =>
143
+ axios.put(
144
+ signedUrl,
145
+ fs.readFileSync(file.path),
146
+ {
147
+ maxContentLength: 100000000000,
148
+ maxBodyLength: 100000000000,
149
+ headers: {
150
+ "Content-Type": contentType,
151
+ },
152
+ },
153
+ ),
154
+ )
155
+ .then(() => {
156
+ res.status(200).send({
157
+ path: s3Config.pathTransform
158
+ ? s3Config.pathTransform(path)
159
+ : path,
160
+ aspectRatio,
161
+ originalFilename,
162
+ });
163
+ })
164
+ .catch((e: unknown) => {
165
+ res.status(500).send(e);
166
+ });
167
+ } else {
168
+ s3.send(
169
+ new PutObjectCommand({
170
+ Body: fs.readFileSync(file.path),
171
+ Bucket: s3Config.bucket,
172
+ ContentType: contentType,
173
+ ACL: "public-read",
174
+ Key: path,
175
+ }),
176
+ )
177
+ .then(() => {
178
+ res.send({
179
+ url: `https://${s3Config.host}/${s3Config.bucket}/${path}`,
180
+ path: s3Config.pathTransform
181
+ ? s3Config.pathTransform(path)
182
+ : path,
183
+ aspectRatio,
184
+ originalFilename,
185
+ });
186
+ })
187
+ .catch((err: unknown) => res.status(500).send(err));
188
+ }
189
+ }
190
+ });
191
+ } else if (column.sftpConfig) {
192
+ if (!column.publicPath) {
193
+ return res
194
+ .status(500)
195
+ .send("You need to pass publicPath parameter when using sftpConfig");
196
+ }
197
+ const form = new multiparty.Form();
198
+ form.parse(req, function (err, fields, files) {
199
+ if (err) {
200
+ return res.status(500).send(err);
201
+ }
202
+ const file = firstUploadedFile(files, fileParamName, res);
203
+ if (!file) return;
204
+ const pathSplitted = file.path.split(/[\/\\]+/);
205
+ const filename = pathSplitted[pathSplitted.length - 1];
206
+ (async () => {
207
+ const sftp = new SftpClient();
208
+ await sftp.connect({
209
+ host: sftpProperty(column.sftpConfig, "host"),
210
+ port: 22,
211
+ username: sftpProperty(column.sftpConfig, "user"),
212
+ password: sftpProperty(column.sftpConfig, "password"),
213
+ });
214
+ await sftp.fastPut(
215
+ file.path,
216
+ column.publicPath +
217
+ (column.subdirectory ? "/" + column.subdirectory : "") +
218
+ "/" +
219
+ filename,
220
+ );
221
+ await sftp.end();
222
+ res.send({
223
+ path: filename,
224
+ originalFilename: file.originalFilename,
225
+ });
226
+ fs.unlinkSync(file.path);
227
+ })();
228
+ });
229
+ } else {
230
+ const publicPath = column.publicPath;
231
+ const subdirectory = column.subdirectory;
232
+ const form = new multiparty.Form({
233
+ uploadDir: publicPath === undefined && subdirectory === undefined
234
+ ? undefined
235
+ : String(publicPath) + String(subdirectory),
236
+ });
237
+ form.parse(req, function (err, fields, files) {
238
+ if (err) {
239
+ return res.status(500).send(err);
240
+ }
241
+ const file = firstUploadedFile(files, fileParamName, res);
242
+ if (!file) return;
243
+ res.send({
244
+ path: file.path.replace(String(column.publicPath), ""),
245
+ originalFilename: file.originalFilename,
246
+ });
247
+ });
248
+ }
249
+ }
@@ -0,0 +1,96 @@
1
+ import type {
2
+ BaseTypeColumn,
3
+ SelectColumn,
4
+ StandardColumn,
5
+ } from "../types/config";
6
+
7
+ type FormattingColumn = Pick<
8
+ BaseTypeColumn,
9
+ "formatValueForExport" | "formattedValue" | "formatValue"
10
+ > & StandardColumn & Partial<Pick<SelectColumn, "options">>;
11
+
12
+ export async function getFormattedValue(
13
+ column: FormattingColumn,
14
+ record: Record<string, unknown>,
15
+ ): Promise<unknown> {
16
+ let value: unknown;
17
+ try {
18
+ record.options = column.options;
19
+ if (column.formatValueForExport) {
20
+ value = await column.formatValueForExport(record);
21
+ } else if (column.formattedValue) {
22
+ value = evalExpression(column.formattedValue, record);
23
+ } else if (column.formatValue) {
24
+ value = await column.formatValue(record);
25
+ } else if (column.dataType == "tinyint") {
26
+ value = record[String(column.name)] == 1 ? "Oui" : "Non";
27
+ } else {
28
+ value = record[String(column.name)];
29
+ }
30
+ return value === null || typeof value == "undefined" ? "" : value;
31
+ } catch (err) {
32
+ return value || "";
33
+ }
34
+ }
35
+
36
+ export function evalExpression(
37
+ expression: string,
38
+ data: Record<string, unknown>,
39
+ ): unknown {
40
+ let varString = "";
41
+ let statement = "";
42
+
43
+ try {
44
+ for (let key in data) {
45
+ statement =
46
+ "var " +
47
+ key +
48
+ " = " +
49
+ (typeof data[key] === "string"
50
+ ? "'" +
51
+ data[key]
52
+ .replace(/'/g, "\\'")
53
+ .replace(/\n/g, "")
54
+ .replace(/\r/g, "") +
55
+ "'"
56
+ : typeof data[key] === "object"
57
+ ? JSON.stringify(data[key])
58
+ : data[key]) +
59
+ ";";
60
+ varString += statement;
61
+ }
62
+ const result: unknown = eval(varString + expression);
63
+ return result;
64
+ } catch (e) {
65
+ console.log("Problème d'évaluation de l'expression", statement);
66
+ console.error(e);
67
+ return "Error";
68
+ }
69
+ }
70
+
71
+ export function formatValueForInsertion(value: unknown, dbType: string) {
72
+ if (value !== 0 && !value) {
73
+ return "NULL";
74
+ }
75
+ if (value instanceof Date) {
76
+ return value.toISOString();
77
+ }
78
+ if (typeof value === "string") {
79
+ if (dbType === "POSTGRESQL") {
80
+ return "'" + value.replace(/'/g, "''") + "'";
81
+ } else {
82
+ return "'" + value.replace(/'/g, "\\'") + "'";
83
+ }
84
+ }
85
+ if (typeof value === "object") {
86
+ return "'" + JSON.stringify(value).replace(/'/g, "\\'") + "'";
87
+ }
88
+ if (typeof value === "boolean") {
89
+ return value ? "TRUE" : "FALSE";
90
+ }
91
+ if (typeof value === "number") {
92
+ return value;
93
+ }
94
+
95
+ return value;
96
+ }