@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
package/src/index.ts CHANGED
@@ -5,6 +5,7 @@ import * as pgSessionStore from 'connect-pg-simple';
5
5
  import * as mysql2 from 'mysql2/promise';
6
6
  import * as pg from 'pg';
7
7
  import dbManager from "./utils/dbManager";
8
+ import type { DatabaseRow } from "./utils/dbManager";
8
9
  import * as bcrypt from "bcryptjs";
9
10
 
10
11
  import * as fs from "fs";
@@ -13,13 +14,17 @@ import * as mime from "mime";
13
14
 
14
15
  import * as moment from "moment";
15
16
 
16
- import { Config } from "./types/config"
17
+ import type { Config } from "./types/config"
17
18
 
18
19
  import api from "./api"
19
20
 
21
+ // The runtime accepts promise pools as well as its declared callback connections.
22
+ type MySQLSessionStoreConstructor = ReturnType<typeof SessionStore> &
23
+ (new (options: SessionStore.Options, connection: mysql2.Pool) => session.Store);
24
+
20
25
  moment.locale('fr');
21
26
 
22
- export default (config: Config): any => {
27
+ export default (config: Config) => {
23
28
  const db = new dbManager();
24
29
 
25
30
  db.init(config);
@@ -51,14 +56,14 @@ export default (config: Config): any => {
51
56
  }),
52
57
  createTableIfMissing: true,
53
58
  tableName: 'galadmin_sessions'
54
- }) : new (SessionStore(session))({
59
+ }) : new (SessionStore(session) as MySQLSessionStoreConstructor)({
55
60
  schema: { tableName: 'galadmin_sessions' }
56
61
  }, mysql2.createPool({
57
- host: config.mysql.host,
58
- user: config.mysql.user,
59
- password: config.mysql.password,
60
- database: config.mysql.database,
61
- port: config.mysql.port || 3306
62
+ host: config.mysql!.host,
63
+ user: config.mysql!.user,
64
+ password: config.mysql!.password,
65
+ database: config.mysql!.database,
66
+ port: config.mysql!.port || 3306
62
67
  })),
63
68
  resave: false,
64
69
  saveUninitialized: false,
