@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
@@ -0,0 +1,194 @@
1
+ import { quoteIdentifier } from "../utils/dbManager";
2
+ import type dbManager from "../utils/dbManager";
3
+ import type { DatabaseRow } from "../utils/dbManager";
4
+ import type { BaseTypeColumn, Filter, StandardColumn } from "../types/config";
5
+
6
+ type Condition = string | Filter[];
7
+ type RecordQueryParams = {
8
+ table: string;
9
+ page: string | number;
10
+ limit: string | number;
11
+ allRecords?: boolean;
12
+ filters?: Condition | null;
13
+ query?: string;
14
+ columns?: (Pick<BaseTypeColumn, "searchable" | "searchableName"> & StandardColumn)[];
15
+ customQuery?: string;
16
+ selectExpression?: string;
17
+ join?: string;
18
+ groupBy?: string;
19
+ having?: string;
20
+ orderBy?: string | null;
21
+ primaryId?: string;
22
+ };
23
+ type RecordQueryResult = {
24
+ rows: DatabaseRow[];
25
+ queryString: string;
26
+ input: { query: string; params: RecordQueryParams };
27
+ count: NonNullable<DatabaseRow[string]> | undefined;
28
+ error?: never;
29
+ } | {
30
+ error: Error;
31
+ rows?: never;
32
+ queryString?: never;
33
+ input?: never;
34
+ count?: never;
35
+ };
36
+
37
+ /*
38
+ #PORTING-EDIT
39
+ Reject absent rows, mutation headers, and nested result sets where record queries require row arrays.
40
+ */
41
+ function readRecordRows(rows: Awaited<ReturnType<dbManager["query"]>>["rows"]): DatabaseRow[] {
42
+ if (!Array.isArray(rows) || !rows.every((row): row is DatabaseRow => !Array.isArray(row))) {
43
+ throw new TypeError("Invalid record query rows");
44
+ }
45
+ return rows;
46
+ }
47
+
48
+ export function sqlAnd(conditions: (Condition | null | undefined)[]) {
49
+ const existingConditions: Condition[] = [];
50
+ for (const condition of conditions) {
51
+ if (condition && condition.length > 0) {
52
+ existingConditions.push(condition);
53
+ }
54
+ }
55
+ if (existingConditions.length <= 1) {
56
+ return existingConditions[0] || null;
57
+ } else {
58
+ let result = "";
59
+ for (let i = 0; i < existingConditions.length; i++) {
60
+ if (i > 0) {
61
+ result += " AND ";
62
+ }
63
+ result += "(" + existingConditions[i] + ")";
64
+ }
65
+ return result;
66
+ }
67
+ }
68
+
69
+ // Collect every SQL template declared anywhere in the (role-filtered) config:
70
+ // panel `query`, `autocompleteListQuery`, etc. customQuery may only run a
71
+ // template that already exists in the config the caller is allowed to see —
72
+ // so it can never be turned into an arbitrary read/write primitive.
73
+ export function collectAllowedQueries(node: unknown, acc = new Set<string>()): Set<string> {
74
+ if (!node || typeof node !== "object") return acc;
75
+ if (Array.isArray(node)) {
76
+ for (const item of node) collectAllowedQueries(item, acc);
77
+ return acc;
78
+ }
79
+ for (const key of Object.keys(node)) {
80
+ const value: unknown = Reflect.get(node, key);
81
+ if (typeof value === "string" && (key === "query" || /Query$/.test(key))) {
82
+ acc.add(value.trim());
83
+ } else if (value && typeof value === "object") {
84
+ collectAllowedQueries(value, acc);
85
+ }
86
+ }
87
+ return acc;
88
+ }
89
+
90
+ export default (db: Pick<dbManager, "query" | "dbType">) => {
91
+ async function getRecords(params: RecordQueryParams, computeCount = false): Promise<RecordQueryResult> {
92
+ let queryString = "";
93
+ if (params.filters) {
94
+ queryString = " WHERE (" + params.filters + ")";
95
+ }
96
+ if (params.query) {
97
+ const columns = params.columns ?? [];
98
+ let isNewCondition = true;
99
+ for (let i in columns) {
100
+ if (
101
+ columns[i].searchable !== false &&
102
+ (columns[i].searchableName || columns[i].name)
103
+ ) {
104
+ if (queryString.length == 0) {
105
+ queryString += " WHERE (";
106
+ } else if (isNewCondition) {
107
+ queryString += " AND (";
108
+ } else {
109
+ queryString += " OR ";
110
+ }
111
+ if (columns[i].searchableName) {
112
+ // on ne doit pas ajouter de backticks sur le searchableName car celui-ci peut être une fonction
113
+ // exemple : searchableName = "CONCAT_WS(' ', firstname, lastname)"
114
+ queryString += columns[i].searchableName;
115
+ } else if (columns[i].name) {
116
+ queryString += columns[i].name
117
+ .split(".")
118
+ .map((x) => quoteIdentifier(x, db.dbType ?? ""))
119
+ .join(".");
120
+ }
121
+ // SECURITY: escape the user query per dialect — PostgreSQL does not
122
+ // treat backslashes as escapes inside standard strings, so a `\'`
123
+ // would terminate the literal early.
124
+ const searchEscaped =
125
+ db.dbType === "POSTGRESQL"
126
+ ? params.query.replace(/'/g, "''")
127
+ : params.query.replace(/'/g, "\\'");
128
+ queryString += " LIKE '%" + searchEscaped + "%'";
129
+ isNewCondition = false;
130
+ }
131
+ }
132
+ queryString += ")";
133
+ }
134
+ const limit = params.allRecords ? 999999 : parseInt(String(params.limit));
135
+ const query =
136
+ params.customQuery ||
137
+ `SELECT
138
+ ${params.selectExpression || "*"}
139
+ FROM ${params.table}
140
+ ${params.join || ""}
141
+ ${queryString}
142
+ ${params.groupBy ? "GROUP BY " + params.groupBy : ""}
143
+ ${params.having ? " HAVING " + params.having : ""}
144
+ ${params.orderBy || params.primaryId ? " ORDER BY " + (params.orderBy || `${params.table}.${params.primaryId}`) : ""}`;
145
+ const { err, rows } = await db.query(
146
+ `${query} LIMIT ${limit} OFFSET ${Math.max((Number(params.page) - 1) * limit, 0)}`,
147
+ );
148
+ if (err) {
149
+ console.error(err);
150
+ return {
151
+ error: err,
152
+ };
153
+ }
154
+ const records = readRecordRows(rows);
155
+ let count: NonNullable<DatabaseRow[string]> | undefined = undefined;
156
+ if (computeCount) {
157
+ const { err: countErr, rows: countRows } = await db.query(
158
+ `SELECT COUNT(*) AS count FROM (${query}) s`,
159
+ );
160
+ if (countErr) {
161
+ const { rows: allRows } = await db.query(query);
162
+ count = readRecordRows(allRows).length;
163
+ } else {
164
+ count = countRows === undefined ? 0 : readRecordRows(countRows)[0]?.count || 0;
165
+ }
166
+ }
167
+
168
+ return {
169
+ rows: records,
170
+ queryString,
171
+ input: {
172
+ query,
173
+ params,
174
+ },
175
+ count,
176
+ };
177
+ }
178
+
179
+ async function findRecordByPosition(params: Omit<RecordQueryParams, "page" | "limit"> & {
180
+ position: string | number;
181
+ offset: string | number;
182
+ page?: string | number;
183
+ limit?: string | number;
184
+ }) {
185
+ return await getRecords(
186
+ Object.assign(params, {
187
+ page: parseInt(String(params.position)) + parseInt(String(params.offset)),
188
+ limit: 1,
189
+ }),
190
+ );
191
+ }
192
+
193
+ return { getRecords, findRecordByPosition };
194
+ };
@@ -0,0 +1,31 @@
1
+ import type {
2
+ Column, Config, CreateConfig, ForeignDatalistColumn, StandardColumn,
3
+ Subview, TablePanel, TableView, View, ViewPanel,
4
+ } from "../types/config";
5
+
6
+ export type { RequestHandler } from "express";
7
+
8
+ type Identified = { __id__?: string };
9
+ type RequestColumn = Column & StandardColumn & Identified
10
+ & Partial<Omit<ForeignDatalistColumn, "dataType">>;
11
+ type RequestPanel = ViewPanel & Identified
12
+ & Partial<Omit<TablePanel, "type" | "createConfig">> & {
13
+ columns?: RequestColumn[];
14
+ createConfig?: CreateConfig & { tableName?: string; columns: RequestColumn[] };
15
+ };
16
+ type RequestView = View & Identified
17
+ & Partial<Omit<TableView, "type" | "columns" | "recordViewPanels" | "subviews">> & {
18
+ columns?: RequestColumn[];
19
+ __hiddenColumns?: string[];
20
+ recordViewPanels?: RequestPanel[];
21
+ subviews?: (Subview & Identified)[];
22
+ };
23
+ type RequestConfig = Omit<Config, "views"> & { views: RequestView[] };
24
+
25
+ declare global {
26
+ namespace Express {
27
+ interface Request {
28
+ config?: RequestConfig;
29
+ }
30
+ }
31
+ }
@@ -0,0 +1,20 @@
1
+ import type { RequestHandler } from "../request";
2
+ import type createRecordQueries from "../queries";
3
+ import type { DatabaseRow } from "../../utils/dbManager";
4
+
5
+ type FindRecordByPosition = ReturnType<typeof createRecordQueries>["findRecordByPosition"];
6
+ type RequestBody = Parameters<FindRecordByPosition>[0];
7
+ type ResponseBody = Error | { recordId: DatabaseRow[string] | undefined };
8
+
9
+ export default (findRecordByPosition: FindRecordByPosition): RequestHandler<Record<string, string>, ResponseBody, RequestBody> =>
10
+ async function (req, res, next) {
11
+ const { rows, error } = await findRecordByPosition(req.body);
12
+ if (error) {
13
+ return res.status(500).send(error);
14
+ }
15
+ // JavaScript coerces an omitted primaryId to the property key "undefined".
16
+ const recordId = rows[0] ? rows[0][req.body.primaryId as string] : null;
17
+ res.send({
18
+ recordId: recordId,
19
+ });
20
+ };
@@ -0,0 +1,15 @@
1
+ import type { RequestHandler } from "../request";
2
+ import * as serializeJs from "serialize-javascript";
3
+
4
+ export default (): RequestHandler =>
5
+ async (req, res) => {
6
+ try {
7
+ const { mysql, auth, sessionSecret, ...safeConfig } = req.config!;
8
+ res.send({
9
+ config: serializeJs(safeConfig),
10
+ });
11
+ } catch (e) {
12
+ console.error(e);
13
+ res.sendStatus(500);
14
+ }
15
+ };
@@ -0,0 +1,131 @@
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 { v4 as uuidv4 } from "uuid";
6
+ import { quoteIdentifier } from "../../utils/dbManager";
7
+ import { areSafeColumnNames, touchesHiddenColumn } from "../authorization";
8
+
9
+ type RequestBody = {
10
+ viewIndex: string | number;
11
+ panelIndex?: string | number | null;
12
+ recordId?: DatabaseRow[string];
13
+ data: Partial<DatabaseRow>;
14
+ };
15
+ type ResponseBody =
16
+ | { id: number | undefined }
17
+ | { err: unknown; message: string }
18
+ | { error: string };
19
+
20
+ export default (db: DbManager): RequestHandler<
21
+ Record<string, string>,
22
+ ResponseBody,
23
+ RequestBody
24
+ > =>
25
+ async function (req, res, next) {
26
+ const view = req.config!.views[req.body.viewIndex as number];
27
+ // panelIndex arrives as a string from the front-end but a numeric caller
28
+ // may send 0 — only null/undefined mean "no panel".
29
+ const hasPanel =
30
+ req.body.panelIndex !== undefined && req.body.panelIndex !== null;
31
+ const panel = hasPanel
32
+ ? view.recordViewPanels![req.body.panelIndex as number]
33
+ : null;
34
+ if (!hasPanel && touchesHiddenColumn(view, req.body.data)) {
35
+ return res.sendStatus(403);
36
+ }
37
+ try {
38
+ for (let key in req.body.data) {
39
+ if (
40
+ req.body.data[key] == "NULL" ||
41
+ req.body.data[key] === "Invalid date"
42
+ ) {
43
+ req.body.data[key] = null;
44
+ }
45
+ }
46
+ if (panel) {
47
+ req.body.data[panel.createConfig!.recordIdKey] = req.body.recordId;
48
+ }
49
+ const primaryColumn = (view || panel)?.columns?.find(
50
+ (x) => x.name === (view || panel).primaryId,
51
+ );
52
+ if (primaryColumn?.dataType == "uuid") {
53
+ req.body.data[String(primaryColumn.name)] = uuidv4();
54
+ }
55
+ const createPayload = JSON.parse(
56
+ JSON.stringify(req.body.data),
57
+ ) as Partial<DatabaseRow>;
58
+ const columns = panel ? panel.createConfig!.columns : view.columns;
59
+ for (let key in createPayload) {
60
+ if (
61
+ columns?.find((x) => x.name === key)?.includedInCreatePayload ===
62
+ false
63
+ ) {
64
+ delete createPayload[key];
65
+ }
66
+ }
67
+ if (!areSafeColumnNames(createPayload)) {
68
+ return res.status(400).send({ error: "Invalid column name" });
69
+ }
70
+ const queryExpression = Object.keys(createPayload)
71
+ .map((x) => quoteIdentifier(x, db.dbType!))
72
+ .join(", ");
73
+ const queryPlaceholders = Object.keys(createPayload)
74
+ .map((x) => "?")
75
+ .join(", ");
76
+ const queryParameters = Object.keys(createPayload).map(
77
+ (x) => createPayload[x],
78
+ );
79
+ const { err, rows: r } = await db.query(
80
+ `INSERT INTO ?? (${queryExpression}) VALUES(${queryPlaceholders})`,
81
+ [panel ? panel.tableName : view.tableName, ...queryParameters],
82
+ );
83
+ if (err) {
84
+ console.error(err);
85
+ return res.status(500).send({
86
+ err,
87
+ message:
88
+ "code" in err && err.code === "ER_DUP_ENTRY"
89
+ ? "Cet élément existe déjà."
90
+ : "Une erreur est survenue.",
91
+ });
92
+ }
93
+ if (panel ? panel.createCallback : view.createCallback) {
94
+ try {
95
+ await axios.post(
96
+ panel ? panel.createCallback! : view.createCallback!,
97
+ {
98
+ id: Array.isArray(r) ? undefined : r!.insertId,
99
+ data: req.body.data,
100
+ },
101
+ );
102
+ } catch (e) {}
103
+ }
104
+ if (panel ? panel.createCallbackFunction : view.createCallbackFunction) {
105
+ // The insert is already committed: a failing callback must not turn
106
+ // into a 500 that invites a client retry (which would duplicate the
107
+ // row). Log and answer 200.
108
+ try {
109
+ await (
110
+ panel
111
+ ? panel.createCallbackFunction!
112
+ : view.createCallbackFunction!
113
+ )({
114
+ id: (Array.isArray(r) ? undefined : r!.insertId) as number, // TODO https://github.com/galadrimteam/galadmin/issues/45: PostgreSQL supplies undefined here.
115
+ data: req.body.data,
116
+ });
117
+ } catch (callbackErr) {
118
+ console.error(callbackErr);
119
+ }
120
+ }
121
+ res.send({
122
+ id: Array.isArray(r) ? undefined : r!.insertId,
123
+ });
124
+ } catch (err) {
125
+ console.error(err);
126
+ res.status(500).send({
127
+ err,
128
+ message: "Une erreur est survenue.",
129
+ });
130
+ }
131
+ };
@@ -0,0 +1,60 @@
1
+ import type { RequestHandler } from "../request";
2
+ import type DbManager from "../../utils/dbManager";
3
+ import type { DatabaseRow } from "../../utils/dbManager";
4
+
5
+ type RequestBody = {
6
+ viewIndex: string | number;
7
+ actionIndex: string | number;
8
+ selectedRecordIds: DatabaseRow[string][];
9
+ };
10
+ type ResponseBody =
11
+ | { success: true; message: string | undefined }
12
+ | { success: false; error: unknown };
13
+ type ActionUser = { userId?: DatabaseRow[string] };
14
+ type QueryRows = NonNullable<Awaited<ReturnType<DbManager["query"]>>["rows"]>;
15
+ type IndexedRows = {
16
+ [index: number]: Extract<QueryRows, object[]>[number] | undefined;
17
+ };
18
+
19
+ export default (db: DbManager): RequestHandler<
20
+ Record<string, string>,
21
+ ResponseBody,
22
+ RequestBody
23
+ > =>
24
+ async function (req, res, next) {
25
+ const view = req.config!.views[req.body.viewIndex as number];
26
+ const action = view.customActions![req.body.actionIndex as number];
27
+ for (const recordId of req.body.selectedRecordIds) {
28
+ try {
29
+ const { rows } = await db.query(
30
+ "SELECT " +
31
+ (view.selectExpression || "*") +
32
+ " FROM ?? " +
33
+ (view.join || "") +
34
+ " WHERE ?? = ?",
35
+ [view.tableName, view.tableName + "." + view.primaryId, recordId],
36
+ );
37
+ const record = (rows as IndexedRows)[0];
38
+ const actionResult = await action.action(
39
+ record,
40
+ (req.session!.user as ActionUser).userId,
41
+ );
42
+ if (actionResult) {
43
+ return res.send({
44
+ success: true,
45
+ message: actionResult,
46
+ });
47
+ }
48
+ } catch (err) {
49
+ console.error(err);
50
+ return res.send({
51
+ success: false,
52
+ error: err,
53
+ });
54
+ }
55
+ }
56
+ res.send({
57
+ success: true,
58
+ message: action.successMessage,
59
+ });
60
+ };
@@ -0,0 +1,44 @@
1
+ import type { RequestHandler } from "../request";
2
+ import * as fs from "fs";
3
+ import * as path from "path";
4
+ import * as rollup from "rollup";
5
+ import babel from "@rollup/plugin-babel";
6
+ import resolve from "@rollup/plugin-node-resolve";
7
+
8
+ export default (__dirname: string): RequestHandler<Record<string, string>, string> =>
9
+ async (req, res) => {
10
+ try {
11
+ const pkg: unknown = JSON.parse(
12
+ fs.readFileSync(
13
+ path.join(__dirname, "../src/front/package.json"),
14
+ "utf-8",
15
+ ),
16
+ );
17
+ const external = Object.keys(
18
+ (pkg as { dependencies?: unknown }).dependencies || {},
19
+ );
20
+ const bundle = await rollup.rollup({
21
+ input: path.join(
22
+ __dirname,
23
+ "../../",
24
+ req.query.componentPath as string,
25
+ ),
26
+ plugins: [
27
+ resolve(),
28
+ babel({
29
+ configFile: path.join(__dirname, "../babel.config"),
30
+ babelHelpers: "bundled",
31
+ }),
32
+ ],
33
+ external,
34
+ });
35
+ const { output } = await bundle.generate({
36
+ format: "cjs",
37
+ exports: "auto",
38
+ });
39
+ res.send(output[0].code);
40
+ } catch (err) {
41
+ console.error(err);
42
+ res.sendStatus(500);
43
+ }
44
+ };
@@ -0,0 +1,53 @@
1
+ import type { RequestHandler } from "../request";
2
+ import { collectAllowedQueries } from "../queries";
3
+ import type DbManager from "../../utils/dbManager";
4
+ import type { DatabaseRow } from "../../utils/dbManager";
5
+
6
+ type QueryRows = NonNullable<Awaited<ReturnType<DbManager["query"]>>["rows"]>;
7
+ type IterableRows = Extract<QueryRows, object[]>;
8
+ type ResponseBody =
9
+ | Awaited<ReturnType<DbManager["query"]>>["rows"]
10
+ | Error
11
+ | { error: string };
12
+
13
+ export default (db: DbManager): RequestHandler<
14
+ Record<string, string>,
15
+ ResponseBody
16
+ > =>
17
+ async function (req, res, next) {
18
+ const template =
19
+ typeof req.query.query === "string" ? req.query.query : "";
20
+ // SECURITY: only run whitelisted, read-only templates, and bind the record id
21
+ // as a parameter instead of substituting it into the SQL string.
22
+ const allowed = collectAllowedQueries(req.config!.views);
23
+ if (!allowed.has(template.trim())) {
24
+ return res.status(403).send({ error: "Query not allowed" });
25
+ }
26
+ if (!/^\s*\(?\s*(select|with)\b/i.test(template) || /;/.test(template)) {
27
+ return res
28
+ .status(400)
29
+ .send({ error: "Only read-only queries are allowed" });
30
+ }
31
+ const placeholderCount = (template.match(/\?/g) || []).length;
32
+ const params = new Array<typeof req.query.primaryValue>(placeholderCount).fill(
33
+ req.query.primaryValue,
34
+ );
35
+ const { err, rows } = await db.query(template, params);
36
+ if (err) {
37
+ return res.status(500).send(err);
38
+ }
39
+ if (req.query.viewIndex && req.query.panelIndex) {
40
+ const panel =
41
+ req.config!.views[req.query.viewIndex as `${number}`].recordViewPanels![
42
+ req.query.panelIndex as `${number}`
43
+ ];
44
+ if (panel.formatValue) {
45
+ for (const row of rows as IterableRows) {
46
+ for (const key in panel.formatValue) {
47
+ (row as DatabaseRow)[key] = await panel.formatValue[key](row);
48
+ }
49
+ }
50
+ }
51
+ }
52
+ res.send(rows);
53
+ };
@@ -0,0 +1,59 @@
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
+
6
+ type RequestBody = {
7
+ viewIndex: string | number;
8
+ primaryValue: DatabaseRow[string];
9
+ };
10
+
11
+ export default (db: DbManager): RequestHandler<
12
+ Record<string, string>,
13
+ string | Error,
14
+ RequestBody
15
+ > =>
16
+ async function (req, res) {
17
+ const view = req.config!.views[req.body.viewIndex as number];
18
+ const { rows, err: err2 } = await db.query(
19
+ "SELECT " +
20
+ (view.selectExpression || "*") +
21
+ " FROM ?? " +
22
+ (view.join || "") +
23
+ " WHERE ?? = ?",
24
+ [
25
+ view.tableName,
26
+ view.tableName + "." + view.primaryId,
27
+ req.body.primaryValue,
28
+ ],
29
+ );
30
+ if (err2) {
31
+ return res.status(500).send(err2);
32
+ }
33
+ const deletedRecord = (rows as DatabaseRow[])[0];
34
+ if (!deletedRecord) {
35
+ return res.status(404).send("Record not found");
36
+ }
37
+ const { err } = await db.query("DELETE FROM ?? WHERE ?? = ?", [
38
+ view.tableName,
39
+ view.primaryId,
40
+ req.body.primaryValue,
41
+ ]);
42
+ if (err) {
43
+ return res.status(500).send(err);
44
+ }
45
+ if (view.deleteCallback) {
46
+ try {
47
+ await axios.post(view.deleteCallback, {
48
+ id: req.body.primaryValue,
49
+ });
50
+ } catch (e) {}
51
+ }
52
+ if (view.deleteCallbackFunction) {
53
+ await view.deleteCallbackFunction({
54
+ id: req.body.primaryValue as number, // TODO https://github.com/galadrimteam/galadmin/issues/45: string and UUID keys are also supplied here.
55
+ deletedRecord,
56
+ });
57
+ }
58
+ res.sendStatus(200);
59
+ };
@@ -0,0 +1,58 @@
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
+
6
+ type RequestBody = {
7
+ viewIndex: string | number;
8
+ panelId: string;
9
+ primaryValue: DatabaseRow[string];
10
+ };
11
+ type DeleteCallbackPayload = {
12
+ id: number;
13
+ deletedRecord: DatabaseRow | undefined;
14
+ };
15
+
16
+ export default (db: DbManager): RequestHandler<
17
+ Record<string, string>,
18
+ string | Error,
19
+ RequestBody
20
+ > =>
21
+ async function (req, res) {
22
+ const view = req.config!.views[req.body.viewIndex as number];
23
+ const panel = view.recordViewPanels!.find(
24
+ (x) => x.__id__ === req.body.panelId,
25
+ )!;
26
+ const { rows, err: err2 } = await db.query(
27
+ "SELECT * FROM ?? WHERE ?? = ?",
28
+ [panel.tableName, panel.primaryId, req.body.primaryValue],
29
+ );
30
+ if (err2) {
31
+ return res.status(500).send(err2);
32
+ }
33
+ const deletedRecord = (rows as (DatabaseRow | undefined)[])[0];
34
+ const { err } = await db.query("DELETE FROM ?? WHERE ?? = ?", [
35
+ panel.tableName,
36
+ panel.primaryId,
37
+ req.body.primaryValue,
38
+ ]);
39
+ if (err) {
40
+ return res.status(500).send(err);
41
+ }
42
+ if (panel.deleteCallback) {
43
+ try {
44
+ await axios.post(panel.deleteCallback, {
45
+ id: req.body.primaryValue,
46
+ });
47
+ } catch (e) {}
48
+ }
49
+ if (panel.deleteCallbackFunction) {
50
+ await (panel.deleteCallbackFunction as (
51
+ payload: DeleteCallbackPayload,
52
+ ) => void)({
53
+ id: req.body.primaryValue as number, // TODO https://github.com/galadrimteam/galadmin/issues/45: string and UUID keys are also supplied here.
54
+ deletedRecord,
55
+ });
56
+ }
57
+ res.sendStatus(200);
58
+ };