@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,28 @@
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
+ primaryValues: DatabaseRow[string][];
8
+ };
9
+
10
+ export default (db: DbManager): RequestHandler<
11
+ Record<string, string>,
12
+ string | Error,
13
+ RequestBody
14
+ > =>
15
+ async function (req, res, next) {
16
+ const view = req.config!.views[req.body.viewIndex as number];
17
+ // SECURITY: bind the primary values as parameters instead of interpolating
18
+ // them into the IN() list (quote-bearing values could break out).
19
+ const placeholders = req.body.primaryValues.map(() => "?").join(", ");
20
+ const { err } = await db.query(
21
+ `DELETE FROM ?? WHERE ?? IN(${placeholders})`,
22
+ [view.tableName, view.primaryId, ...req.body.primaryValues],
23
+ );
24
+ if (err) {
25
+ return res.status(500).send(err);
26
+ }
27
+ res.sendStatus(200);
28
+ };
@@ -0,0 +1,73 @@
1
+ import type { RequestHandler } from "../request";
2
+ import { writableTables } from "../authorization";
3
+ import type DbManager from "../../utils/dbManager";
4
+ import type { DatabaseRow } from "../../utils/dbManager";
5
+
6
+ type RequestBody = {
7
+ table: string;
8
+ primaryValue: DatabaseRow[string];
9
+ duplicateConfig: {
10
+ primaryId: string;
11
+ fields: string[];
12
+ foreignTables: { table: string; fields: string[] }[];
13
+ };
14
+ };
15
+
16
+ export default (db: DbManager): RequestHandler<
17
+ Record<string, string>,
18
+ string | Error | null,
19
+ RequestBody
20
+ > =>
21
+ async function (req, res, next) {
22
+ // SECURITY: bound the (client-supplied) target table to tables the role may write.
23
+ const allowedTables = writableTables(req);
24
+ const requestedTables = [
25
+ req.body.table,
26
+ ...(req.body.duplicateConfig?.foreignTables || []).map((t) => t.table),
27
+ ];
28
+ if (requestedTables.some((t) => !allowedTables.has(t))) {
29
+ return res.sendStatus(403);
30
+ }
31
+ const primaryId = req.body.duplicateConfig.primaryId;
32
+ const { err, rows } = await db.query("SELECT * FROM ?? WHERE ?? = ?", [
33
+ req.body.table,
34
+ primaryId,
35
+ req.body.primaryValue,
36
+ ]);
37
+ if (err) {
38
+ return res.status(500).send(err);
39
+ }
40
+ const record = (rows as Partial<DatabaseRow>[])[0];
41
+ const data: Partial<DatabaseRow> = {};
42
+ for (let field of req.body.duplicateConfig.fields) {
43
+ data[field] = record[field];
44
+ }
45
+ const { err: err2, rows: r } = await db.query(
46
+ "INSERT IGNORE INTO ?? SET ?",
47
+ [req.body.table, data],
48
+ );
49
+ if (err2) {
50
+ return res.status(500).send(err);
51
+ }
52
+ const newRecordId = (r as { insertId?: number }).insertId;
53
+ for (let foreignTable of req.body.duplicateConfig.foreignTables) {
54
+ const { err, rows } = await db.query("SELECT * FROM ?? WHERE ?? = ?", [
55
+ foreignTable.table,
56
+ primaryId,
57
+ req.body.primaryValue,
58
+ ]);
59
+ if (err) console.log(err);
60
+ for (let record of rows as Partial<DatabaseRow>[]) {
61
+ const data: Partial<DatabaseRow> = {};
62
+ data[primaryId] = newRecordId;
63
+ for (let field of foreignTable.fields) {
64
+ data[field] = record[field];
65
+ }
66
+ await db.query("INSERT IGNORE INTO ?? SET ?", [
67
+ foreignTable.table,
68
+ data,
69
+ ]);
70
+ }
71
+ }
72
+ res.sendStatus(200);
73
+ };
@@ -0,0 +1,80 @@
1
+ import type { RequestHandler } from "../request";
2
+ import { evalExpression } from "../values";
3
+ import { sqlAnd } from "../queries";
4
+ import type createRecordQueries from "../queries";
5
+ import type DbManager from "../../utils/dbManager";
6
+ import { filterUserFromSession } from "../filterUser";
7
+
8
+ type ForeignSelectRecordsRequestBody = {
9
+ viewIndex: string | number;
10
+ columnId: string;
11
+ query?: string;
12
+ };
13
+
14
+ export default (db: DbManager, getRecords: ReturnType<typeof createRecordQueries>["getRecords"]): RequestHandler<Record<string, string>, object, ForeignSelectRecordsRequestBody> =>
15
+ async (req, res, next) => {
16
+ const view = req.config!.views[req.body.viewIndex as number];
17
+ const column = [
18
+ ...view.columns!,
19
+ ...(view.recordViewPanels
20
+ ?.map((x) => x?.createConfig?.columns)
21
+ ?.filter((x) => x)
22
+ ?.flat() || []),
23
+ ].find((x) => x!.__id__ === req.body.columnId)!;
24
+ const queryEscaped =
25
+ db.dbType === "POSTGRESQL"
26
+ ? req.body.query?.replace(/'/g, "''")
27
+ : req.body.query?.replace(/'/g, "\\'");
28
+ const queryStart = {
29
+ table: column.table!,
30
+ page: 1,
31
+ };
32
+ const filters: Parameters<typeof sqlAnd>[0] = [
33
+ column.filters,
34
+ req.body.query
35
+ ? `${column.searchableName} LIKE '${column.wholeSearch ? "%" : ""}${queryEscaped}%'`
36
+ : null,
37
+ ];
38
+ /*
39
+ #PORTING-EDIT
40
+ Pass a checked FilterUser to the configured callback and return 401 before querying when its session identity is invalid.
41
+ */
42
+ if (column.filtersFromUser) {
43
+ const user = filterUserFromSession(req.session?.user);
44
+ if (user === undefined) return res.sendStatus(401);
45
+ filters.push(column.filtersFromUser(user));
46
+ } else {
47
+ filters.push(null);
48
+ }
49
+ const { rows: records, error } = await getRecords({
50
+ ...queryStart,
51
+ filters: sqlAnd(filters),
52
+ join: column.join,
53
+ selectExpression: column.selectExpression,
54
+ orderBy: column.orderBy,
55
+ limit: column.limit || 1000,
56
+ });
57
+ if (error) {
58
+ return res.status(500).send(error);
59
+ }
60
+ const options = await Promise.all(
61
+ records.map(async (record) => {
62
+ let label;
63
+ if (column.formatForeignValue) {
64
+ label = await column.formatForeignValue(record);
65
+ } else if (column.formatValue) {
66
+ label = await column.formatValue(record);
67
+ } else if (column.formattedValue) {
68
+ label = evalExpression(column.formattedValue, record);
69
+ } else {
70
+ label = record[column.name!];
71
+ }
72
+ const value = record[column.foreignName!] || record[column.name!];
73
+ return {
74
+ value,
75
+ label,
76
+ };
77
+ }),
78
+ );
79
+ res.send(options);
80
+ };
@@ -0,0 +1,47 @@
1
+ import type { RequestHandler } from "../request";
2
+ import type DbManager from "../../utils/dbManager";
3
+ import type { DatabaseRow } from "../../utils/dbManager";
4
+ import type { BaseTypeView } from "../../types/config";
5
+
6
+ type BadgeTarget = Pick<BaseTypeView, "notificationBadge"> & { __id__?: string };
7
+ type BadgeRows = { length?: number; [index: number]: Partial<DatabaseRow> };
8
+ type NotificationBadges = Record<string, DatabaseRow[string] | undefined>;
9
+
10
+ export default (db: DbManager): RequestHandler<
11
+ Record<string, string>,
12
+ NotificationBadges
13
+ > =>
14
+ async function (req, res) {
15
+ const notificationBadges: NotificationBadges = {};
16
+
17
+ async function checkAndGetNotificationBadge(obj: BadgeTarget) {
18
+ if (obj.notificationBadge) {
19
+ const { err, rows } = await db.query(obj.notificationBadge.query);
20
+ if (err) {
21
+ console.error(err);
22
+ } else if (
23
+ ((rows as BadgeRows).length as number) > 0 &&
24
+ (rows as BadgeRows)[0][obj.notificationBadge.attribute]
25
+ ) {
26
+ const notificationBadge =
27
+ (rows as BadgeRows)[0][obj.notificationBadge.attribute];
28
+ if (
29
+ !obj.notificationBadge.displayCondition ||
30
+ obj.notificationBadge.displayCondition(notificationBadge)
31
+ ) {
32
+ notificationBadges[obj.__id__ as string] = notificationBadge;
33
+ }
34
+ }
35
+ }
36
+ }
37
+
38
+ for (const view of req.config!.views) {
39
+ await checkAndGetNotificationBadge(view);
40
+ if (view.subviews) {
41
+ for (const subview of view.subviews) {
42
+ await checkAndGetNotificationBadge(subview);
43
+ }
44
+ }
45
+ }
46
+ res.send(notificationBadges);
47
+ };
@@ -0,0 +1,48 @@
1
+ import { stripHiddenColumns } from "../authorization";
2
+ import type { RequestHandler } from "../request";
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 QueryElement = Extract<QueryRows, object[]>[number];
8
+ type IndexedRows = {
9
+ length?: number;
10
+ [index: number]: QueryElement | undefined;
11
+ };
12
+ type ResponseBody = QueryElement | undefined | string | Error;
13
+
14
+ export default (db: DbManager): RequestHandler<
15
+ Record<string, string>,
16
+ ResponseBody
17
+ > =>
18
+ async function (req, res) {
19
+ const view = req.config!.views[req.query.viewIndex as `${number}`];
20
+ const { err, rows } = await db.query(
21
+ "SELECT " +
22
+ (view.selectExpression || "*") +
23
+ " FROM ?? " +
24
+ (view.join || "") +
25
+ " WHERE ?? = ?",
26
+ [
27
+ view.tableName,
28
+ view.tableName + "." + view.primaryId,
29
+ req.query.recordId,
30
+ ],
31
+ );
32
+ if (err) {
33
+ return res.status(500).send(err);
34
+ }
35
+ if (((rows as IndexedRows).length as number) > 0) {
36
+ const record = (rows as IndexedRows)[0];
37
+ for (const column of view.columns!) {
38
+ if (column.formatValue) {
39
+ (record as DatabaseRow)[`__formatValue-${column.__id__}__`] =
40
+ await column.formatValue(record);
41
+ }
42
+ }
43
+ // @ts-expect-error TODO: remove this annotation in the related porting pass
44
+ res.send(stripHiddenColumns(view, record));
45
+ } else {
46
+ res.sendStatus(404);
47
+ }
48
+ };
@@ -0,0 +1,91 @@
1
+ import { stripHiddenColumns } from "../authorization";
2
+ import type { RequestHandler } from "../request";
3
+ import { sqlAnd } from "../queries";
4
+ import type createRecordQueries from "../queries";
5
+ import { filterUserFromSession } from "../filterUser";
6
+
7
+ type RecordsRequestBody = {
8
+ viewIndex: string | number;
9
+ subviewIndex?: string | number;
10
+ isDesc?: boolean | null;
11
+ orderBy?: string;
12
+ page: string | number;
13
+ query?: string;
14
+ filters: Parameters<typeof sqlAnd>[0];
15
+ };
16
+
17
+ export default (getRecords: ReturnType<typeof createRecordQueries>["getRecords"]): RequestHandler<Record<string, string>, object, RecordsRequestBody> =>
18
+ async function (req, res, next) {
19
+ const view = req.config!.views[req.body.viewIndex as number];
20
+ const isDesc = req.body.isDesc === null ? view.isDesc : req.body.isDesc;
21
+ const subview =
22
+ view.subviews && req.body.subviewIndex !== undefined
23
+ ? view.subviews[req.body.subviewIndex as number]
24
+ : null;
25
+ const orderBy = req.body.orderBy || subview?.orderBy || view.orderBy;
26
+ const queryStart = {
27
+ table: view.tableName!,
28
+ page: req.body.page,
29
+ };
30
+ const filters: Parameters<typeof sqlAnd>[0] = [
31
+ view.filters,
32
+ subview?.filters,
33
+ ...req.body.filters,
34
+ ];
35
+ /*
36
+ #PORTING-EDIT
37
+ Pass a checked FilterUser to the configured callback and return 401 before querying when its session identity is invalid.
38
+ */
39
+ if (view.filtersFromUser) {
40
+ const user = filterUserFromSession(req.session?.user);
41
+ if (user === undefined) return res.sendStatus(401);
42
+ filters.push(view.filtersFromUser(user));
43
+ } else {
44
+ filters.push(null);
45
+ }
46
+ const {
47
+ rows: records,
48
+ input,
49
+ error,
50
+ count,
51
+ } = await getRecords(
52
+ {
53
+ ...queryStart,
54
+ filters: sqlAnd(filters),
55
+ having: subview?.having,
56
+ customQuery: view.customQuery,
57
+ join: view.join,
58
+ selectExpression: view.selectExpression,
59
+ orderBy: [
60
+ orderBy ? `${orderBy} ${isDesc ? "DESC" : "ASC"}` : null,
61
+ view.primaryId,
62
+ ]
63
+ .filter((x) => x)
64
+ .join(", "),
65
+ primaryId: view.primaryId,
66
+ query: req.body.query,
67
+ groupBy: view.groupBy,
68
+ columns: view.columns,
69
+ limit: view.limit || 10,
70
+ },
71
+ true,
72
+ );
73
+ if (error) {
74
+ return res.status(500).send(error);
75
+ }
76
+
77
+ for (const column of view.columns!) {
78
+ if (column.formatValue) {
79
+ for (const record of records) {
80
+ record[`__formatValue-${column.__id__}__`] =
81
+ await column.formatValue(record);
82
+ }
83
+ }
84
+ }
85
+ for (const record of records) stripHiddenColumns(view, record);
86
+ res.send({
87
+ records,
88
+ input,
89
+ count,
90
+ });
91
+ };
@@ -0,0 +1,92 @@
1
+ import type { RequestHandler } from "../request";
2
+ import { getFormattedValue } from "../values";
3
+ import { sqlAnd } from "../queries";
4
+ import type createRecordQueries from "../queries";
5
+ import { filterUserFromSession } from "../filterUser";
6
+
7
+ type RecordsExportRequestBody = {
8
+ viewIndex: string | number;
9
+ subviewIndex?: string | number;
10
+ isDesc?: boolean | null;
11
+ orderBy?: string;
12
+ query?: string;
13
+ filters: Parameters<typeof sqlAnd>[0][number];
14
+ };
15
+
16
+ export default (getRecords: ReturnType<typeof createRecordQueries>["getRecords"]): RequestHandler<Record<string, string>, object, RecordsExportRequestBody> =>
17
+ async function (req, res) {
18
+ const view = req.config!.views[req.body.viewIndex as number];
19
+ const orderBy = req.body.orderBy || view.orderBy;
20
+ const isDesc = req.body.isDesc === null ? view.isDesc : req.body.isDesc;
21
+ const subview =
22
+ view.subviews && req.body.subviewIndex !== undefined
23
+ ? view.subviews[req.body.subviewIndex as number]
24
+ : null;
25
+ const queryStart = {
26
+ table: view.tableName!,
27
+ page: 1,
28
+ allRecords: true,
29
+ };
30
+ const filters: Parameters<typeof sqlAnd>[0] = [
31
+ view.filters,
32
+ subview?.filters,
33
+ req.body.filters,
34
+ ];
35
+ // SECURITY: scope the export exactly like the list view, otherwise a
36
+ // user could download rows their filtersFromUser clause hides.
37
+ /*
38
+ #PORTING-EDIT
39
+ Pass a checked FilterUser to the configured callback and return 401 before querying when its session identity is invalid.
40
+ */
41
+ if (view.filtersFromUser) {
42
+ const user = filterUserFromSession(req.session?.user);
43
+ if (user === undefined) return res.sendStatus(401);
44
+ filters.push(view.filtersFromUser(user));
45
+ } else {
46
+ filters.push(null);
47
+ }
48
+ const { rows: records, error } = await getRecords({
49
+ ...queryStart,
50
+ filters: sqlAnd(filters),
51
+ having: subview?.having,
52
+ customQuery: view.customQuery,
53
+ join: view.join,
54
+ selectExpression: view.selectExpression,
55
+ orderBy: orderBy ? `${orderBy} ${isDesc ? "DESC" : "ASC"}` : null,
56
+ primaryId: view.primaryId,
57
+ query: req.body.query,
58
+ groupBy: view.groupBy,
59
+ columns: view.columns!,
60
+ limit: view.limit || 10,
61
+ });
62
+ if (error) {
63
+ return res.status(500).send(error);
64
+ }
65
+ let output = "";
66
+ const columns = view.columns!;
67
+ for (let j = 0; j < columns.length; j++) {
68
+ if (!columns[j].hideInTable || columns[j].includeInExport) {
69
+ if (j > 0) output += ";";
70
+ output += columns[j].label;
71
+ }
72
+ }
73
+ for (let i in records) {
74
+ const el = records[i];
75
+ output += "\r\n";
76
+ for (let j = 0; j < columns.length; j++) {
77
+ if (!columns[j].hideInTable || columns[j].includeInExport) {
78
+ if (j > 0) output += ";";
79
+ if (columns[j].dataType == "textarea") {
80
+ output += '"';
81
+ }
82
+ output += await getFormattedValue(columns[j], el);
83
+ if (columns[j].dataType == "textarea") {
84
+ output += '"';
85
+ }
86
+ }
87
+ }
88
+ }
89
+ res.send({
90
+ content: output,
91
+ });
92
+ };
@@ -0,0 +1,115 @@
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, hiddenColumnsOf } 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 in fields
17
+ const view = req.config!.views[fields.viewIndex![0] as `${number}`];
18
+ // SECURITY: the generic write-guard cannot see a viewIndex buried in a
19
+ // multipart body, so imports enforce the view's write roles here.
20
+ if (!view || !canWriteView(view, roleOf(req))) {
21
+ return res.sendStatus(403);
22
+ }
23
+ if (typeof view.parseImportFile !== "function") {
24
+ return res
25
+ .status(500)
26
+ .send(
27
+ '"parseImportFile" doit être défini dans la configuration de la vue.',
28
+ );
29
+ }
30
+
31
+ const records: unknown = await view.parseImportFile(files.data![0]);
32
+ /*
33
+ #PORTING-EDIT
34
+ The approved import shape check rejects non-object parser output before SQL insertion.
35
+ */
36
+ if (!isImportRecords(records)) {
37
+ return res.sendStatus(400);
38
+ }
39
+ const columns: string[] = [];
40
+ if (records.length === 0) {
41
+ return res.send({
42
+ affectedRows: 0,
43
+ });
44
+ }
45
+
46
+ try {
47
+ for (let record of records) {
48
+ for (let key in record) {
49
+ if (!columns.includes(key)) {
50
+ columns.push(key);
51
+ }
52
+ }
53
+ }
54
+
55
+ if (columns.some((name) => hiddenColumnsOf(view).includes(name))) {
56
+ return res.sendStatus(403);
57
+ }
58
+
59
+ let queryString;
60
+ if (db.dbType === "POSTGRESQL") {
61
+ queryString = `INSERT INTO ?? (${columns.map((x) => quoteIdentifier(x, db.dbType!)).join(", ")}) VALUES`;
62
+
63
+ for (let record of records) {
64
+ const valuesList = columns
65
+ .map((x) => formatValueForInsertion(record[x], db.dbType!))
66
+ .join(", ");
67
+ queryString += ` (${valuesList}),`;
68
+ }
69
+ queryString =
70
+ queryString.slice(0, -1) +
71
+ ` ON CONFLICT (${view.importFileUniqueField}) DO UPDATE SET `;
72
+
73
+ for (let column of columns) {
74
+ queryString += `${quoteIdentifier(column, db.dbType!)} = EXCLUDED.${quoteIdentifier(column, db.dbType!)},`;
75
+ }
76
+ } else {
77
+ queryString =
78
+ "INSERT INTO ?? (" +
79
+ columns.map((x) => quoteIdentifier(x, db.dbType!)).join(", ") +
80
+ ") VALUES";
81
+ for (let record of records) {
82
+ queryString +=
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
+ }
94
+ queryString = queryString.slice(0, -1); // Remove the trailing comma
95
+
96
+ const { err, rows: r } = await db.query(queryString, [view.tableName]);
97
+ if (err) {
98
+ console.error(err);
99
+ return res.status(500).send({
100
+ err,
101
+ message: "Une erreur est survenue.",
102
+ });
103
+ }
104
+ res.send({
105
+ affectedRows: (r as { affectedRows?: number }).affectedRows,
106
+ });
107
+ } catch (err) {
108
+ console.error(err);
109
+ res.status(500).send({
110
+ err,
111
+ message: "Une erreur est survenue.",
112
+ });
113
+ }
114
+ });
115
+ };
@@ -0,0 +1,96 @@
1
+ import type { RequestHandler } from "../request";
2
+ import axios from "axios";
3
+
4
+ type JsonValue = string | number | boolean | null | JsonValue[] | JsonObject;
5
+ type JsonObject = { [key: string]: JsonValue | undefined };
6
+ type CompanySuggestion = {
7
+ id: string;
8
+ companyName: JsonValue | undefined;
9
+ siret: JsonValue | undefined;
10
+ siren: JsonValue | undefined;
11
+ address: string;
12
+ city: JsonValue | undefined;
13
+ zipCode: JsonValue | undefined;
14
+ activityNumber: JsonValue | undefined;
15
+ };
16
+ type SearchQuery = string & {
17
+ replaceAll(search: string, replacement: string): string;
18
+ };
19
+
20
+ function isJsonValue(value: unknown): value is JsonValue {
21
+ const pending: unknown[] = [value];
22
+ while (pending.length > 0) {
23
+ const candidate = pending.pop();
24
+ if (
25
+ candidate === null ||
26
+ typeof candidate === "string" ||
27
+ typeof candidate === "number" ||
28
+ typeof candidate === "boolean"
29
+ ) {
30
+ continue;
31
+ }
32
+ if (typeof candidate !== "object") return false;
33
+ const children: unknown[] = Array.isArray(candidate)
34
+ ? candidate
35
+ : Object.values(candidate);
36
+ for (const child of children) pending.push(child);
37
+ }
38
+ return true;
39
+ }
40
+
41
+ function readEstablishments(data: unknown): JsonValue[] {
42
+ if (
43
+ typeof data !== "object" ||
44
+ data === null ||
45
+ !("results" in data) ||
46
+ !Array.isArray(data.results)
47
+ ) {
48
+ throw new TypeError("Invalid establishment results");
49
+ }
50
+ const results: unknown[] = data.results;
51
+ if (!results.every(isJsonValue)) {
52
+ throw new TypeError("Invalid establishment JSON");
53
+ }
54
+ return results;
55
+ }
56
+
57
+ function isActiveEstablishment(value: JsonValue): value is JsonObject {
58
+ if (value === null) throw new TypeError("Cannot read properties of null");
59
+ return (
60
+ typeof value === "object" &&
61
+ !Array.isArray(value) &&
62
+ value.etatadministratifetablissement === "Actif"
63
+ );
64
+ }
65
+
66
+ export default (): RequestHandler<Record<string, string>, CompanySuggestion[]> =>
67
+ async (req, res) => {
68
+ const processedQuery = (req.query.query as SearchQuery).replaceAll(" ", "-");
69
+ try {
70
+ const response = await axios.get<unknown>(
71
+ `https://public.opendatasoft.com/api/explore/v2.1/catalog/datasets/economicref-france-sirene-v3/records`,
72
+ {
73
+ params: {
74
+ where: `denominationunitelegale LIKE '${processedQuery}*' OR siret LIKE '${processedQuery}*'`,
75
+ limit: 100,
76
+ },
77
+ },
78
+ );
79
+ const results = readEstablishments(response.data)
80
+ .filter(isActiveEstablishment)
81
+ .map((etablissement) => ({
82
+ id: Math.random().toString().slice(2),
83
+ companyName: etablissement.denominationunitelegale,
84
+ siret: etablissement.siret,
85
+ siren: etablissement.siren,
86
+ address: `${etablissement.numerovoieetablissement || ""} ${etablissement.typevoieetablissement || ""} ${etablissement.libellevoieetablissement || ""}`,
87
+ city: etablissement.libellecommuneetablissement,
88
+ zipCode: etablissement.codepostaletablissement,
89
+ activityNumber: etablissement.activiteprincipaleunitelegale,
90
+ }))
91
+ .slice(0, 30);
92
+ res.send(results);
93
+ } catch (e) {
94
+ res.send([]);
95
+ }
96
+ };