@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,79 @@
1
+ import { createPool } from "mysql";
2
+ import { enrichConfig, generateUniqueIdForObject } from "../../src/api/config";
3
+ import dbManager from "../../src/utils/dbManager";
4
+ import { baseConfig, fruitView } from "../helpers/fixtures";
5
+ import {
6
+ DbHarness, describeIntegration, getMysqlHarness, readIntegrationState,
7
+ } from "./dbHarness";
8
+
9
+ describeIntegration("config enrichment metadata boundary", () => {
10
+ let harness: DbHarness;
11
+
12
+ beforeAll(async () => {
13
+ harness = await getMysqlHarness();
14
+ const { err } = await harness.query("CREATE TABLE port_config_enum (state ENUM('open','closed') NOT NULL)");
15
+ if (err) throw err;
16
+ });
17
+
18
+ afterAll(async () => {
19
+ try {
20
+ const { err } = await harness.query("DROP TABLE port_config_enum");
21
+ if (err) throw err;
22
+ } finally {
23
+ await harness.stop();
24
+ }
25
+ });
26
+
27
+ const config = () => baseConfig({
28
+ views: [fruitView({
29
+ tableName: "port_config_enum",
30
+ columns: [{ label: "State", name: "state" }],
31
+ })],
32
+ });
33
+
34
+ it("keeps real enum options, required inference, and stable identifiers", async () => {
35
+ const value = config();
36
+ const id = generateUniqueIdForObject(value.views[0].columns[0]);
37
+ await enrichConfig(value, harness.manager);
38
+ expect(value.views[0].columns[0]).toEqual({
39
+ label: "State", name: "state", __id__: id,
40
+ dataType: "select", options: ["open", "closed"], required: true,
41
+ });
42
+ const viewId = value.views[0].__id__;
43
+ await enrichConfig(value, harness.manager);
44
+ expect(value.views[0].__id__).toBe(viewId);
45
+ expect(value.views[0].columns[0].__id__).toBe(id);
46
+ expect(value.enriched).toBe(true);
47
+ });
48
+
49
+ it.each<[string, string | number, string]>([
50
+ ["TABLE_NAME", 42, "Invalid database schema columns"],
51
+ ["COLUMN_TYPE", "enum(1)", "Invalid enum options"],
52
+ ])("rejects invalid %s returned by a real driver decoder", async (fieldName, decodedValue, message) => {
53
+ const conn = readIntegrationState()?.mysql;
54
+ if (!conn) throw new Error("Missing MySQL integration connection");
55
+ const pool = createPool({
56
+ ...conn,
57
+ typeCast(field, next) {
58
+ return field.name === fieldName ? decodedValue : next();
59
+ },
60
+ });
61
+ const manager = new dbManager();
62
+ manager.dbType = "MYSQL";
63
+ manager._db = pool;
64
+ const value = config();
65
+ try {
66
+ await expect(enrichConfig(value, manager)).rejects.toThrow(message);
67
+ expect(value.enriched).toBeUndefined();
68
+ if (fieldName === "TABLE_NAME") {
69
+ expect(value.views[0].columns[0]).toEqual({ label: "State", name: "state" });
70
+ expect(value.views[0].__id__).toBeUndefined();
71
+ } else {
72
+ expect(value.views[0].columns[0].dataType).toBe("select");
73
+ expect(value.views[0].columns[0].options).toBeUndefined();
74
+ }
75
+ } finally {
76
+ await new Promise<void>((resolve, reject) => pool.end((err) => err ? reject(err) : resolve()));
77
+ }
78
+ });
79
+ });
@@ -0,0 +1,58 @@
1
+ import * as express from "express";
2
+ import * as request from "supertest";
3
+ import * as fs from "fs";
4
+ import * as os from "os";
5
+ import * as path from "path";
6
+ import customComponentRoute from "../../src/api/routes/customComponent";
7
+ import { describeIntegration } from "./dbHarness";
8
+
9
+ describeIntegration("custom component package boundary with real Rollup", () => {
10
+ let directory: string;
11
+ let packagePath: string;
12
+ let app: express.Express;
13
+
14
+ beforeEach(() => {
15
+ directory = fs.mkdtempSync(path.join(os.tmpdir(), "galadmin-component-"));
16
+ const packageRoot = path.join(directory, "package");
17
+ fs.mkdirSync(path.join(packageRoot, "src/front"), { recursive: true });
18
+ fs.mkdirSync(path.join(packageRoot, "dist"));
19
+ packagePath = path.join(packageRoot, "src/front/package.json");
20
+ fs.writeFileSync(packagePath, "{}");
21
+ fs.writeFileSync(path.join(packageRoot, "babel.config"), "{}");
22
+ fs.writeFileSync(path.join(directory, "component.js"), "export default 42;");
23
+ app = express();
24
+ app.get("/component", customComponentRoute(path.join(packageRoot, "dist")));
25
+ });
26
+
27
+ afterEach(() => fs.rmSync(directory, { recursive: true, force: true }));
28
+
29
+ it("bundles a component and keeps declared dependencies external", async () => {
30
+ fs.writeFileSync(packagePath, JSON.stringify({ dependencies: { "fixture-dependency": "1" } }));
31
+ fs.writeFileSync(path.join(directory, "component.js"),
32
+ "import value from 'fixture-dependency'; export default value;");
33
+ const result = await request(app).get("/component")
34
+ .query({ componentPath: "component.js" }).expect(200);
35
+ expect(result.text).toContain("require('fixture-dependency')");
36
+ expect(result.text).toContain("module.exports");
37
+ });
38
+
39
+ it.each(["{}", "0", '"metadata"', '{"dependencies":false}'])(
40
+ "retains JavaScript property access for package %s", async (metadata) => {
41
+ fs.writeFileSync(packagePath, metadata);
42
+ const result = await request(app).get("/component")
43
+ .query({ componentPath: "component.js" }).expect(200);
44
+ expect(result.text).toContain("42");
45
+ expect(result.text).toContain("module.exports");
46
+ },
47
+ );
48
+
49
+ it.each(["null", "{invalid"])("returns 500 for package %s", async (metadata) => {
50
+ fs.writeFileSync(packagePath, metadata);
51
+ await request(app).get("/component")
52
+ .query({ componentPath: "component.js" }).expect(500);
53
+ });
54
+
55
+ it("returns 500 for a missing component path", async () => {
56
+ await request(app).get("/component").expect(500);
57
+ });
58
+ });
@@ -1,8 +1,13 @@
1
+ import * as request from "supertest";
2
+ import buildIndexApp from "../../src/index";
3
+ import dbManager from "../../src/utils/dbManager";
1
4
  import {
2
5
  DbHarness,
3
6
  describeIntegration,
4
7
  getMysql2Harness,
5
8
  getPgHarness,
9
+ readIntegrationState,
10
+ resetTables,
6
11
  } from "./dbHarness";
