@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.
- package/.agents/report/blockers.json +1 -0
- package/.agents/report/failures/2026-09-23-api-user-filter-contract.md +31 -0
- package/.agents/report/failures/2026-09-23-api-values-contract.md +25 -0
- package/.agents/report/failures/2026-10-01-cycle-02-create-callback-contract.md +17 -0
- package/.agents/report/failures/2026-10-01-cycle-06-delete-callback-id-contract.md +17 -0
- package/.agents/report/failures/2026-10-01-cycle-07-panel-delete-callback-id-contract.md +24 -0
- package/.agents/report/failures/2026-10-01-cycle-13-update-callback-id-contract.md +17 -0
- package/.agents/report/failures/2026-10-01-cycle-15-rich-text-upload-boundary-contract.md +24 -0
- package/.agents/report/failures/2026-10-03-cycle-06-record-import-parser-contract.md +11 -0
- package/.agents/report/failures/2026-10-03-cycle-07-panel-import-parser-contract.md +24 -0
- package/.agents/report/failures/2026-10-07-resume-cycle-02-api-assembly-contract.md +46 -0
- package/.agents/report/port/2026-09-23-api-config-enrichment.md +13 -0
- package/.agents/report/port/2026-09-23-api-file-upload.md +11 -0
- package/.agents/report/port/2026-09-23-api-query-authorization.md +14 -0
- package/.agents/report/port/2026-09-23-api-request-configuration.md +11 -0
- package/.agents/report/port/2026-09-23-database-manager.md +13 -0
- package/.agents/report/port/2026-10-01-api-adjacent-record-id.md +9 -0
- package/.agents/report/port/2026-10-01-cycle-01-api-config-route.md +9 -0
- package/.agents/report/port/2026-10-01-cycle-03-custom-action.md +9 -0
- package/.agents/report/port/2026-10-01-cycle-04-custom-component.md +7 -0
- package/.agents/report/port/2026-10-01-cycle-05-custom-query.md +7 -0
- package/.agents/report/port/2026-10-01-cycle-08-bulk-delete.md +9 -0
- package/.agents/report/port/2026-10-01-cycle-09-duplicate-record.md +9 -0
- package/.agents/report/port/2026-10-01-cycle-10-notification-badges.md +9 -0
- package/.agents/report/port/2026-10-01-cycle-11-record.md +7 -0
- package/.agents/report/port/2026-10-01-cycle-12-search-companies.md +11 -0
- package/.agents/report/port/2026-10-01-cycle-14-upload-file.md +7 -0
- package/.agents/report/port/2026-10-03-cycle-01-create-record.md +11 -0
- package/.agents/report/port/2026-10-03-cycle-02-delete-record.md +11 -0
- package/.agents/report/port/2026-10-03-cycle-03-panel-delete-record.md +11 -0
- package/.agents/report/port/2026-10-03-cycle-04-update-record.md +11 -0
- package/.agents/report/port/2026-10-03-cycle-05-api-values.md +11 -0
- package/.agents/report/port/2026-10-06-cycle-01-record-import.md +9 -0
- package/.agents/report/port/2026-10-06-cycle-03-rich-text-upload.md +7 -0
- package/.agents/report/port/2026-10-06-cycle-04-user-filters.md +11 -0
- package/.agents/report/port/2026-10-06-resume-cycle-01-panel-import.md +9 -0
- package/.agents/report/port/2026-10-07-cycle-01-api-assembly.md +16 -0
- package/.agents/report/port/2026-10-07-cycle-02-index-entry-point.md +13 -0
- package/.agents/report/remediation/2026-10-02-api-user-filter-remediation.md +71 -0
- package/.agents/report/remediation/2026-10-02-api-values-remediation.md +17 -0
- package/.agents/report/remediation/2026-10-03-callback-id-contract-remediation.md +36 -0
- package/.agents/report/remediation/2026-10-06-import-parser-remediation.md +41 -0
- package/.agents/report/remediation/2026-10-06-rich-text-upload-remediation.md +33 -0
- package/.agents/report/remediation/2026-10-07-api-assembly-remediation.md +32 -0
- package/.agents/skills/port-javascript-to-typescript/SKILL.md +42 -0
- package/.agents/skills/port-javascript-to-typescript/agents/openai.yaml +3 -0
- package/.agents/skills/port-javascript-to-typescript/references/blockers.md +31 -0
- package/.agents/skills/port-javascript-to-typescript/references/planning.md +43 -0
- package/.agents/skills/port-javascript-to-typescript/references/porting.md +9 -0
- package/.agents/skills/port-javascript-to-typescript/references/reporting.md +24 -0
- package/.agents/skills/port-javascript-to-typescript/references/rules.md +8 -0
- package/.agents/skills/port-javascript-to-typescript/references/selection.md +16 -0
- package/.agents/skills/port-javascript-to-typescript/references/tools.md +20 -0
- package/.agents/skills/port-javascript-to-typescript/scripts/check.sh +17 -0
- package/.agents/skills/port-javascript-to-typescript/scripts/graph.sh +30 -0
- package/.agents/skills/port-javascript-to-typescript/scripts/madge.json +12 -0
- package/.agents/skills/port-javascript-to-typescript/scripts/oxlint.json +14 -0
- package/.agents/skills/port-javascript-to-typescript/scripts/test-tools.cjs +76 -0
- package/.github/actions/version-bump/action.yml +41 -0
- package/.github/workflows/alpha.yml +53 -0
- package/.github/workflows/ci.yml +8 -9
- package/.github/workflows/main.yml +13 -14
- package/AGENTS.md +5 -0
- package/dist/api/authorization.d.ts +38 -0
- package/dist/api/authorization.js +74 -0
- package/dist/api/authorization.js.map +1 -0
- package/dist/api/config.d.ts +42 -0
- package/dist/api/config.js +105 -0
- package/dist/api/config.js.map +1 -0
- package/dist/api/filterUser.d.ts +2 -0
- package/dist/api/filterUser.js +24 -0
- package/dist/api/filterUser.js.map +1 -0
- package/dist/api/importRecords.d.ts +1 -0
- package/dist/api/importRecords.js +16 -0
- package/dist/api/importRecords.js.map +1 -0
- package/dist/api/queries.d.ts +48 -0
- package/dist/api/queries.js +139 -0
- package/dist/api/queries.js.map +1 -0
- package/dist/api/request.d.ts +29 -0
- package/dist/api/request.js +3 -0
- package/dist/api/request.js.map +1 -0
- package/dist/api/routes/adjacentRecordId.d.ts +10 -0
- package/dist/api/routes/adjacentRecordId.js +13 -0
- package/dist/api/routes/adjacentRecordId.js.map +1 -0
- package/dist/api/routes/config.d.ts +3 -0
- package/dist/api/routes/config.js +27 -0
- package/dist/api/routes/config.js.map +1 -0
- package/dist/api/routes/createRecord.d.ts +19 -0
- package/dist/api/routes/createRecord.js +93 -0
- package/dist/api/routes/createRecord.js.map +1 -0
- package/dist/api/routes/customAction.d.ts +17 -0
- package/dist/api/routes/customAction.js +35 -0
- package/dist/api/routes/customAction.js.map +1 -0
- package/dist/api/routes/customComponent.d.ts +3 -0
- package/dist/api/routes/customComponent.js +34 -0
- package/dist/api/routes/customComponent.js.map +1 -0
- package/dist/api/routes/customQuery.d.ts +7 -0
- package/dist/api/routes/customQuery.js +33 -0
- package/dist/api/routes/customQuery.js.map +1 -0
- package/dist/api/routes/deleteRecord.d.ts +9 -0
- package/dist/api/routes/deleteRecord.js +46 -0
- package/dist/api/routes/deleteRecord.js.map +1 -0
- package/dist/api/routes/deleteRecordFromPanel.d.ts +10 -0
- package/dist/api/routes/deleteRecordFromPanel.js +36 -0
- package/dist/api/routes/deleteRecordFromPanel.js.map +1 -0
- package/dist/api/routes/deleteRecords.d.ts +9 -0
- package/dist/api/routes/deleteRecords.js +12 -0
- package/dist/api/routes/deleteRecords.js.map +1 -0
- package/dist/api/routes/duplicateRecord.d.ts +17 -0
- package/dist/api/routes/duplicateRecord.js +55 -0
- package/dist/api/routes/duplicateRecord.js.map +1 -0
- package/dist/api/routes/foreignSelectRecords.d.ts +10 -0
- package/dist/api/routes/foreignSelectRecords.js +61 -0
- package/dist/api/routes/foreignSelectRecords.js.map +1 -0
- package/dist/api/routes/notificationBadges.d.ts +6 -0
- package/dist/api/routes/notificationBadges.js +31 -0
- package/dist/api/routes/notificationBadges.js.map +1 -0
- package/dist/api/routes/record.d.ts +7 -0
- package/dist/api/routes/record.js +32 -0
- package/dist/api/routes/record.js.map +1 -0
- package/dist/api/routes/records.d.ts +14 -0
- package/dist/api/routes/records.js +57 -0
- package/dist/api/routes/records.js.map +1 -0
- package/dist/api/routes/recordsExport.d.ts +13 -0
- package/dist/api/routes/recordsExport.js +67 -0
- package/dist/api/routes/recordsExport.js.map +1 -0
- package/dist/api/routes/recordsImport.d.ts +4 -0
- package/dist/api/routes/recordsImport.js +100 -0
- package/dist/api/routes/recordsImport.js.map +1 -0
- package/dist/api/routes/searchCompanies.d.ts +17 -0
- package/dist/api/routes/searchCompanies.js +72 -0
- package/dist/api/routes/searchCompanies.js.map +1 -0
- package/dist/api/routes/subrecordsImport.d.ts +4 -0
- package/dist/api/routes/subrecordsImport.js +94 -0
- package/dist/api/routes/subrecordsImport.js.map +1 -0
- package/dist/api/routes/updateRecord.d.ts +13 -0
- package/dist/api/routes/updateRecord.js +55 -0
- package/dist/api/routes/updateRecord.js.map +1 -0
- package/dist/api/routes/uploadFile.d.ts +3 -0
- package/dist/api/routes/uploadFile.js +13 -0
- package/dist/api/routes/uploadFile.js.map +1 -0
- package/dist/api/routes/uploadFileFromBetterRichTextInput.d.ts +3 -0
- package/dist/api/routes/uploadFileFromBetterRichTextInput.js +33 -0
- package/dist/api/routes/uploadFileFromBetterRichTextInput.js.map +1 -0
- package/dist/api/uploadFile.d.ts +4 -0
- package/dist/api/uploadFile.js +209 -0
- package/dist/api/uploadFile.js.map +1 -0
- package/dist/api/values.d.ts +6 -0
- package/dist/api/values.js +88 -0
- package/dist/api/values.js.map +1 -0
- package/dist/api.d.ts +8 -27
- package/dist/api.js +80 -1387
- package/dist/api.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +8 -2
- package/dist/index.js.map +1 -1
- package/dist/tsconfig.strict.tsbuildinfo +1 -0
- package/dist/types/config.d.ts +15 -5
- package/dist/types/config.js +4 -0
- package/dist/types/config.js.map +1 -1
- package/dist/types/database.d.ts +1 -0
- package/dist/types/database.js +3 -0
- package/dist/types/database.js.map +1 -0
- package/dist/utils/dbManager.d.ts +32 -7
- package/dist/utils/dbManager.js +59 -7
- package/dist/utils/dbManager.js.map +1 -1
- package/package.json +12 -3
- package/src/api/authorization.ts +129 -0
- package/src/api/config.ts +152 -0
- package/src/api/filterUser.ts +28 -0
- package/src/api/importRecords.ts +16 -0
- package/src/api/queries.ts +194 -0
- package/src/api/request.ts +31 -0
- package/src/api/routes/adjacentRecordId.ts +20 -0
- package/src/api/routes/config.ts +15 -0
- package/src/api/routes/createRecord.ts +131 -0
- package/src/api/routes/customAction.ts +60 -0
- package/src/api/routes/customComponent.ts +44 -0
- package/src/api/routes/customQuery.ts +53 -0
- package/src/api/routes/deleteRecord.ts +59 -0
- package/src/api/routes/deleteRecordFromPanel.ts +58 -0
- package/src/api/routes/deleteRecords.ts +28 -0
- package/src/api/routes/duplicateRecord.ts +73 -0
- package/src/api/routes/foreignSelectRecords.ts +80 -0
- package/src/api/routes/notificationBadges.ts +47 -0
- package/src/api/routes/record.ts +48 -0
- package/src/api/routes/records.ts +91 -0
- package/src/api/routes/recordsExport.ts +92 -0
- package/src/api/routes/recordsImport.ts +115 -0
- package/src/api/routes/searchCompanies.ts +96 -0
- package/src/api/routes/subrecordsImport.ts +114 -0
- package/src/api/routes/updateRecord.ts +74 -0
- package/src/api/routes/uploadFile.ts +16 -0
- package/src/api/routes/uploadFileFromBetterRichTextInput.ts +41 -0
- package/src/api/uploadFile.ts +249 -0
- package/src/api/values.ts +96 -0
- package/src/api.ts +94 -1580
- package/src/front/ckeditor5-build/node_modules/semver/bin/semver.js +0 -0
- package/src/index.ts +42 -25
- package/src/types/config.ts +15 -5
- package/src/types/database.ts +2 -0
- package/src/utils/dbManager.ts +99 -14
- package/tests/api-helpers.test.ts +59 -0
- package/tests/dbManager.test.ts +16 -1
- package/tests/helpers/fakeDb.ts +9 -7
- package/tests/integration/authorizationBoundary.integration.test.ts +57 -0
- package/tests/integration/configEnrichment.integration.test.ts +79 -0
- package/tests/integration/customComponentBoundary.integration.test.ts +58 -0
- package/tests/integration/dbManager.integration.test.ts +105 -3
- package/tests/integration/importParserChild.cjs +55 -0
- package/tests/integration/indexRuntime.integration.test.ts +22 -0
- package/tests/integration/indexRuntimeChild.cjs +110 -0
- package/tests/integration/queries.integration.test.ts +88 -0
- package/tests/integration/requestConfig.integration.test.ts +118 -0
- package/tests/integration/routes.authorization.integration.test.ts +1 -0
- package/tests/integration/routes.config.integration.test.ts +39 -1
- package/tests/integration/routes.crud.integration.test.ts +233 -1
- package/tests/integration/routes.custom.integration.test.ts +119 -0
- package/tests/integration/routes.foreign.integration.test.ts +3 -2
- package/tests/integration/routes.importExport.integration.test.ts +308 -6
- package/tests/integration/routes.integration.test.ts +4 -0
- package/tests/integration/routes.records.integration.test.ts +82 -6
- package/tests/integration/routes.upload.integration.test.ts +123 -0
- package/tests/integration/userFilters.integration.test.ts +225 -0
- package/tsconfig.json +1 -1
- package/tsconfig.strict.json +45 -0
- package/dist/front/ckeditor5-build/src/ckeditor.d.ts +0 -43
- package/dist/front/ckeditor5-build/src/ckeditor.js +0 -130
- package/dist/front/ckeditor5-build/src/ckeditor.js.map +0 -1
- package/dist/tsconfig.tsbuildinfo +0 -1
- package/src/front/ckeditor5-build/node_modules/.bin/semver +0 -12
- package/src/front/ckeditor5-build/node_modules/.bin/semver.cmd +0 -17
- 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
|
+
}
|