@@ -70,13 +75,13 @@ export default (config: Config): any => {
70
75
  if (config.trustedAuth) {
71
76
  app.use(async (req, res, next) => {
72
77
  try {
73
- const user = await config.trustedAuth(req);
78
+ const user = await config.trustedAuth!(req);
74
79
  if (user) {
75
- if (JSON.stringify(req.session.user) !== JSON.stringify(user)) {
76
- req.session.user = user;
80
+ if (JSON.stringify(req.session!.user) !== JSON.stringify(user)) {
81
+ req.session!.user = user;
77
82
  }
78
- } else if (req.session.user) {
79
- delete req.session.user;
83
+ } else if (req.session!.user) {
84
+ delete req.session!.user;
80
85
  }
81
86
  next();
82
87
  } catch (err) {
@@ -85,7 +90,7 @@ export default (config: Config): any => {
85
90
  });
86
91
  }
87
92
 
88
- const passwordMatch = async (password: string, hash: string, useArgon2: boolean) => {
93
+ const passwordMatch = async (password: string, hash: string, useArgon2: boolean | undefined) => {
89
94
  if (password === hash || bcrypt.compareSync(password, hash)) {
90
95
  return true;
91
96
  }
@@ -105,15 +110,23 @@ export default (config: Config): any => {
105
110
  return res.send({ success: false })
106
111
  }
107
112
  if (config.auth && req.body.username === config.auth.username && req.body.password === config.auth.password) {
108
- req.session.user = { role: "root" }
113
+ req.session!.user = { role: "root" }
109
114
  res.send({ success: true })
110
115
  } else if (config.authRoles) {
111
116
  for (const authRole of config.authRoles) {
112
117
  const filters = authRole.filters ? `AND ${authRole.filters}` : ""
113
118
  const { rows } = await db.query(`SELECT * FROM ?? WHERE ?? = ? ${filters}`, [authRole.table, authRole.usernameField, req.body.username]);
114
- let user;
119
+ /*
120
+ #PORTING-EDIT
121
+ A mutation result cannot identify a user; ignore non-row results and non-text passwords.
122
+ */
123
+ if (!Array.isArray(rows)) continue;
124
+ let user: DatabaseRow | undefined;
115
125
  for (const row of rows) {
116
- if (await passwordMatch(req.body.password, row[authRole.passwordField], authRole.useArgon2)) {
126
+ if (Array.isArray(row)) continue;
127
+ const record: DatabaseRow = row;
128
+ const hash = record[authRole.passwordField];
129
+ if (typeof hash === "string" && await passwordMatch(req.body.password, hash, authRole.useArgon2)) {
117
130
  user = row;
118
131
  break;
119
132
  }
@@ -121,7 +134,7 @@ export default (config: Config): any => {
121
134
  if (!user) {
122
135
  continue;
123
136
  }
124
- req.session.user = { role: authRole.role, userId: user[authRole.idField] }
137
+ req.session!.user = { role: authRole.role, userId: user[authRole.idField] }
125
138
  return res.send({ success: true })
126
139
  }
127
140
  res.send({ success: false })
@@ -133,13 +146,13 @@ export default (config: Config): any => {
133
146
 
134
147
  app.get('/api/isAuthenticated', (req, res) => {
135
148
  res.send({
136
- isAuthenticated: !!req.session.user
149
+ isAuthenticated: !!req.session!.user
137
150
  })
138
151
  })
139
152
 
140
153
 
141
154
  app.get('/api/logout', (req, res) => {
142
- req.session.destroy((err) => {
155
+ req.session!.destroy((err) => {
143
156
  if (err) {
144
157
  console.error(err)
145
158
  }
@@ -148,17 +161,21 @@ export default (config: Config): any => {
148
161
  })
149
162
 
150
163
  app.use("/api", async (req, res, next) => {
151
- if (req.method === "OPTIONS" || req.session.user) {
164
+ if (req.method === "OPTIONS" || req.session!.user) {
152
165
  if (req.session?.user?.userId && !config.trustedAuth) {
153
166
  // we check that the user still exists
154
167
  let stillExists = false;
155
- for (const authRole of config.authRoles) {
168
+ for (const authRole of config.authRoles!) {
156
169
  // Re-apply the role's `filters` here too, so an account that becomes
157
170
  // ineligible (e.g. deactivated) has its existing session invalidated
158
171
  // on its next request — not just blocked at login time.
159
172
  const filters = authRole.filters ? `AND ${authRole.filters}` : "";
160
173
  const { rows } = await db.query(`SELECT * FROM ?? WHERE ?? = ? ${filters}`, [authRole.table, authRole.idField, req.session.user.userId]);
161
- if (rows.length > 0) {
174
+ /*
175
+ #PORTING-EDIT
176
+ A non-row result cannot establish that the session user still exists.
177
+ */
178
+ if (Array.isArray(rows) && rows.length > 0) {
162
179
  stillExists = true;
163
180
  break;
164
181
  }
@@ -189,13 +206,13 @@ export default (config: Config): any => {
189
206
 
190
207
  // Send media files just like they are
191
208
  if ([".woff", ".png"].includes(path.extname(filePath))) {
192
- res.contentType(mime.getType(filePath));
209
+ res.contentType(mime.getType(filePath)!);
193
210
  res.send(fs.readFileSync(filePath))
194
211
  } else {
195
212
  // For text files, we replace _HOMEPAGE_ with the base url of the back-office
196
213
  let fileContent = fs.readFileSync(filePath, { encoding: "utf-8" })
197
214
  fileContent = fileContent.replace(/\/_HOMEPAGE_/g, req.baseUrl)
198
- res.contentType(mime.getType(filePath));
215
+ res.contentType(mime.getType(filePath)!);
199
216
  res.send(fileContent)
200
217
  }
201
218
  })
@@ -1,5 +1,15 @@
1
+ import type { DatabaseId } from "./database";
2
+
1
3
  export type Filter = {};
2
4
 
5
+ export type RootUser = { role: "root" };
6
+ export type RegularUser = { role: string; userId?: DatabaseId };
7
+ export type FilterUser = RootUser | RegularUser;
8
+
9
+ export function isRegularUser(user: FilterUser): user is RegularUser {
10
+ return user.role !== "root";
11
+ }
12
+
3
13
  export type BaseTypeColumn = {
4
14
  label: string;
5
15
  hideInTable?: boolean;
@@ -89,7 +99,7 @@ export type ForeignSelectColumn = {
89
99
  selectExpression?: string;
90
100
  join?: string;
91
101
  filters?: Filter[];
92
- filtersFromUser?: (userId: number, role?: string) => string;
102
+ filtersFromUser?: (user: FilterUser) => string;
93
103
  orderBy?: string;
94
104
  optional?: boolean;
95
105
  formatForeignValue?: (record: any) => string;
@@ -105,7 +115,7 @@ export type ForeignDatalistColumn = {
105
115
  selectExpression?: string;
106
116
  join?: string;
107
117
  filters?: Filter[];
108
- filtersFromUser?: (userId: number, role?: string) => string;
118
+ filtersFromUser?: (user: FilterUser) => string;
109
119
  orderBy?: string;
110
120
  optional?: boolean;
111
121
  formatForeignValue?: (record: any) => string;
@@ -237,7 +247,7 @@ export type TableView = {
237
247
  isDesc?: boolean;
238
248
  limit?: number;
239
249
  filters?: string;
240
- filtersFromUser?: (userId: number, role?: string) => string;
250
+ filtersFromUser?: (user: FilterUser) => string;
241
251
  notClickable?: boolean;
242
252
  clickToEdit?: boolean;
243
253
  openInNewTab?: boolean;
@@ -292,7 +302,7 @@ export type BaseTypePanel = {
292
302
  label: string;
293
303
  folded?: boolean;
294
304
  displayCondition?: (record: any) => boolean;
295
- };
305
+ } & Pick<BaseTypeView, "roles">;
296
306
 
297
307
  export type CreateConfig = {
298
308
  label: string;
@@ -430,7 +440,7 @@ export type AuthRole = {
430
440
 
431
441
  export type TrustedUser = {
432
442
  role: string;
433
- userId?: string | number;
443
+ userId?: DatabaseId;
434
444
  };
435
445
 
436
446
  export type Config = {
@@ -0,0 +1,2 @@
1
+ /** A supported database identifier: numeric, string, or UUID. */
2
+ export type DatabaseId = string | number;
@@ -2,6 +2,82 @@ import * as mysql from "mysql";
2
2
  import * as mysql2 from "mysql2";
3
3
  import * as pg from "pg";
4
4
 
5
+ type DatabaseValue = string | number | bigint | boolean | null | Date | Buffer
6
+ | DatabaseValue[] | DatabaseRow;
7
+ export type DatabaseRow = { [column: string]: DatabaseValue };
8
+ type MutationResult = Pick<mysql2.ResultSetHeader, "insertId" | "affectedRows">;
9
+ type QueryRows = DatabaseRow[] | MutationResult | QueryRows[];
10
+ type QueryField = Pick<pg.FieldDef, "name">;
11
+ type QueryFields = (QueryField | QueryField[] | undefined)[];
12
+ type QueryResult = {
13
+ err: Error | null;
14
+ rows: QueryRows | undefined;
15
+ fields: QueryFields | undefined;
16
+ };
17
+ type DatabaseConfig = {
18
+ useMysql2?: boolean;
19
+ mysql?: Pick<mysql.PoolConfig, "host" | "user" | "password" | "database" | "port" | "multipleStatements">;
20
+ postgresql?: Pick<pg.PoolConfig, "host" | "user" | "password" | "database" | "port" | "ssl">;
21
+ };
22
+ type DatabasePool = {
23
+ query(sql: string, values: mysql2.QueryValues[], callback: (
24
+ error: Error | null | undefined, result: unknown, fields?: unknown,
25
+ ) => void): void;
26
+ };
27
+
28
+ function isDatabaseValue(value: unknown): value is DatabaseValue {
29
+ return value === null || typeof value === "string" || typeof value === "number"
30
+ || typeof value === "bigint" || typeof value === "boolean"
31
+ || value instanceof Date || Buffer.isBuffer(value)
32
+ || (Array.isArray(value) ? value.every(isDatabaseValue)
33
+ : typeof value === "object" && Object.values(value).every(isDatabaseValue));
34
+ }
35
+
36
+ function isDatabaseRow(value: unknown): value is DatabaseRow {
37
+ return value !== null && typeof value === "object" && !Array.isArray(value)
38
+ && !(value instanceof Date) && !Buffer.isBuffer(value)
39
+ && Object.values(value).every(isDatabaseValue);
40
+ }
41
+
42
+ function isQueryRows(value: unknown): value is QueryRows {
43
+ return Array.isArray(value)
44
+ ? value.every(isDatabaseRow) || value.every(isQueryRows)
45
+ : value !== null && typeof value === "object"
46
+ && "insertId" in value && typeof value.insertId === "number"
47
+ && "affectedRows" in value && typeof value.affectedRows === "number";
48
+ }
49
+
50
+ function isQueryField(value: unknown): value is QueryField {
51
+ return value !== null && typeof value === "object" && "name" in value && typeof value.name === "string";
52
+ }
53
+
54
+ function isQueryFields(value: unknown): value is QueryFields {
55
+ return Array.isArray(value) && value.every((field: unknown) =>
56
+ field === undefined || isQueryField(field) || (Array.isArray(field) && field.every(isQueryField)));
57
+ }
58
+
59
+ /*
60
+ #PORTING-EDIT
61
+ Check driver results before exposing database values, reject unsupported shapes, and use undefined for absent rows.
62
+ */
63
+ function readQueryResult(result: unknown, fields: unknown, postgres: boolean) {
64
+ let rows = result;
65
+ if (postgres) {
66
+ if (result === undefined || result === null) return { rows: undefined, fields: undefined };
67
+ if (typeof result !== "object") throw new TypeError("Invalid PostgreSQL result");
68
+ rows = "rows" in result ? result.rows : undefined;
69
+ fields = "fields" in result ? result.fields : undefined;
70
+ }
71
+ let checkedFields: QueryFields | undefined;
72
+ if (fields !== undefined) {
73
+ if (!isQueryFields(fields)) throw new TypeError("Invalid database fields");
74
+ checkedFields = fields;
75
+ }
76
+ if (rows === undefined || rows === null) return { rows: undefined, fields: checkedFields };
77
+ if (isQueryRows(rows)) return { rows, fields: checkedFields };
78
+ throw new TypeError("Invalid database rows");
79
+ }
80
+
5
81
  export function quoteIdentifier(name: string, dbType: string): string {
6
82
  const quoteChar = dbType === "POSTGRESQL" ? '"' : "`";
7
83
  return quoteChar + name + quoteChar;
@@ -42,9 +118,9 @@ function endOfLiteralOrComment(query: string, start: number): number {
42
118
  return query.length;
43
119
  }
44
120
 
45
- export function convertQuery(mysqlQuery, params) {
121
+ export function convertQuery(mysqlQuery: string, params: mysql2.QueryValues[]) {
46
122
  let pgQuery = "";
47
- let pgParams = [];
123
+ let pgParams: mysql2.QueryValues[] = [];
48
124
 
49
125
  let paramIndex = 1;
50
126
  let charIndex = 0;
@@ -81,7 +157,7 @@ export function convertQuery(mysqlQuery, params) {
81
157
  };
82
158
  }
83
159
 
84
- export function quotePostgresQuery(query) {
160
+ export function quotePostgresQuery(query: string) {
85
161
  query = query.replace(/`/g, '"');
86
162
 
87
163
  const reservedWords = [
@@ -174,7 +250,7 @@ export function quotePostgresQuery(query) {
174
250
  "RETURNING",
175
251
  ];
176
252
 
177
- const wordsToAddQuotes = new Set();
253
+ const wordsToAddQuotes = new Set<string>();
178
254
  const parts = query.split(/[ ,\(\)]/);
179
255
 
180
256
  let inString = false;
@@ -197,7 +273,7 @@ export function quotePostgresQuery(query) {
197
273
  ) {
198
274
  } else {
199
275
  for (let x of part.split(".")) {
200
- if (isNaN(x)) {
276
+ if (isNaN(Number(x))) {
201
277
  wordsToAddQuotes.add(x);
202
278
  }
203
279
  }
@@ -221,12 +297,14 @@ export function quotePostgresQuery(query) {
221
297
  }
222
298
 
223
299
  export default class dbManager {
224
- _db;
225
- dbType;
300
+ _db?: DatabasePool;
301
+ dbType?: "MYSQL" | "POSTGRESQL";
226
302
 
227
- init(config) {
303
+ init(config: DatabaseConfig) {
228
304
  if (config.mysql) {
229
- this._db = (config.useMysql2 ? mysql2 : mysql).createPool({
305
+ const driver: { createPool(options: mysql.PoolConfig & mysql2.PoolOptions): DatabasePool } =
306
+ config.useMysql2 ? mysql2 : mysql;
307
+ this._db = driver.createPool({
230
308
  connectionLimit: 10,
231
309
  host: config.mysql.host,
232
310
  user: config.mysql.user,
@@ -254,18 +332,25 @@ export default class dbManager {
254
332
  }
255
333
  }
256
334
 
257
- async query(queryString, args = []): Promise<any> {
335
+ async query(queryString: string, args: mysql2.QueryValues[] = []): Promise<QueryResult> {
258
336
  return new Promise((resolve, reject) => {
259
337
  if (this.dbType === "POSTGRESQL") {
260
338
  const result = convertQuery(queryString, args);
261
339
  queryString = quotePostgresQuery(result.pgQuery);
262
340
  args = result.pgParams;
263
341
  }
342
+ if (!this._db) throw new TypeError("Cannot read properties of undefined (reading 'query')");
264
343
  this._db.query(queryString, args, (a, b, c) => {
265
- if (this.dbType === "MYSQL") {
266
- resolve({ err: a, rows: b, fields: c });
267
- } else if (this.dbType === "POSTGRESQL") {
268
- resolve({ err: a, rows: b?.rows, fields: b?.fields });
344
+ if (this.dbType === "MYSQL" || this.dbType === "POSTGRESQL") {
345
+ try {
346
+ /*
347
+ #PORTING-EDIT
348
+ Both drivers signal success without an error; use null for PostgreSQL's undefined success value.
349
+ */
350
+ resolve({ err: a ?? null, ...readQueryResult(b, c, this.dbType === "POSTGRESQL") });
351
+ } catch (error) {
352
+ reject(error);
353
+ }
269
354
  }
270
355
  });
271
356
  });
@@ -185,6 +185,46 @@ describe("sqlAnd", () => {
185
185
  });
186
186
 
187
187
  describe("evalExpression", () => {
188
+ it("keeps direct evaluation scope and repeated field getter reads", () => {
189
+ const data = { a: "x" };
190
+ expect(evalExpression("[data, statement, varString]", data)).toEqual([
191
+ data, "var a = 'x';", "var a = 'x';",
192
+ ]);
193
+ expect(evalExpression("data", data)).toBe(data);
194
+ let reads = 0;
195
+ expect(evalExpression("text", {
196
+ get text() { return ++reads === 1 ? "guard" : "second read"; },
197
+ })).toBe("second read");
198
+ expect(reads).toBe(2);
199
+ reads = 0;
200
+ expect(evalExpression("item.read", {
201
+ get item() { return { read: ++reads }; },
202
+ })).toBe(3);
203
+ expect(reads).toBe(3);
204
+ reads = 0;
205
+ expect(evalExpression("text", {
206
+ get text() { return ++reads === 1 ? "guard" : 42; },
207
+ })).toBe("Error");
208
+ expect(reads).toBe(2);
209
+ });
210
+ it("preserves unchecked result identity, lazy properties and CSV/JSON coercion", () => {
211
+ const data = { reads: 0 };
212
+ const result = evalExpression("({get label(){ return ++data.reads; }})", data);
213
+ expect(data.reads).toBe(0);
214
+ expect(JSON.stringify({ label: result })).toBe('{"label":{"label":1}}');
215
+ const coercible = evalExpression('({toString(){return "csv";}, toJSON(){return "json";}})', {});
216
+ expect("" + coercible).toBe("csv");
217
+ expect(JSON.stringify({ label: coercible })).toBe('{"label":"json"}');
218
+ const cycle: { self?: object } = {};
219
+ cycle.self = cycle;
220
+ const holder = Object.defineProperty({}, "cycle", { value: cycle });
221
+ expect(evalExpression("data.cycle", holder)).toBe(cycle);
222
+ const fn = evalExpression("(function label(){ return 7; })", {});
223
+ expect("" + fn).toContain("function label");
224
+ expect(JSON.stringify({ label: fn })).toBe("{}");
225
+ expect(evalExpression("undefined", {})).toBeUndefined();
226
+ expect(evalExpression("null", {})).toBeNull();
227
+ });
188
228
  it("evaluates an expression against record fields", () => {
189
229
  expect(
190
230
  evalExpression("weight + ' kg'", { weight: 42 }),
@@ -205,6 +245,17 @@ describe("evalExpression", () => {
205
245
  });
206
246
 
207
247
  describe("getFormattedValue", () => {
248
+ it("preserves raw object identity, absent-name indexing and own undefined options", async () => {
249
+ const value = { nested: true };
250
+ const record: Record<string, unknown> = { undefined: value };
251
+ await expect(getFormattedValue({}, record)).resolves.toBe(value);
252
+ expect(Object.prototype.hasOwnProperty.call(record, "options")).toBe(true);
253
+ expect(record.options).toBeUndefined();
254
+ const options = ["x"];
255
+ await expect(getFormattedValue({ formattedValue: "data", options }, record)).resolves.toBe(record);
256
+ expect(record.options).toBe(options);
257
+ await expect(getFormattedValue({ formatValueForExport: () => { throw new Error("formatter failed"); } }, record)).resolves.toBe("");
258
+ });
208
259
  it("prefers formatValueForExport", async () => {
209
260
  const column = {
210
261
  name: "a",
@@ -240,6 +291,14 @@ describe("getFormattedValue", () => {
240
291
  });
241
292
 
242
293
  describe("formatValueForInsertion", () => {
294
+ it("preserves unchecked fallback values and existing serialization failures", () => {
295
+ const symbol = Symbol("raw");
296
+ const callback = () => "raw";
297
+ expect(formatValueForInsertion(symbol, "MYSQL")).toBe(symbol);
298
+ expect(formatValueForInsertion(callback, "MYSQL")).toBe(callback);
299
+ expect(formatValueForInsertion(BigInt(9), "MYSQL")).toBe(BigInt(9));
300
+ expect(() => formatValueForInsertion({ toJSON: () => undefined }, "MYSQL")).toThrow(TypeError);
301
+ });
243
302
  it("maps falsy (but not 0) to NULL", () => {
244
303
  expect(formatValueForInsertion(null, "MYSQL")).toBe("NULL");
245
304
  expect(formatValueForInsertion(undefined, "MYSQL")).toBe("NULL");
@@ -1,4 +1,4 @@
1
- import {
1
+ import dbManager, {
2
2
  convertQuery,
3
3
  quotePostgresQuery,
4
4
  quoteIdentifier,
@@ -263,3 +263,18 @@ describe("convertQuery / quotePostgresQuery (adversarial cases)", () => {
263
263
  );
264
264
  });
265
265
  });
266
+
267
+
268
+ describe("dbManager input behavior", () => {
269
+ it("rejects a query when the manager has no database", async () => {
270
+ await expect(new dbManager().query("SELECT 1"))
271
+ .rejects.toThrow("Cannot read properties of undefined (reading 'query')");
272
+ });
273
+
274
+ it("preserves parameter consumption and exhausted placeholders", () => {
275
+ const params = ["table", 1];
276
+ expect(convertQuery("SELECT ? FROM ?? WHERE a = ?", params))
277
+ .toEqual({ pgQuery: "SELECT $1 FROM 1 WHERE a = $2", pgParams: ["table", undefined] });
278
+ expect(params).toEqual([]);
279
+ });
280
+ });
@@ -1,3 +1,5 @@
1
+ import type DbManager from "../../src/utils/dbManager";
2
+
1
3
  /**
2
4
  * Scripted fake for the dbManager abstraction used by the galadmin routes.
3
5
  *
@@ -15,13 +17,15 @@
15
17
  export type FakeQueryResult = { err?: any; rows?: any; fields?: any };
16
18
  export type RecordedQuery = { sql: string; params: any[] };
17
19
 
18
- export class FakeDb {
20
+ export class FakeDb implements DbManager {
19
21
  dbType: "MYSQL" | "POSTGRESQL" = "MYSQL";
20
22
  schemaRows: any[] = [];
21
23
  queries: RecordedQuery[] = [];
22
24
  private queue: FakeQueryResult[] = [];
23
25
  defaultResult: FakeQueryResult = { err: null, rows: [] };
24
26
 
27
+ init(_config: Parameters<DbManager["init"]>[0]): void {}
28
+
25
29
  queueResult(result: FakeQueryResult) {
26
30
  this.queue.push({ err: null, rows: [], ...result });
27
31
  return this;
@@ -35,15 +39,13 @@ export class FakeDb {
35
39
  return this.queueResult({ err, rows: null });
36
40
  }
37
41
 
38
- async query(sql: string, params: any[] = []): Promise<FakeQueryResult> {
42
+ async query(sql: string, params: any[] = []): Promise<Awaited<ReturnType<DbManager["query"]>>> {
39
43
  this.queries.push({ sql, params });
40
44
  if (typeof sql === "string" && sql.includes("information_schema")) {
41
- return { err: null, rows: this.schemaRows };
42
- }
43
- if (this.queue.length > 0) {
44
- return this.queue.shift()!;
45
+ return { err: null, rows: this.schemaRows, fields: undefined };
45
46
  }
46
- return this.defaultResult;
47
+ const result = this.queue.length > 0 ? this.queue.shift()! : this.defaultResult;
48
+ return { err: null, rows: [], fields: undefined, ...result };
47
49
  }
48
50
 
49
51
  reset() {
@@ -0,0 +1,57 @@
1
+ import * as request from "supertest";
2
+ import { canWriteView, roleOf, viewFromRequest } from "../../src/api/authorization";
3
+ import { buildAppForManager } from "../helpers/app";
4
+ import { baseConfig, fruitView } from "../helpers/fixtures";
5
+ import { DbHarness, describeIntegration, getMysqlHarness, resetTables } from "./dbHarness";
6
+
7
+ describeIntegration("authorization input boundaries", () => {
8
+ let harness: DbHarness;
9
+ beforeAll(async () => { harness = await getMysqlHarness(); });
10
+ beforeEach(async () => { await resetTables(harness); });
11
+ afterAll(async () => { await harness.stop(); });
12
+
13
+ it("uses one absent-role value and preserves declared role decisions", () => {
14
+ for (const user of [null, undefined, 0, "editor", {}, { role: null }, { role: 42 }]) {
15
+ const role = roleOf({ session: { user } });
16
+ expect(role).toBeUndefined();
17
+ expect(canWriteView({ roles: ["editor"] }, role)).toBe(false);
18
+ expect(canWriteView({}, role)).toBe(true);
19
+ }
20
+ expect(roleOf({ session: { user: { role: "root" } } })).toBe("root");
21
+ expect(canWriteView({ roles: [] }, "root")).toBe(true);
22
+ });
23
+
24
+ it("preserves canonical index coercion, input precedence, and view identity", () => {
25
+ const view = { roles: ["editor"] };
26
+ const config = { views: [view] };
27
+ for (const viewIndex of [0, "0", [0], { toString: () => "0" }]) {
28
+ expect(viewFromRequest({ body: { viewIndex }, query: { viewIndex: 99 }, config })).toBe(view);
29
+ }
30
+ expect(viewFromRequest({ body: {}, query: { viewIndex: "0" }, config })).toBe(view);
31
+ for (const viewIndex of [null, {}, [], "01", "length", "__proto__", "constructor", ["length"]]) {
32
+ expect(viewFromRequest({ body: { viewIndex }, query: { viewIndex: "0" }, config })).toBeUndefined();
33
+ }
34
+ });
35
+
36
+ it("rejects non-view properties before HTTP writes and keeps valid index writes", async () => {
37
+ const app = buildAppForManager(harness.manager, baseConfig());
38
+ for (const viewIndex of ["length", "__proto__", "constructor", ["length"], "01", null]) {
39
+ await request(app).post("/api/createRecord").query({ viewIndex: "0" })
40
+ .send({ viewIndex, data: { fruitName: "rejected" } }).expect(403);
41
+ }
42
+ expect((await harness.query("SELECT * FROM fruits")).rows).toEqual([]);
43
+ for (const viewIndex of [0, "0", [0]]) {
44
+ await request(app).post("/api/createRecord").query({ viewIndex: "99" })
45
+ .send({ viewIndex, data: { fruitName: "allowed" } }).expect(200);
46
+ }
47
+ expect((await harness.query("SELECT * FROM fruits")).rows).toHaveLength(3);
48
+ });
49
+
50
+ it("keeps anonymous HTTP writes denied for role-restricted views", async () => {
51
+ const config = baseConfig({ views: [fruitView({ roles: ["editor"] })] });
52
+ const app = buildAppForManager(harness.manager, config, null);
53
+ await request(app).post("/api/createRecord")
54
+ .send({ viewIndex: 0, data: { fruitName: "rejected" } }).expect(403);
55
+ expect((await harness.query("SELECT * FROM fruits")).rows).toEqual([]);
56
+ });
57
+ });