7
12
 
8
13
  /**
@@ -33,9 +38,7 @@ describeIntegration("dbManager against real databases", () => {
33
38
 
34
39
  it("unwraps {rows, fields} from the pg result object", async () => {
35
40
  const result = await harnesses.postgresql.query("SELECT 1 AS one");
36
- // pg resolves success with an undefined err (MySQL passes null); every
37
- // consumer only checks truthiness.
38
- expect(result.err).toBeFalsy();
41
+ expect(result.err).toBeNull();
39
42
  expect(result.rows).toEqual([{ one: 1 }]);
40
43
  expect(result.fields).toBeDefined();
41
44
  });
@@ -62,8 +65,107 @@ describeIntegration("dbManager against real databases", () => {
62
65
  expect(rows).toEqual([{ name: "pomme" }]);
63
66
  });
64
67
 
68
+ it("preserves PostgreSQL scalar, date, binary, and JSON values", async () => {
69
+ const raw = harnesses.postgresql.rawPg;
70
+ await raw.query("CREATE TABLE port_values (number INTEGER, flag BOOLEAN, stamp TIMESTAMPTZ, bytes BYTEA, data JSONB, missing TEXT)");
71
+ try {
72
+ const stamp = new Date("2026-01-02T03:04:05.000Z");
73
+ const bytes = Buffer.from("port");
74
+ const data = { nested: [true, 2, null, "text"] };
75
+ await raw.query("INSERT INTO port_values VALUES ($1, $2, $3, $4, $5, $6)", [7, true, stamp, bytes, data, null]);
76
+ const result = await harnesses.postgresql.query("SELECT * FROM port_values");
77
+ expect(result.err).toBeNull();
78
+ expect(result.rows).toEqual([{ number: 7, flag: true, stamp, bytes, data, missing: null }]);
79
+ expect(result.fields).toEqual(expect.arrayContaining([expect.objectContaining({ name: "stamp" })]));
80
+ } finally {
81
+ await raw.query("DROP TABLE port_values");
82
+ }
83
+ });
84
+
85
+ it("preserves MySQL mutation headers and multiple result sets when opted in", async () => {
86
+ const harness = new DbHarness();
87
+ harness.manager = new dbManager();
88
+ harness.manager.init({ useMysql2: true, mysql: { ...readIntegrationState().mysql, multipleStatements: true } });
89
+ try {
90
+ const setup = await harness.query("CREATE TEMPORARY TABLE port_multi (id INT PRIMARY KEY)");
91
+ expect(setup.err).toBeNull();
92
+ const result = await harness.query("INSERT INTO port_multi VALUES (1); SELECT id FROM port_multi");
93
+ expect(result.err).toBeNull();
94
+ expect(result.rows).toEqual([expect.objectContaining({ insertId: 0, affectedRows: 1 }), [{ id: 1 }]]);
95
+ expect(result.fields[0]).toBeUndefined();
96
+ expect(result.fields[1]).toEqual([expect.objectContaining({ name: "id" })]);
97
+ const failure = await harness.query("SELECT missing_column FROM port_multi");
98
+ expect(failure.err).toBeInstanceOf(Error);
99
+ expect(failure.rows).toBeUndefined();
100
+ expect(failure.fields).toBeUndefined();
101
+ } finally {
102
+ await harness.stop();
103
+ }
104
+ });
105
+
106
+ it("preserves the existing PostgreSQL multiple-result response", async () => {
107
+ expect(await harnesses.postgresql.query("SELECT 'one'; SELECT 'two'"))
108
+ .toEqual({ err: null, rows: undefined, fields: undefined });
109
+ });
110
+
65
111
  it("rejects stacked statements (multipleStatements off by default)", async () => {
66
112
  const result = await harnesses.mysql2.query("SELECT 1; SELECT 2");
67
113
  expect(result.err).toBeTruthy();
68
114
  });
69
115
  });
116
+
117
+
118
+ describeIntegration("authentication with real PostgreSQL sessions", () => {
119
+ let harness: DbHarness;
120
+ let app: ReturnType<typeof buildIndexApp>;
121
+
122
+ beforeAll(async () => {
123
+ harness = await getPgHarness();
124
+ await resetTables(harness);
125
+ await harness.rawPg.query(
126
+ 'INSERT INTO fruits ("fruitName", acidity, note) VALUES ($1, $2, NULL), ($3, NULL, $4)',
127
+ ["numeric-user", 123, "text-user", "secret"],
128
+ );
129
+ app = buildIndexApp({
130
+ name: "Port integration",
131
+ sessionSecret: "port-integration-session-secret",
132
+ auth: { username: "root", password: "root-secret" },
133
+ postgresql: readIntegrationState().postgresql,
134
+ authRoles: [
135
+ { role: "numeric", table: "fruits", usernameField: "fruitName", passwordField: "acidity", idField: "id", filters: "acidity IS NOT NULL" },
136
+ { role: "editor", table: "fruits", usernameField: "fruitName", passwordField: "note", idField: "id", filters: "note IS NOT NULL" },
137
+ ],
138
+ views: [],
139
+ });
140
+ });
141
+
142
+ afterAll(async () => {
143
+ try {
144
+ // The app owns its pools; wait for their native idle timeout before stopping PostgreSQL.
145
+ const deadline = Date.now() + 15_000;
146
+ while (true) {
147
+ const { rows } = await harness.rawPg.query(
148
+ "SELECT count(*)::int AS connections FROM pg_stat_activity WHERE datname = current_database() AND usename = current_user AND pid <> pg_backend_pid()",
149
+ );
150
+ if (rows[0].connections === 0) break;
151
+ if (Date.now() >= deadline) throw new Error("Application database pools did not become idle");
152
+ await new Promise((resolve) => setTimeout(resolve, 250));
153
+ }
154
+ } finally {
155
+ await harness?.stop();
156
+ }
157
+ });
158
+
159
+ it("rejects numeric passwords, accepts text passwords, and revokes a deleted user's session", async () => {
160
+ await request(app).post("/api/login")
161
+ .send({ username: "numeric-user", password: 123 })
162
+ .expect(200).expect({ success: false });
163
+ const agent = request.agent(app);
164
+ await agent.post("/api/login")
165
+ .send({ username: "text-user", password: "secret" })
166
+ .expect(200).expect({ success: true });
167
+ await agent.get("/api/isAuthenticated").expect(200).expect({ isAuthenticated: true });
168
+ await harness.rawPg.query('DELETE FROM fruits WHERE "fruitName" = $1', ["text-user"]);
169
+ await agent.post("/api/records").send({ viewIndex: 0 }).expect(401);
170
+ });
171
+ });
@@ -0,0 +1,55 @@
1
+ // Isolate unhandled parser rejections so characterization cannot crash Jest.
2
+ const fs = require("fs");
3
+ const ts = require("typescript");
4
+ require.extensions[".ts"] = (module, filename) => {
5
+ const source = fs.readFileSync(filename, "utf8");
6
+ const { outputText } = ts.transpileModule(source, {
7
+ compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2019 },
8
+ fileName: filename,
9
+ });
10
+ module._compile(outputText, filename);
11
+ };
12
+ const request = require("supertest");
13
+ const { buildAppForManager } = require("../helpers/app.ts");
14
+ const { baseConfig, fruitView } = require("../helpers/fixtures.ts");
15
+ const { getMysqlHarness, getMysql2Harness, getPgHarness } = require("./dbHarness.ts");
16
+
17
+ (async () => {
18
+ const factory = { mysql: getMysqlHarness, mysql2: getMysql2Harness, postgresql: getPgHarness };
19
+ const harness = await factory[process.env.IMPORT_BACKEND]();
20
+ let finished = false;
21
+ const finish = async (result) => {
22
+ if (finished) return;
23
+ finished = true;
24
+ await harness.stop();
25
+ process.send(result, () => process.exit(0));
26
+ };
27
+ process.on("unhandledRejection", (error) => finish({
28
+ kind: "unhandled", message: error.message,
29
+ }));
30
+ const parseImportFile = () => {
31
+ switch (process.env.IMPORT_CASE) {
32
+ case "throw": throw new Error("parser sync failure");
33
+ case "reject": return Promise.reject(new Error("parser async failure"));
34
+ case "null": return Promise.resolve(null);
35
+ case "undefined": return Promise.resolve(undefined);
36
+ default: throw new Error("Unknown parser case");
37
+ }
38
+ };
39
+ const panel = process.env.IMPORT_ROUTE === "panel";
40
+ const view = panel
41
+ ? fruitView({ recordViewPanels: [{
42
+ label: "Notes", tableName: "fruit_notes", parseImportFile,
43
+ createConfig: { label: "New note", recordIdKey: "fruitId", columns: [] },
44
+ }] })
45
+ : fruitView({ primaryId: "id", importFileUniqueField: "id", parseImportFile });
46
+ const config = baseConfig({ views: [view] });
47
+ const pending = request(buildAppForManager(harness.manager, config))
48
+ .post(panel ? "/api/subrecordsImport" : "/api/recordsImport").field("viewIndex", "0");
49
+ if (panel) pending.field("panelIndex", "0").field("recordId", "7");
50
+ const response = await pending.attach("data", Buffer.from("csv"), "import.csv");
51
+ await finish({ kind: "response", status: response.status, body: response.body });
52
+ })().catch((error) => {
53
+ console.error(error);
54
+ process.exit(1);
55
+ });
@@ -0,0 +1,22 @@
1
+ import { fork } from "child_process";
2
+ import * as path from "path";
3
+ import { describeIntegration } from "./dbHarness";
4
+
5
+ describeIntegration("index runtime with real MySQL sessions and static files", () => {
6
+ it("preserves promise-pool persistence, authentication, assets and unknown-MIME errors", async () => {
7
+ const result = await new Promise<unknown>((resolve, reject) => {
8
+ const child = fork(path.join(__dirname, "indexRuntimeChild.cjs"), [], {
9
+ env: process.env,
10
+ stdio: ["ignore", "ignore", "pipe", "ipc"],
11
+ });
12
+ let result: unknown;
13
+ let errors = "";
14
+ child.stderr?.on("data", (chunk) => { errors += chunk.toString(); });
15
+ child.on("message", (message) => { result = message; });
16
+ child.on("error", reject);
17
+ child.on("exit", (code) => code === 0 ? resolve(result) : reject(new Error(errors || `Child exited ${code}`)));
18
+ });
19
+ expect(result).toEqual({ sessionPool: "PromisePool", persisted: true, logout: true, trustedCalls: 2,
20
+ text: 200, media: 200, unknownMime: 500 });
21
+ });
22
+ });
@@ -0,0 +1,110 @@
1
+ // Isolate the unchanged index runtime from the known session-store declaration mismatch.
2
+ // Final build/strict checks still compile production source with the installed declarations.
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const assert = require("assert").strict;
6
+ const ts = require("typescript");
7
+ require.extensions[".ts"] = (module, filename) => {
8
+ const { outputText } = ts.transpileModule(fs.readFileSync(filename, "utf8"), {
9
+ compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2019 },
10
+ fileName: filename,
11
+ });
12
+ module._compile(outputText, filename);
13
+ };
14
+ const request = require("supertest");
15
+ const express = require("express");
16
+ const buildIndexApp = require("../../src/index.ts").default;
17
+ const { getMysql2Harness, readIntegrationState } = require("./dbHarness.ts");
18
+
19
+ (async () => {
20
+ const harness = await getMysql2Harness();
21
+ const stores = new Set();
22
+ const files = [];
23
+ try {
24
+ const config = {
25
+ name: "Index runtime", sessionSecret: "index-runtime-characterization-secret",
26
+ mysql: readIntegrationState().mysql, enriched: true, views: [],
27
+ auth: { username: "root", password: "secret" },
28
+ };
29
+ const mount = (settings) => {
30
+ const outer = express();
31
+ outer.use((req, res, next) => {
32
+ res.on("finish", () => { if (req.sessionStore) stores.add(req.sessionStore); });
33
+ next();
34
+ });
35
+ outer.use("/admin", buildIndexApp(settings));
36
+ return outer;
37
+ };
38
+ const app = mount(config);
39
+ const agent = request.agent(app);
40
+ await agent.get("/admin/api/isAuthenticated").expect(200, { isAuthenticated: false });
41
+ const store = [...stores][0];
42
+ await store.onReady();
43
+ assert.equal(store.connection.constructor.name, "PromisePool");
44
+ const login = await agent.post("/admin/api/login").send({ username: "root", password: "secret" })
45
+ .expect(200, { success: true });
46
+ const cookie = login.headers["set-cookie"][0].split(";")[0];
47
+ const signed = decodeURIComponent(cookie.slice(cookie.indexOf("=") + 1));
48
+ const sid = signed.slice(2, signed.indexOf("."));
49
+ const persisted = await harness.query("SELECT data FROM galadmin_sessions WHERE session_id = ?", [sid]);
50
+ assert.equal(persisted.err, null);
51
+ assert.equal(persisted.rows.length, 1);
52
+ assert.deepEqual(JSON.parse(persisted.rows[0].data).user, { role: "root" });
53
+ await agent.get("/admin/api/isAuthenticated").expect(200, { isAuthenticated: true });
54
+ await agent.get("/admin/api/logout").expect(200);
55
+ let remaining = true;
56
+ for (let attempt = 0; attempt < 50; attempt++) {
57
+ const result = await harness.query("SELECT session_id FROM galadmin_sessions WHERE session_id = ?", [sid]);
58
+ assert.equal(result.err, null);
59
+ remaining = result.rows.length !== 0;
60
+ if (!remaining) break;
61
+ await new Promise((resolve) => setTimeout(resolve, 20));
62
+ }
63
+ assert.equal(remaining, false);
64
+ await agent.get("/admin/api/isAuthenticated").expect(200, { isAuthenticated: false });
65
+
66
+ let hostUser = { role: "root", userId: "host-id" };
67
+ let calls = 0;
68
+ const trustedConfig = { ...config, trustedAuth(req) {
69
+ assert.equal(this, trustedConfig);
70
+ assert.equal(req.method, "GET");
71
+ calls++;
72
+ return hostUser;
73
+ } };
74
+ const trusted = request.agent(mount(trustedConfig));
75
+ await trusted.get("/admin/api/isAuthenticated").expect(200, { isAuthenticated: true });
76
+ hostUser = null;
77
+ await trusted.get("/admin/api/isAuthenticated").expect(200, { isAuthenticated: false });
78
+ assert.equal(calls, 2);
79
+
80
+ const dir = path.resolve(__dirname, "../../src/front/build");
81
+ fs.mkdirSync(dir, { recursive: true });
82
+ const fixture = (extension, content) => {
83
+ const name = `__port_index_${process.pid}.${extension}`;
84
+ const file = path.join(dir, name);
85
+ fs.writeFileSync(file, content, { flag: "wx" });
86
+ files.push(file);
87
+ return "/admin/" + name;
88
+ };
89
+ const text = await request(app).get(fixture("txt", "url=/_HOMEPAGE_/asset")).expect(200);
90
+ assert.match(text.headers["content-type"], /^text\/plain/);
91
+ assert.equal(text.text, "url=/admin/asset");
92
+ const bytes = Buffer.from([137, 80, 78, 71, 0, 255]);
93
+ const media = await request(app).get(fixture("png", bytes)).expect(200);
94
+ assert.match(media.headers["content-type"], /^image\/png/);
95
+ assert.deepEqual(media.body, bytes);
96
+ const unknown = await request(app).get(fixture("port-unknown-mime", "unknown type")).expect(500);
97
+ assert.match(unknown.text, /indexOf/);
98
+ assert.match(unknown.headers["content-type"], /^text\/html/);
99
+ process.send({ sessionPool: "PromisePool", persisted: true, logout: true, trustedCalls: calls,
100
+ text: 200, media: 200, unknownMime: unknown.status });
101
+ } finally {
102
+ for (const file of files) fs.unlinkSync(file);
103
+ for (const store of stores) {
104
+ await store.onReady();
105
+ await store.close();
106
+ await store.connection.end();
107
+ }
108
+ await harness.stop();
109
+ }
110
+ })().then(() => process.exit(0), (error) => { console.error(error); process.exit(1); });
@@ -0,0 +1,88 @@
1
+ import { createPool } from "mysql";
2
+ import createRecordQueries, { collectAllowedQueries, sqlAnd } from "../../src/api/queries";
3
+ import dbManager from "../../src/utils/dbManager";
4
+ import {
5
+ DbHarness, describeIntegration, getMysqlHarness, getMysql2Harness, getPgHarness,
6
+ readIntegrationState,
7
+ } from "./dbHarness";
8
+
9
+ for (const [backend, connect] of [
10
+ ["mysql", getMysqlHarness], ["mysql2", getMysql2Harness], ["postgresql", getPgHarness],
11
+ ] as const) {
12
+ describeIntegration(`record query results (${backend})`, () => {
13
+ let harness: DbHarness;
14
+ beforeAll(async () => { harness = await connect(); });
15
+ afterAll(async () => { await harness.stop(); });
16
+
17
+ it("preserves rows, driver count values, and the original input", async () => {
18
+ const queries = createRecordQueries(harness.manager);
19
+ const params = { table: "fruits", customQuery: "SELECT 1 AS id", page: "1", limit: "2", orderBy: null };
20
+ const result = await queries.getRecords(params, true);
21
+ expect(result.error).toBeUndefined();
22
+ expect(result.rows).toEqual([{ id: 1 }]);
23
+ expect(result.count).toBe(backend === "postgresql" ? "1" : 1);
24
+ expect(result.input?.params).toBe(params);
25
+ expect(params.orderBy).toBeNull();
26
+ expect(Object.keys(result)).toEqual(["rows", "queryString", "input", "count"]);
27
+ const adjacent = { table: "fruits", customQuery: "SELECT 1 AS id", position: "1", offset: 0 };
28
+ const found = await queries.findRecordByPosition(adjacent);
29
+ expect(found.input?.params).toBe(adjacent);
30
+ expect(adjacent).toMatchObject({ page: 1, limit: 1 });
31
+ expect(found.count).toBeUndefined();
32
+ });
33
+
34
+ it("preserves the error-only result", async () => {
35
+ const result = await createRecordQueries(harness.manager).getRecords({
36
+ table: "port_missing_table", page: 1, limit: 1,
37
+ });
38
+ expect(result.error).toBeInstanceOf(Error);
39
+ expect(Object.keys(result)).toEqual(["error"]);
40
+ });
41
+
42
+ if (backend !== "postgresql") {
43
+ it("uses the unpaged row count when the count subquery fails", async () => {
44
+ const result = await createRecordQueries(harness.manager).getRecords({
45
+ table: "fruits", customQuery: "SELECT 1 AS id, 2 AS id", page: 1, limit: 1,
46
+ }, true);
47
+ expect(result.rows).toEqual([{ id: 2 }]);
48
+ expect(result.count).toBe(1);
49
+ });
50
+ } else {
51
+ it("rejects absent rows from a PostgreSQL multiple-result response", async () => {
52
+ await expect(createRecordQueries(harness.manager).getRecords({
53
+ table: "fruits", customQuery: "SELECT 1 AS id; SELECT 2 AS id", page: 1, limit: 1,
54
+ })).rejects.toThrow("Invalid record query rows");
55
+ });
56
+ }
57
+ });
58
+ }
59
+
60
+ describeIntegration("record query boundary", () => {
61
+ it("rejects MySQL nested result sets and mutation headers", async () => {
62
+ const conn = readIntegrationState()?.mysql;
63
+ if (!conn) throw new Error("Missing MySQL integration connection");
64
+ const pool = createPool({ ...conn, multipleStatements: true });
65
+ const manager = new dbManager();
66
+ manager.dbType = "MYSQL";
67
+ manager._db = pool;
68
+ try {
69
+ for (const customQuery of ["SELECT 1 AS id; SELECT 2 AS id", "SET @port_query_value = 1 -- "]) {
70
+ await expect(createRecordQueries(manager).getRecords({
71
+ table: "fruits", customQuery, page: 1, limit: 1,
72
+ })).rejects.toThrow("Invalid record query rows");
73
+ }
74
+ } finally {
75
+ await new Promise<void>((resolve, reject) => pool.end((err) => err ? reject(err) : resolve()));
76
+ }
77
+ });
78
+
79
+ it("preserves filter array identity and recursive template collection", () => {
80
+ const filters = ["id = 1"];
81
+ expect(sqlAnd([null, filters, undefined])).toBe(filters);
82
+ expect(sqlAnd([filters, "id > 0"])).toBe("(id = 1) AND (id > 0)");
83
+ const acc = new Set(["SELECT 0"]);
84
+ const config = [{ nested: { autocompleteListQuery: " SELECT 1 " } }, null, () => "ignored"];
85
+ expect(collectAllowedQueries(config, acc)).toBe(acc);
86
+ expect(acc).toEqual(new Set(["SELECT 0", "SELECT 1"]));
87
+ });
88
+ });
@@ -0,0 +1,118 @@
1
+ import * as request from "supertest";
2
+ import type { Column, Config } from "../../src/types/config";
3
+ import { buildAppForManager } from "../helpers/app";
4
+ import { DbHarness, describeIntegration, getMysqlHarness, resetTables } from "./dbHarness";
5
+
6
+ describeIntegration("request configuration data operations", () => {
7
+ let harness: DbHarness;
8
+ beforeAll(async () => { harness = await getMysqlHarness(); });
9
+ afterAll(async () => { await harness?.stop(); });
10
+ beforeEach(async () => { await resetTables(harness); });
11
+
12
+ describe("hidden column names", () => {
13
+ beforeAll(async () => {
14
+ const { err } = await harness.query(
15
+ "CREATE TABLE api_hidden_names (id INT PRIMARY KEY, `undefined` VARCHAR(255), `null` VARCHAR(255), note VARCHAR(255))",
16
+ );
17
+ expect(err).toBeNull();
18
+ });
19
+ afterAll(async () => {
20
+ const { err } = await harness.query("DROP TABLE api_hidden_names");
21
+ expect(err).toBeNull();
22
+ });
23
+ beforeEach(async () => {
24
+ expect((await harness.query("TRUNCATE TABLE api_hidden_names")).err).toBeNull();
25
+ expect((await harness.query(
26
+ "INSERT INTO api_hidden_names (id, `undefined`, `null`, note) VALUES (1, 'undefined value', 'null value', 'private')",
27
+ )).err).toBeNull();
28
+ });
29
+
30
+ const appForColumns = (columns: Column[]) => {
31
+ const config: Config = {
32
+ name: "Hidden names", sessionSecret: "test-secret", enriched: true,
33
+ views: [{ type: "table", label: "Names", tableName: "api_hidden_names", primaryId: "id", columns }],
34
+ };
35
+ const app = buildAppForManager(harness.manager, config, { role: "viewer" });
36
+ app.get("/api/inspectHiddenNames", (req, res) => {
37
+ res.send({ hidden: req.config?.views[0].__hiddenColumns });
38
+ });
39
+ return app;
40
+ };
41
+
42
+ it("preserves named restrictions and empty-string hidden names", async () => {
43
+ const columns: Column[] = [
44
+ { label: "Private", name: "note", roles: ["editor"] },
45
+ { label: "Empty", name: "", roles: ["editor"] },
46
+ ];
47
+ const app = appForColumns(columns);
48
+ await request(app).get("/api/inspectHiddenNames").expect(200, { hidden: ["note", ""] });
49
+ const read = await request(app).get("/api/record").query({ viewIndex: 0, recordId: 1 }).expect(200);
50
+ expect(read.body).toEqual({ id: 1, undefined: "undefined value", null: "null value" });
51
+ await request(app).post("/api/updateRecord")
52
+ .send({ viewIndex: 0, primaryValue: 1, data: { note: "forbidden" } }).expect(403);
53
+ expect((await harness.query("SELECT note FROM api_hidden_names WHERE id = 1")).rows).toEqual([{ note: "private" }]);
54
+ expect(columns.map((column) => (column as { name?: string }).name)).toEqual(["note", ""]);
55
+ const literalApp = appForColumns([
56
+ { label: "Undefined field", name: "undefined", roles: ["editor"] },
57
+ { label: "Null field", name: "null", roles: ["editor"] },
58
+ ]);
59
+ await request(literalApp).get("/api/inspectHiddenNames").expect(200, { hidden: ["undefined", "null"] });
60
+ await request(literalApp).get("/api/record").query({ viewIndex: 0, recordId: 1 })
61
+ .expect(200, { id: 1, note: "private" });
62
+ for (const name of ["undefined", "null"]) {
63
+ await request(literalApp).post("/api/updateRecord")
64
+ .send({ viewIndex: 0, primaryValue: 1, data: { [name]: "forbidden" } }).expect(403);
65
+ }
66
+ });
67
+
68
+ // The externally approved nullish filter changes these original behaviors.
69
+ it.each([undefined, null])("SANITIZATION: omits %s hidden names without hiding their literal property keys", async (name) => {
70
+ const unnamed: Column = { label: "Unnamed", name, roles: ["editor"] };
71
+ const app = appForColumns([unnamed, { label: "Private", name: "note", roles: ["editor"] }]);
72
+ const metadata = await request(app).get("/api/inspectHiddenNames").expect(200);
73
+ const read = await request(app).get("/api/record").query({ viewIndex: 0, recordId: 1 }).expect(200);
74
+ const key = String(name);
75
+ const write = await request(app).post("/api/updateRecord")
76
+ .send({ viewIndex: 0, primaryValue: 1, data: { [key]: "updated" } });
77
+ const { rows, err } = await harness.query("SELECT ?? AS value FROM api_hidden_names WHERE id = 1", [key]);
78
+ expect(err).toBeNull();
79
+ expect(unnamed.name).toBe(name);
80
+ expect(read.body).not.toHaveProperty("note");
81
+ expect({ hidden: metadata.body.hidden, read: read.body[key], write: write.status, rows }).toEqual({
82
+ hidden: ["note"], read: `${key} value`, write: 200, rows: [{ value: "updated" }],
83
+ });
84
+ });
85
+ });
86
+
87
+ it.each(["table", "custom", "iframe", "link"] as const)("preserves configured data operations for %s views", async (type) => {
88
+ const common = {
89
+ label: "Fruit operations",
90
+ tableName: "fruits",
91
+ primaryId: "id",
92
+ columns: [{ label: "Name", name: "fruitName" }],
93
+ };
94
+ const view = type === "table" ? { ...common, type }
95
+ : type === "custom" ? { ...common, type, componentPath: "unused-custom-component.js" }
96
+ : type === "iframe" ? { ...common, type, src: "https://example.com" }
97
+ : { ...common, type, src: "https://example.com" };
98
+ const config: Config = {
99
+ name: "Request config",
100
+ sessionSecret: "test-secret",
101
+ auth: { username: "test", password: "test" },
102
+ enriched: true,
103
+ views: [view],
104
+ };
105
+ const app = buildAppForManager(harness.manager, config);
106
+ await request(app).post("/api/createRecord")
107
+ .send({ viewIndex: 0, data: { fruitName: "pomme" } }).expect(200);
108
+ await request(app).post("/api/updateRecord")
109
+ .send({ viewIndex: 0, primaryValue: 1, data: { fruitName: "poire" } }).expect(200);
110
+ const response = await request(app).post("/api/records")
111
+ .send({ viewIndex: 0, page: 1, filters: [], isDesc: false }).expect(200);
112
+ expect(response.body.records).toHaveLength(1);
113
+ expect(response.body.records[0].fruitName).toBe("poire");
114
+ const { rows, err } = await harness.query("SELECT fruitName FROM fruits WHERE id = ?", [1]);
115
+ expect(err).toBeNull();
116
+ expect(rows).toEqual([{ fruitName: "poire" }]);
117
+ });
118
+ });