efarmz-slackbot-data 1.0.0

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 (131) hide show
  1. package/.clever.json +12 -0
  2. package/.dockerignore +13 -0
  3. package/.env.example +28 -0
  4. package/.github/workflows/deploy-production.yaml +34 -0
  5. package/.prettierrc +6 -0
  6. package/.tasks/F1-bootstrap.md +110 -0
  7. package/.tasks/F2-domain-layer.md +173 -0
  8. package/.tasks/F3-application-layer.md +166 -0
  9. package/.tasks/F4-infrastructure-layer.md +229 -0
  10. package/.tasks/F5-config-main.md +160 -0
  11. package/.tasks/F6-schemas-deployment.md +129 -0
  12. package/CLAUDE.md +163 -0
  13. package/Dockerfile +15 -0
  14. package/PRD.md +119 -0
  15. package/docs/schemas/.gitkeep +0 -0
  16. package/docs/schemas/_guidelines.md +89 -0
  17. package/docs/schemas/efarmz_db.md +759 -0
  18. package/docs/schemas/example.md +16 -0
  19. package/eslint.config.mjs +18 -0
  20. package/package.json +54 -0
  21. package/releaserc.json +15 -0
  22. package/src/.gitkeep +0 -0
  23. package/src/application/agent/.gitkeep +0 -0
  24. package/src/application/agent/AgentContext.test.ts +263 -0
  25. package/src/application/agent/AgentContext.ts +93 -0
  26. package/src/application/agent/AgentLoop.test.ts +275 -0
  27. package/src/application/agent/AgentLoop.ts +101 -0
  28. package/src/application/agent/AgentRunResult.ts +11 -0
  29. package/src/application/agent/LLMMessage.ts +16 -0
  30. package/src/application/agent/tools/RunSqlTool.ts +23 -0
  31. package/src/application/formatting/.gitkeep +0 -0
  32. package/src/application/formatting/CsvRenderer.test.ts +162 -0
  33. package/src/application/formatting/CsvRenderer.ts +34 -0
  34. package/src/application/formatting/MonospaceTableRenderer.test.ts +129 -0
  35. package/src/application/formatting/MonospaceTableRenderer.ts +58 -0
  36. package/src/application/formatting/RenderedResponse.ts +7 -0
  37. package/src/application/formatting/ResponseRenderer.test.ts +159 -0
  38. package/src/application/formatting/ResponseRenderer.ts +39 -0
  39. package/src/application/formatting/ScalarRenderer.test.ts +36 -0
  40. package/src/application/formatting/ScalarRenderer.ts +12 -0
  41. package/src/application/usecases/.gitkeep +0 -0
  42. package/src/application/usecases/AnswerQuestion.test.ts +362 -0
  43. package/src/application/usecases/AnswerQuestion.ts +69 -0
  44. package/src/application/usecases/ParseQuestion.test.ts +39 -0
  45. package/src/application/usecases/ParseQuestion.ts +9 -0
  46. package/src/config/.gitkeep +0 -0
  47. package/src/config/Container.test.ts +35 -0
  48. package/src/config/Container.ts +74 -0
  49. package/src/config/constants.ts +9 -0
  50. package/src/config/env.test.ts +103 -0
  51. package/src/config/env.ts +41 -0
  52. package/src/domain/entities/.gitkeep +0 -0
  53. package/src/domain/entities/Conversation.test.ts +69 -0
  54. package/src/domain/entities/Conversation.ts +26 -0
  55. package/src/domain/entities/ConversationMessage.test.ts +49 -0
  56. package/src/domain/entities/ConversationMessage.ts +18 -0
  57. package/src/domain/entities/index.ts +2 -0
  58. package/src/domain/errors/.gitkeep +0 -0
  59. package/src/domain/errors/AgentLoopExceededError.ts +12 -0
  60. package/src/domain/errors/DomainError.test.ts +106 -0
  61. package/src/domain/errors/DomainError.ts +11 -0
  62. package/src/domain/errors/InvalidSqlError.ts +15 -0
  63. package/src/domain/errors/LLMError.ts +15 -0
  64. package/src/domain/errors/SchemaLoadError.ts +15 -0
  65. package/src/domain/errors/SqlExecutionError.ts +15 -0
  66. package/src/domain/errors/index.ts +15 -0
  67. package/src/domain/ports/.gitkeep +0 -0
  68. package/src/domain/ports/AdminLogger.ts +16 -0
  69. package/src/domain/ports/ConversationRepository.ts +10 -0
  70. package/src/domain/ports/LLMProvider.ts +33 -0
  71. package/src/domain/ports/Logger.ts +8 -0
  72. package/src/domain/ports/SchemaCatalog.ts +5 -0
  73. package/src/domain/ports/SlackMessenger.ts +8 -0
  74. package/src/domain/ports/SqlExecutor.ts +8 -0
  75. package/src/domain/ports/SqlValidator.ts +5 -0
  76. package/src/domain/ports/index.ts +17 -0
  77. package/src/domain/value-objects/.gitkeep +0 -0
  78. package/src/domain/value-objects/LLMProviderName.ts +6 -0
  79. package/src/domain/value-objects/QueryResult.test.ts +51 -0
  80. package/src/domain/value-objects/QueryResult.ts +18 -0
  81. package/src/domain/value-objects/Question.test.ts +59 -0
  82. package/src/domain/value-objects/Question.ts +22 -0
  83. package/src/domain/value-objects/QuestionFlags.test.ts +59 -0
  84. package/src/domain/value-objects/QuestionFlags.ts +18 -0
  85. package/src/domain/value-objects/ResponseRendering.ts +7 -0
  86. package/src/domain/value-objects/SqlQuery.test.ts +40 -0
  87. package/src/domain/value-objects/SqlQuery.ts +12 -0
  88. package/src/domain/value-objects/ThreadId.test.ts +68 -0
  89. package/src/domain/value-objects/ThreadId.ts +27 -0
  90. package/src/domain/value-objects/index.ts +13 -0
  91. package/src/infrastructure/llm/.gitkeep +0 -0
  92. package/src/infrastructure/llm/AnthropicLLMProvider.test.ts +229 -0
  93. package/src/infrastructure/llm/AnthropicLLMProvider.ts +45 -0
  94. package/src/infrastructure/llm/index.ts +4 -0
  95. package/src/infrastructure/llm/mappers/AnthropicMessageMapper.test.ts +173 -0
  96. package/src/infrastructure/llm/mappers/AnthropicMessageMapper.ts +34 -0
  97. package/src/infrastructure/llm/prompts/SystemPromptBuilder.test.ts +41 -0
  98. package/src/infrastructure/llm/prompts/SystemPromptBuilder.ts +31 -0
  99. package/src/infrastructure/llm/prompts/ToolDefinitions.ts +7 -0
  100. package/src/infrastructure/logging/.gitkeep +0 -0
  101. package/src/infrastructure/logging/PinoLogger.test.ts +59 -0
  102. package/src/infrastructure/logging/PinoLogger.ts +28 -0
  103. package/src/infrastructure/logging/index.ts +1 -0
  104. package/src/infrastructure/persistence/.gitkeep +0 -0
  105. package/src/infrastructure/persistence/InMemoryConversationRepository.test.ts +325 -0
  106. package/src/infrastructure/persistence/InMemoryConversationRepository.ts +69 -0
  107. package/src/infrastructure/persistence/PostgresPoolFactory.ts +11 -0
  108. package/src/infrastructure/persistence/PostgresSqlExecutor.test.ts +130 -0
  109. package/src/infrastructure/persistence/PostgresSqlExecutor.ts +34 -0
  110. package/src/infrastructure/persistence/index.ts +3 -0
  111. package/src/infrastructure/schemas/.gitkeep +0 -0
  112. package/src/infrastructure/schemas/FileSystemSchemaCatalog.test.ts +163 -0
  113. package/src/infrastructure/schemas/FileSystemSchemaCatalog.ts +35 -0
  114. package/src/infrastructure/schemas/index.ts +4 -0
  115. package/src/infrastructure/slack/.gitkeep +0 -0
  116. package/src/infrastructure/slack/BoltSlackMessenger.test.ts +59 -0
  117. package/src/infrastructure/slack/BoltSlackMessenger.ts +36 -0
  118. package/src/infrastructure/slack/SlackAdminLogger.test.ts +54 -0
  119. package/src/infrastructure/slack/SlackAdminLogger.ts +27 -0
  120. package/src/infrastructure/slack/SlackApp.ts +9 -0
  121. package/src/infrastructure/slack/handlers/AppMentionHandler.ts +52 -0
  122. package/src/infrastructure/slack/handlers/DirectMessageHandler.ts +65 -0
  123. package/src/infrastructure/slack/index.ts +5 -0
  124. package/src/infrastructure/sql/.gitkeep +0 -0
  125. package/src/infrastructure/sql/RegexSqlValidator.test.ts +242 -0
  126. package/src/infrastructure/sql/RegexSqlValidator.ts +53 -0
  127. package/src/infrastructure/sql/index.ts +1 -0
  128. package/src/main.ts +19 -0
  129. package/tsconfig.json +23 -0
  130. package/vitest.config.ts +15 -0
  131. package/vitest.setup.ts +23 -0
@@ -0,0 +1,129 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import MonospaceTableRenderer from "../formatting/MonospaceTableRenderer";
3
+ import QueryResult from "@/domain/value-objects/QueryResult";
4
+
5
+ describe("MonospaceTableRenderer", () => {
6
+ it("renders simple 2x2 table with proper alignment", () => {
7
+ const result = QueryResult.create(
8
+ ["id", "name"],
9
+ [
10
+ { id: 1, name: "Alice" },
11
+ { id: 2, name: "Bob" },
12
+ ]
13
+ );
14
+
15
+ const output = MonospaceTableRenderer.render(result);
16
+
17
+ expect(output).toContain("```");
18
+ expect(output).toContain("id name");
19
+ expect(output).toContain("-- ----");
20
+ expect(output).toContain("1 Alice");
21
+ expect(output).toContain("2 Bob");
22
+ });
23
+
24
+ it("handles columns with different value widths", () => {
25
+ const result = QueryResult.create(
26
+ ["short", "much_longer_value"],
27
+ [
28
+ { short: "x", much_longer_value: "value" },
29
+ { short: "abc", much_longer_value: "v" },
30
+ ]
31
+ );
32
+
33
+ const output = MonospaceTableRenderer.render(result);
34
+
35
+ expect(output).toContain("```");
36
+ expect(output).toContain("short");
37
+ expect(output).toContain("much_longer_value");
38
+ expect(output).toMatch(/-----\s+-+/);
39
+ });
40
+
41
+ it("displays null values as empty", () => {
42
+ const result = QueryResult.create(
43
+ ["col1", "col2"],
44
+ [{ col1: "value", col2: null }]
45
+ );
46
+
47
+ const output = MonospaceTableRenderer.render(result);
48
+
49
+ expect(output).toContain("col1 col2");
50
+ expect(output).toContain("value");
51
+ });
52
+
53
+ it("truncates rows when maxRows is specified", () => {
54
+ const result = QueryResult.create(
55
+ ["id"],
56
+ [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }, { id: 5 }]
57
+ );
58
+
59
+ const output = MonospaceTableRenderer.render(result, 2);
60
+
61
+ expect(output).toMatch(/^1\s/m);
62
+ expect(output).toMatch(/^2\s/m);
63
+ expect(output).not.toMatch(/^3\s/m);
64
+ expect(output).toContain("... (3 lignes supplémentaires)");
65
+ });
66
+
67
+ it("uses correct singular for single remaining line", () => {
68
+ const result = QueryResult.create(
69
+ ["id"],
70
+ [{ id: 1 }, { id: 2 }]
71
+ );
72
+
73
+ const output = MonospaceTableRenderer.render(result, 1);
74
+
75
+ expect(output).toContain("1 ligne supplémentaire");
76
+ expect(output).not.toContain("lignes supplémentaires");
77
+ });
78
+
79
+ it("handles empty table gracefully", () => {
80
+ const result = QueryResult.create([], []);
81
+
82
+ const output = MonospaceTableRenderer.render(result);
83
+
84
+ expect(output).toContain("(empty table)");
85
+ });
86
+
87
+ it("handles single row table", () => {
88
+ const result = QueryResult.create(
89
+ ["name", "age"],
90
+ [{ name: "Alice", age: 30 }]
91
+ );
92
+
93
+ const output = MonospaceTableRenderer.render(result);
94
+
95
+ expect(output).toContain("name");
96
+ expect(output).toContain("age");
97
+ expect(output).toContain("Alice");
98
+ expect(output).toContain("30");
99
+ });
100
+
101
+ it("handles numeric and string values consistently", () => {
102
+ const result = QueryResult.create(
103
+ ["count", "description"],
104
+ [
105
+ { count: 42, description: "the answer" },
106
+ { count: 0, description: "" },
107
+ ]
108
+ );
109
+
110
+ const output = MonospaceTableRenderer.render(result);
111
+
112
+ expect(output).toContain("42");
113
+ expect(output).toContain("the answer");
114
+ expect(output).toContain("0");
115
+ });
116
+
117
+ it("includes header and separator in output", () => {
118
+ const result = QueryResult.create(
119
+ ["id", "name"],
120
+ [{ id: 1, name: "Test" }]
121
+ );
122
+
123
+ const output = MonospaceTableRenderer.render(result);
124
+ const lines = output.split("\n");
125
+
126
+ expect(lines[1]).toMatch(/^id\s+name$/);
127
+ expect(lines[2]).toMatch(/^--\s+----$/);
128
+ });
129
+ });
@@ -0,0 +1,58 @@
1
+ import type { QueryResult } from "@/domain/value-objects/QueryResult";
2
+
3
+ const render = (result: QueryResult, maxRows?: number): string => {
4
+ const { columns, rows, rowCount } = result;
5
+
6
+ if (columns.length === 0 || rows.length === 0) {
7
+ return `\`\`\`\n(empty table)\n\`\`\``;
8
+ }
9
+
10
+ const widths: number[] = columns.map((col) => {
11
+ const headerWidth = col.length;
12
+ const valueWidths = rows.map((row) => {
13
+ const val = row[col];
14
+ // eslint-disable-next-line @typescript-eslint/no-base-to-string
15
+ const str =
16
+ val === null || val === undefined ? `` : String(val);
17
+ return str.length;
18
+ });
19
+ return Math.max(headerWidth, ...valueWidths);
20
+ });
21
+
22
+ const header = columns
23
+ .map((col, i) => col.padEnd(widths[i] ?? 0))
24
+ .join(` `);
25
+ const separator = widths.map((w) => `-`.repeat(w)).join(` `);
26
+
27
+ const displayRows =
28
+ maxRows !== undefined ? rows.slice(0, maxRows) : rows;
29
+ const lines = displayRows.map((row) =>
30
+ columns
31
+ .map((col, i) => {
32
+ const val = row[col];
33
+ // eslint-disable-next-line @typescript-eslint/no-base-to-string
34
+ const str =
35
+ val === null || val === undefined ? `` : String(val);
36
+ return str.padEnd(widths[i] ?? 0);
37
+ })
38
+ .join(` `)
39
+ );
40
+
41
+ let table = [header, separator, ...lines].join(`\n`);
42
+
43
+ if (maxRows !== undefined && rowCount > maxRows) {
44
+ const remaining = rowCount - maxRows;
45
+ const phrase =
46
+ remaining === 1
47
+ ? `ligne supplémentaire`
48
+ : `lignes supplémentaires`;
49
+ // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
50
+ table += `\n... (${remaining} ${phrase})`;
51
+ }
52
+
53
+ return `\`\`\`\n${table}\n\`\`\``;
54
+ };
55
+
56
+ const MonospaceTableRenderer = { render };
57
+
58
+ export default MonospaceTableRenderer;
@@ -0,0 +1,7 @@
1
+ type RenderedResponse = {
2
+ readonly text: string;
3
+ readonly csvBuffer?: Buffer;
4
+ readonly csvFilename?: string;
5
+ };
6
+
7
+ export default RenderedResponse;
@@ -0,0 +1,159 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import ResponseRenderer from '../formatting/ResponseRenderer';
3
+ import QueryResult from '@/domain/value-objects/QueryResult';
4
+
5
+ describe(`ResponseRenderer`, () => {
6
+ const csvThreshold = 15;
7
+ const renderer = new ResponseRenderer(csvThreshold);
8
+
9
+ it(`returns finalText when queryResult is null`, () => {
10
+ const result = renderer.render({
11
+ finalText: `No data available`,
12
+ queryResult: null,
13
+ executedSqls: [],
14
+ flags: { showSql: false, debug: false, noContext: false },
15
+ });
16
+
17
+ expect(result.text).toBe(`No data available`);
18
+ expect(result.csvBuffer).toBeUndefined();
19
+ expect(result.csvFilename).toBeUndefined();
20
+ });
21
+
22
+ it(`returns "Aucun résultat." when rowCount is 0`, () => {
23
+ const queryResult = QueryResult.create([`id`, `name`], []);
24
+
25
+ const result = renderer.render({
26
+ finalText: `The question was:`,
27
+ queryResult,
28
+ executedSqls: [],
29
+ flags: { showSql: false, debug: false, noContext: false },
30
+ });
31
+
32
+ expect(result.text).toBe(`Aucun résultat.`);
33
+ expect(result.csvBuffer).toBeUndefined();
34
+ expect(result.csvFilename).toBeUndefined();
35
+ });
36
+
37
+ it(`renders scalar (1×1) value in bold`, () => {
38
+ const queryResult = QueryResult.create([`count`], [{ count: 42 }]);
39
+
40
+ const result = renderer.render({
41
+ finalText: `Result:`,
42
+ queryResult,
43
+ executedSqls: [],
44
+ flags: { showSql: false, debug: false, noContext: false },
45
+ });
46
+
47
+ expect(result.text).toBe(`**42**`);
48
+ expect(result.csvBuffer).toBeUndefined();
49
+ expect(result.csvFilename).toBeUndefined();
50
+ });
51
+
52
+ it(`renders table when rowCount ≤ csvThreshold`, () => {
53
+ const rows = Array.from({ length: 5 }, (_, i) => ({
54
+ id: i + 1,
55
+ name: `user${(i + 1).toString()}`,
56
+ }));
57
+ const queryResult = QueryResult.create([`id`, `name`], rows);
58
+
59
+ const result = renderer.render({
60
+ finalText: `Users:`,
61
+ queryResult,
62
+ executedSqls: [],
63
+ flags: { showSql: false, debug: false, noContext: false },
64
+ });
65
+
66
+ expect(result.text).toContain(`\`\`\``);
67
+ expect(result.text).toContain(`id`);
68
+ expect(result.text).toContain(`name`);
69
+ expect(result.csvBuffer).toBeUndefined();
70
+ expect(result.csvFilename).toBeUndefined();
71
+ });
72
+
73
+ it(`renders truncated table + csv when rowCount > csvThreshold`, () => {
74
+ const rows = Array.from({ length: 20 }, (_, i) => ({
75
+ id: i + 1,
76
+ name: `user${(i + 1).toString()}`,
77
+ }));
78
+ const queryResult = QueryResult.create([`id`, `name`], rows);
79
+
80
+ const result = renderer.render({
81
+ finalText: `Users:`,
82
+ queryResult,
83
+ executedSqls: [],
84
+ flags: { showSql: false, debug: false, noContext: false },
85
+ });
86
+
87
+ expect(result.text).toContain(`\`\`\``);
88
+ expect(result.text).toContain(`... (10 lignes supplémentaires)`);
89
+ expect(result.csvBuffer).toBeDefined();
90
+ expect(result.csvFilename).toMatch(/^result_\d+\.csv$/);
91
+ });
92
+
93
+ it(`appends SQL block when showSql flag is true`, () => {
94
+ const queryResult = QueryResult.create([`count`], [{ count: 42 }]);
95
+ const sql = `SELECT COUNT(*) as count FROM users`;
96
+
97
+ const result = renderer.render({
98
+ finalText: `Count:`,
99
+ queryResult,
100
+ executedSqls: [sql],
101
+ flags: { showSql: true, debug: false, noContext: false },
102
+ });
103
+
104
+ expect(result.text).toContain(`**42**`);
105
+ expect(result.text).toContain(`\`\`\`sql`);
106
+ expect(result.text).toContain(sql);
107
+ });
108
+
109
+ it(`appends multiple SQL blocks when multiple queries executed`, () => {
110
+ const queryResult = QueryResult.create([`count`], [{ count: 42 }]);
111
+ const sqls = [
112
+ `SELECT COUNT(*) FROM users`,
113
+ `SELECT * FROM orders LIMIT 10`,
114
+ ];
115
+
116
+ const result = renderer.render({
117
+ finalText: `Results:`,
118
+ queryResult,
119
+ executedSqls: sqls,
120
+ flags: { showSql: true, debug: false, noContext: false },
121
+ });
122
+
123
+ expect(result.text).toContain(sqls[0]);
124
+ expect(result.text).toContain(sqls[1]);
125
+ expect(result.text).toMatch(/```sql[\s\S]*```[\s\S]*```sql[\s\S]*```/);
126
+ });
127
+
128
+ it(`handles table with scalar when rowCount is 1 and columns is 1`, () => {
129
+ const queryResult = QueryResult.create([`total`], [{ total: 999 }]);
130
+
131
+ const result = renderer.render({
132
+ finalText: `Total orders:`,
133
+ queryResult,
134
+ executedSqls: [],
135
+ flags: { showSql: false, debug: false, noContext: false },
136
+ });
137
+
138
+ expect(result.text).toBe(`**999**`);
139
+ });
140
+
141
+ it(`respects custom csvThreshold`, () => {
142
+ const rendererSmallThreshold = new ResponseRenderer(3);
143
+ const rows = Array.from({ length: 5 }, (_, i) => ({
144
+ id: i + 1,
145
+ value: i * 10,
146
+ }));
147
+ const queryResult = QueryResult.create([`id`, `value`], rows);
148
+
149
+ const result = rendererSmallThreshold.render({
150
+ finalText: `Data:`,
151
+ queryResult,
152
+ executedSqls: [],
153
+ flags: { showSql: false, debug: false, noContext: false },
154
+ });
155
+
156
+ expect(result.csvBuffer).toBeDefined();
157
+ expect(result.csvFilename).toBeDefined();
158
+ });
159
+ });
@@ -0,0 +1,39 @@
1
+ import type { QueryResult } from "@/domain/value-objects/QueryResult";
2
+ import type { QuestionFlagsType } from "@/domain/value-objects/QuestionFlags";
3
+ import type RenderedResponse from "./RenderedResponse";
4
+ import CsvRenderer from "./CsvRenderer";
5
+
6
+ type RenderParams = {
7
+ readonly finalText: string;
8
+ readonly queryResult: QueryResult | null;
9
+ readonly executedSqls: readonly string[];
10
+ readonly flags: QuestionFlagsType;
11
+ };
12
+
13
+ class ResponseRenderer {
14
+ constructor(private readonly csvThreshold: number) {}
15
+
16
+ render(params: RenderParams): RenderedResponse {
17
+ const { finalText, queryResult, executedSqls, flags } = params;
18
+
19
+ let text = finalText;
20
+ let csvBuffer: Buffer | undefined;
21
+ let csvFilename: string | undefined;
22
+
23
+ if (queryResult !== null && queryResult.rowCount > this.csvThreshold) {
24
+ csvBuffer = CsvRenderer.render(queryResult);
25
+ csvFilename = `result_${Date.now()}.csv`;
26
+ }
27
+
28
+ if (flags.showSql && executedSqls.length > 0) {
29
+ const sqlBlock = executedSqls
30
+ .map((sql) => `\`\`\`sql\n${sql}\n\`\`\``)
31
+ .join(`\n`);
32
+ text = `${text}\n\n${sqlBlock}`;
33
+ }
34
+
35
+ return { text, csvBuffer, csvFilename };
36
+ }
37
+ }
38
+
39
+ export default ResponseRenderer;
@@ -0,0 +1,36 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import ScalarRenderer from "../formatting/ScalarRenderer";
3
+
4
+ describe(`ScalarRenderer`, () => {
5
+ it(`renders number as bold`, () => {
6
+ expect(ScalarRenderer.render(42)).toBe(`**42**`);
7
+ });
8
+
9
+ it(`renders string as bold`, () => {
10
+ expect(ScalarRenderer.render(`hello`)).toBe(`**hello**`);
11
+ });
12
+
13
+ it(`renders null as N/A`, () => {
14
+ expect(ScalarRenderer.render(null)).toBe(`**N/A**`);
15
+ });
16
+
17
+ it(`renders undefined as N/A`, () => {
18
+ expect(ScalarRenderer.render(undefined)).toBe(`**N/A**`);
19
+ });
20
+
21
+ it(`renders zero as bold`, () => {
22
+ expect(ScalarRenderer.render(0)).toBe(`**0**`);
23
+ });
24
+
25
+ it(`renders empty string as bold`, () => {
26
+ expect(ScalarRenderer.render(``)).toBe(`****`);
27
+ });
28
+
29
+ it(`renders boolean true as bold`, () => {
30
+ expect(ScalarRenderer.render(true)).toBe(`**true**`);
31
+ });
32
+
33
+ it(`renders boolean false as bold`, () => {
34
+ expect(ScalarRenderer.render(false)).toBe(`**false**`);
35
+ });
36
+ });
@@ -0,0 +1,12 @@
1
+ const render = (value: unknown): string => {
2
+ if (value === null || value === undefined) {
3
+ return `*N/A*`;
4
+ }
5
+ // eslint-disable-next-line @typescript-eslint/no-base-to-string
6
+ const str = typeof value === `string` ? value : String(value);
7
+ return `*${str}*`;
8
+ };
9
+
10
+ const ScalarRenderer = { render };
11
+
12
+ export default ScalarRenderer;
File without changes