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.
- package/.clever.json +12 -0
- package/.dockerignore +13 -0
- package/.env.example +28 -0
- package/.github/workflows/deploy-production.yaml +34 -0
- package/.prettierrc +6 -0
- package/.tasks/F1-bootstrap.md +110 -0
- package/.tasks/F2-domain-layer.md +173 -0
- package/.tasks/F3-application-layer.md +166 -0
- package/.tasks/F4-infrastructure-layer.md +229 -0
- package/.tasks/F5-config-main.md +160 -0
- package/.tasks/F6-schemas-deployment.md +129 -0
- package/CLAUDE.md +163 -0
- package/Dockerfile +15 -0
- package/PRD.md +119 -0
- package/docs/schemas/.gitkeep +0 -0
- package/docs/schemas/_guidelines.md +89 -0
- package/docs/schemas/efarmz_db.md +759 -0
- package/docs/schemas/example.md +16 -0
- package/eslint.config.mjs +18 -0
- package/package.json +54 -0
- package/releaserc.json +15 -0
- package/src/.gitkeep +0 -0
- package/src/application/agent/.gitkeep +0 -0
- package/src/application/agent/AgentContext.test.ts +263 -0
- package/src/application/agent/AgentContext.ts +93 -0
- package/src/application/agent/AgentLoop.test.ts +275 -0
- package/src/application/agent/AgentLoop.ts +101 -0
- package/src/application/agent/AgentRunResult.ts +11 -0
- package/src/application/agent/LLMMessage.ts +16 -0
- package/src/application/agent/tools/RunSqlTool.ts +23 -0
- package/src/application/formatting/.gitkeep +0 -0
- package/src/application/formatting/CsvRenderer.test.ts +162 -0
- package/src/application/formatting/CsvRenderer.ts +34 -0
- package/src/application/formatting/MonospaceTableRenderer.test.ts +129 -0
- package/src/application/formatting/MonospaceTableRenderer.ts +58 -0
- package/src/application/formatting/RenderedResponse.ts +7 -0
- package/src/application/formatting/ResponseRenderer.test.ts +159 -0
- package/src/application/formatting/ResponseRenderer.ts +39 -0
- package/src/application/formatting/ScalarRenderer.test.ts +36 -0
- package/src/application/formatting/ScalarRenderer.ts +12 -0
- package/src/application/usecases/.gitkeep +0 -0
- package/src/application/usecases/AnswerQuestion.test.ts +362 -0
- package/src/application/usecases/AnswerQuestion.ts +69 -0
- package/src/application/usecases/ParseQuestion.test.ts +39 -0
- package/src/application/usecases/ParseQuestion.ts +9 -0
- package/src/config/.gitkeep +0 -0
- package/src/config/Container.test.ts +35 -0
- package/src/config/Container.ts +74 -0
- package/src/config/constants.ts +9 -0
- package/src/config/env.test.ts +103 -0
- package/src/config/env.ts +41 -0
- package/src/domain/entities/.gitkeep +0 -0
- package/src/domain/entities/Conversation.test.ts +69 -0
- package/src/domain/entities/Conversation.ts +26 -0
- package/src/domain/entities/ConversationMessage.test.ts +49 -0
- package/src/domain/entities/ConversationMessage.ts +18 -0
- package/src/domain/entities/index.ts +2 -0
- package/src/domain/errors/.gitkeep +0 -0
- package/src/domain/errors/AgentLoopExceededError.ts +12 -0
- package/src/domain/errors/DomainError.test.ts +106 -0
- package/src/domain/errors/DomainError.ts +11 -0
- package/src/domain/errors/InvalidSqlError.ts +15 -0
- package/src/domain/errors/LLMError.ts +15 -0
- package/src/domain/errors/SchemaLoadError.ts +15 -0
- package/src/domain/errors/SqlExecutionError.ts +15 -0
- package/src/domain/errors/index.ts +15 -0
- package/src/domain/ports/.gitkeep +0 -0
- package/src/domain/ports/AdminLogger.ts +16 -0
- package/src/domain/ports/ConversationRepository.ts +10 -0
- package/src/domain/ports/LLMProvider.ts +33 -0
- package/src/domain/ports/Logger.ts +8 -0
- package/src/domain/ports/SchemaCatalog.ts +5 -0
- package/src/domain/ports/SlackMessenger.ts +8 -0
- package/src/domain/ports/SqlExecutor.ts +8 -0
- package/src/domain/ports/SqlValidator.ts +5 -0
- package/src/domain/ports/index.ts +17 -0
- package/src/domain/value-objects/.gitkeep +0 -0
- package/src/domain/value-objects/LLMProviderName.ts +6 -0
- package/src/domain/value-objects/QueryResult.test.ts +51 -0
- package/src/domain/value-objects/QueryResult.ts +18 -0
- package/src/domain/value-objects/Question.test.ts +59 -0
- package/src/domain/value-objects/Question.ts +22 -0
- package/src/domain/value-objects/QuestionFlags.test.ts +59 -0
- package/src/domain/value-objects/QuestionFlags.ts +18 -0
- package/src/domain/value-objects/ResponseRendering.ts +7 -0
- package/src/domain/value-objects/SqlQuery.test.ts +40 -0
- package/src/domain/value-objects/SqlQuery.ts +12 -0
- package/src/domain/value-objects/ThreadId.test.ts +68 -0
- package/src/domain/value-objects/ThreadId.ts +27 -0
- package/src/domain/value-objects/index.ts +13 -0
- package/src/infrastructure/llm/.gitkeep +0 -0
- package/src/infrastructure/llm/AnthropicLLMProvider.test.ts +229 -0
- package/src/infrastructure/llm/AnthropicLLMProvider.ts +45 -0
- package/src/infrastructure/llm/index.ts +4 -0
- package/src/infrastructure/llm/mappers/AnthropicMessageMapper.test.ts +173 -0
- package/src/infrastructure/llm/mappers/AnthropicMessageMapper.ts +34 -0
- package/src/infrastructure/llm/prompts/SystemPromptBuilder.test.ts +41 -0
- package/src/infrastructure/llm/prompts/SystemPromptBuilder.ts +31 -0
- package/src/infrastructure/llm/prompts/ToolDefinitions.ts +7 -0
- package/src/infrastructure/logging/.gitkeep +0 -0
- package/src/infrastructure/logging/PinoLogger.test.ts +59 -0
- package/src/infrastructure/logging/PinoLogger.ts +28 -0
- package/src/infrastructure/logging/index.ts +1 -0
- package/src/infrastructure/persistence/.gitkeep +0 -0
- package/src/infrastructure/persistence/InMemoryConversationRepository.test.ts +325 -0
- package/src/infrastructure/persistence/InMemoryConversationRepository.ts +69 -0
- package/src/infrastructure/persistence/PostgresPoolFactory.ts +11 -0
- package/src/infrastructure/persistence/PostgresSqlExecutor.test.ts +130 -0
- package/src/infrastructure/persistence/PostgresSqlExecutor.ts +34 -0
- package/src/infrastructure/persistence/index.ts +3 -0
- package/src/infrastructure/schemas/.gitkeep +0 -0
- package/src/infrastructure/schemas/FileSystemSchemaCatalog.test.ts +163 -0
- package/src/infrastructure/schemas/FileSystemSchemaCatalog.ts +35 -0
- package/src/infrastructure/schemas/index.ts +4 -0
- package/src/infrastructure/slack/.gitkeep +0 -0
- package/src/infrastructure/slack/BoltSlackMessenger.test.ts +59 -0
- package/src/infrastructure/slack/BoltSlackMessenger.ts +36 -0
- package/src/infrastructure/slack/SlackAdminLogger.test.ts +54 -0
- package/src/infrastructure/slack/SlackAdminLogger.ts +27 -0
- package/src/infrastructure/slack/SlackApp.ts +9 -0
- package/src/infrastructure/slack/handlers/AppMentionHandler.ts +52 -0
- package/src/infrastructure/slack/handlers/DirectMessageHandler.ts +65 -0
- package/src/infrastructure/slack/index.ts +5 -0
- package/src/infrastructure/sql/.gitkeep +0 -0
- package/src/infrastructure/sql/RegexSqlValidator.test.ts +242 -0
- package/src/infrastructure/sql/RegexSqlValidator.ts +53 -0
- package/src/infrastructure/sql/index.ts +1 -0
- package/src/main.ts +19 -0
- package/tsconfig.json +23 -0
- package/vitest.config.ts +15 -0
- package/vitest.setup.ts +23 -0
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import RegexSqlValidator from "./RegexSqlValidator.js";
|
|
3
|
+
import InvalidSqlError from "@/domain/errors/InvalidSqlError.js";
|
|
4
|
+
import { SQL_MAX_ROWS } from "@/config/constants.js";
|
|
5
|
+
|
|
6
|
+
describe("RegexSqlValidator", () => {
|
|
7
|
+
const validator = new RegexSqlValidator();
|
|
8
|
+
|
|
9
|
+
describe("valid queries", () => {
|
|
10
|
+
it("accepts SELECT query and adds LIMIT", () => {
|
|
11
|
+
const result = validator.validate(`SELECT * FROM orders`);
|
|
12
|
+
expect(result).toBe(`SELECT * FROM orders LIMIT ${SQL_MAX_ROWS}`);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("accepts SELECT with lowercase", () => {
|
|
16
|
+
const result = validator.validate(`select * from orders`);
|
|
17
|
+
expect(result).toBe(`select * from orders LIMIT ${SQL_MAX_ROWS}`);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("accepts WITH query", () => {
|
|
21
|
+
const result = validator.validate(`WITH cte AS (SELECT 1) SELECT * FROM cte`);
|
|
22
|
+
expect(result).toBe(`WITH cte AS (SELECT 1) SELECT * FROM cte LIMIT ${SQL_MAX_ROWS}`);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("accepts WITH with lowercase", () => {
|
|
26
|
+
const result = validator.validate(`with cte as (select 1) select * from cte`);
|
|
27
|
+
expect(result).toBe(`with cte as (select 1) select * from cte LIMIT ${SQL_MAX_ROWS}`);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("does not duplicate LIMIT when present", () => {
|
|
31
|
+
const result = validator.validate(`SELECT * FROM orders LIMIT 10`);
|
|
32
|
+
expect(result).toBe(`SELECT * FROM orders LIMIT 10`);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("accepts trailing semicolon", () => {
|
|
36
|
+
const result = validator.validate(`SELECT * FROM orders;`);
|
|
37
|
+
expect(result).toBe(`SELECT * FROM orders LIMIT ${SQL_MAX_ROWS}`);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("accepts keyword in string literal", () => {
|
|
41
|
+
const result = validator.validate(
|
|
42
|
+
`SELECT * FROM orders WHERE name LIKE 'drop%'`,
|
|
43
|
+
);
|
|
44
|
+
expect(result).toBe(
|
|
45
|
+
`SELECT * FROM orders WHERE name LIKE 'drop%' LIMIT ${SQL_MAX_ROWS}`,
|
|
46
|
+
);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("normalizes multiple whitespaces", () => {
|
|
50
|
+
const result = validator.validate(`SELECT * FROM orders`);
|
|
51
|
+
expect(result).toBe(`SELECT * FROM orders LIMIT ${SQL_MAX_ROWS}`);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("normalizes leading and trailing whitespace", () => {
|
|
55
|
+
const result = validator.validate(` SELECT * FROM orders `);
|
|
56
|
+
expect(result).toBe(`SELECT * FROM orders LIMIT ${SQL_MAX_ROWS}`);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("normalizes spacing in LIMIT clause", () => {
|
|
60
|
+
const result = validator.validate(`SELECT * FROM orders LIMIT 50`);
|
|
61
|
+
expect(result).toBe(`SELECT * FROM orders LIMIT 50`);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("handles complex WITH and SELECT", () => {
|
|
65
|
+
const sql = `WITH RECURSIVE nums AS (SELECT 1 AS n UNION ALL SELECT n + 1 FROM nums WHERE n < 100) SELECT * FROM nums`;
|
|
66
|
+
const result = validator.validate(sql);
|
|
67
|
+
expect(result).toContain(`LIMIT ${SQL_MAX_ROWS}`);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe("invalid queries - mutation keywords", () => {
|
|
72
|
+
const mutationKeywords = [
|
|
73
|
+
`INSERT`,
|
|
74
|
+
`UPDATE`,
|
|
75
|
+
`DELETE`,
|
|
76
|
+
`DROP`,
|
|
77
|
+
`CREATE`,
|
|
78
|
+
`ALTER`,
|
|
79
|
+
`TRUNCATE`,
|
|
80
|
+
`GRANT`,
|
|
81
|
+
`REVOKE`,
|
|
82
|
+
`COPY`,
|
|
83
|
+
`CALL`,
|
|
84
|
+
`VACUUM`,
|
|
85
|
+
`MERGE`,
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
mutationKeywords.forEach((keyword) => {
|
|
89
|
+
it(`rejects ${keyword} keyword`, () => {
|
|
90
|
+
const queries = [
|
|
91
|
+
`${keyword} INTO orders VALUES (1)`,
|
|
92
|
+
`${keyword} orders SET id = 1`,
|
|
93
|
+
`${keyword} TABLE orders`,
|
|
94
|
+
`SELECT * FROM orders; ${keyword} orders`,
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
queries.forEach((query) => {
|
|
98
|
+
if (/^(INSERT|UPDATE|DELETE)/.test(query)) {
|
|
99
|
+
expect(() => validator.validate(query)).toThrow(InvalidSqlError);
|
|
100
|
+
} else {
|
|
101
|
+
expect(() => validator.validate(query)).toThrow(InvalidSqlError);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe("invalid queries - structure", () => {
|
|
109
|
+
it("rejects empty string", () => {
|
|
110
|
+
expect(() => validator.validate(``)).toThrow(InvalidSqlError);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("rejects whitespace only", () => {
|
|
114
|
+
expect(() => validator.validate(` `)).toThrow(InvalidSqlError);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("rejects query not starting with SELECT or WITH", () => {
|
|
118
|
+
expect(() => validator.validate(`EXPLAIN SELECT * FROM orders`)).toThrow(
|
|
119
|
+
InvalidSqlError,
|
|
120
|
+
);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("rejects multiple statements with semicolon separator", () => {
|
|
124
|
+
expect(() =>
|
|
125
|
+
validator.validate(`SELECT * FROM orders; DROP TABLE orders`),
|
|
126
|
+
).toThrow(InvalidSqlError);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("rejects semicolon in middle of query", () => {
|
|
130
|
+
expect(() =>
|
|
131
|
+
validator.validate(
|
|
132
|
+
`SELECT * FROM orders WHERE id = 1; SELECT * FROM users`,
|
|
133
|
+
),
|
|
134
|
+
).toThrow(InvalidSqlError);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("rejects query starting with numbers", () => {
|
|
138
|
+
expect(() => validator.validate(`123 SELECT * FROM orders`)).toThrow(
|
|
139
|
+
InvalidSqlError,
|
|
140
|
+
);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("rejects query starting with comments", () => {
|
|
144
|
+
expect(() => validator.validate(`-- comment\nSELECT *`)).toThrow(
|
|
145
|
+
InvalidSqlError,
|
|
146
|
+
);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe("error messages", () => {
|
|
151
|
+
it("provides meaningful error for empty query", () => {
|
|
152
|
+
try {
|
|
153
|
+
validator.validate(``);
|
|
154
|
+
expect.fail(`Should throw`);
|
|
155
|
+
} catch (error) {
|
|
156
|
+
expect(error).toBeInstanceOf(InvalidSqlError);
|
|
157
|
+
expect((error as InvalidSqlError).reason).toBe(`Query cannot be empty`);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("provides meaningful error for missing SELECT/WITH", () => {
|
|
162
|
+
try {
|
|
163
|
+
validator.validate(`EXPLAIN SELECT * FROM orders`);
|
|
164
|
+
expect.fail(`Should throw`);
|
|
165
|
+
} catch (error) {
|
|
166
|
+
expect(error).toBeInstanceOf(InvalidSqlError);
|
|
167
|
+
expect((error as InvalidSqlError).reason).toContain(
|
|
168
|
+
`must start with SELECT or WITH`,
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("rejects queries starting with mutation keywords", () => {
|
|
174
|
+
try {
|
|
175
|
+
validator.validate(`INSERT INTO orders VALUES (1)`);
|
|
176
|
+
expect.fail(`Should throw`);
|
|
177
|
+
} catch (error) {
|
|
178
|
+
expect(error).toBeInstanceOf(InvalidSqlError);
|
|
179
|
+
expect((error as InvalidSqlError).reason).toContain(
|
|
180
|
+
`must start with SELECT or WITH`,
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
it("provides meaningful error for multiple statements", () => {
|
|
187
|
+
try {
|
|
188
|
+
validator.validate(`SELECT * FROM a; SELECT * FROM b`);
|
|
189
|
+
expect.fail(`Should throw`);
|
|
190
|
+
} catch (error) {
|
|
191
|
+
expect(error).toBeInstanceOf(InvalidSqlError);
|
|
192
|
+
expect((error as InvalidSqlError).reason).toContain(
|
|
193
|
+
`Multiple statements are forbidden`,
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("stores original SQL in error", () => {
|
|
199
|
+
const originalSql = `INVALID SQL HERE`;
|
|
200
|
+
try {
|
|
201
|
+
validator.validate(originalSql);
|
|
202
|
+
expect.fail(`Should throw`);
|
|
203
|
+
} catch (error) {
|
|
204
|
+
expect(error).toBeInstanceOf(InvalidSqlError);
|
|
205
|
+
expect((error as InvalidSqlError).sql).toBe(originalSql);
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
describe("edge cases", () => {
|
|
211
|
+
it("accepts SELECT inside string literal with keyword", () => {
|
|
212
|
+
const result = validator.validate(
|
|
213
|
+
`SELECT * FROM orders WHERE description = 'INSERT INTO backup'`,
|
|
214
|
+
);
|
|
215
|
+
expect(result).toContain(`LIMIT ${SQL_MAX_ROWS}`);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("handles mixed case keywords", () => {
|
|
219
|
+
expect(() =>
|
|
220
|
+
validator.validate(`SeLeCt * FrOm orders; DrOp TaBlE orders`),
|
|
221
|
+
).toThrow(InvalidSqlError);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("accepts word boundary for keywords", () => {
|
|
225
|
+
const result = validator.validate(
|
|
226
|
+
`SELECT * FROM insert_logs WHERE inserted_at > NOW()`,
|
|
227
|
+
);
|
|
228
|
+
expect(result).toContain(`LIMIT ${SQL_MAX_ROWS}`);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("preserves tabs and newlines as single space", () => {
|
|
232
|
+
const result = validator.validate(`SELECT\n*\tFROM\norders`);
|
|
233
|
+
expect(result).toBe(`SELECT * FROM orders LIMIT ${SQL_MAX_ROWS}`);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it("handles very long query", () => {
|
|
237
|
+
const longQuery = `SELECT ${`col1, col2, col3, `.repeat(100)} col_final FROM orders`;
|
|
238
|
+
const result = validator.validate(longQuery);
|
|
239
|
+
expect(result).toContain(`LIMIT ${SQL_MAX_ROWS}`);
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import SqlValidator from "@/domain/ports/SqlValidator.js";
|
|
2
|
+
import InvalidSqlError from "@/domain/errors/InvalidSqlError.js";
|
|
3
|
+
import { SQL_MAX_ROWS } from "@/config/constants.js";
|
|
4
|
+
|
|
5
|
+
class RegexSqlValidator implements SqlValidator {
|
|
6
|
+
private readonly mutationKeywords =
|
|
7
|
+
/\b(INSERT|UPDATE|DELETE|DROP|CREATE|ALTER|TRUNCATE|GRANT|REVOKE|COPY|CALL|VACUUM|MERGE)\b/i;
|
|
8
|
+
|
|
9
|
+
private stripStringLiterals(sql: string): string {
|
|
10
|
+
return sql.replace(/'(?:''|[^'])*'/g, `''`);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
validate(raw: string): string {
|
|
14
|
+
const trimmed = raw.trim().replace(/\s+/g, ` `);
|
|
15
|
+
|
|
16
|
+
if (!trimmed) {
|
|
17
|
+
throw new InvalidSqlError(raw, `Query cannot be empty`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (!/^(SELECT|WITH)\b/i.test(trimmed)) {
|
|
21
|
+
throw new InvalidSqlError(
|
|
22
|
+
raw,
|
|
23
|
+
`Query must start with SELECT or WITH`,
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const sqlWithoutStrings = this.stripStringLiterals(trimmed);
|
|
28
|
+
|
|
29
|
+
if (this.mutationKeywords.test(sqlWithoutStrings)) {
|
|
30
|
+
throw new InvalidSqlError(
|
|
31
|
+
raw,
|
|
32
|
+
`Mutation keywords are forbidden`,
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const withoutTrailingSemicolon = trimmed.replace(/;$/, ``);
|
|
37
|
+
|
|
38
|
+
if (withoutTrailingSemicolon.includes(`;`)) {
|
|
39
|
+
throw new InvalidSqlError(
|
|
40
|
+
raw,
|
|
41
|
+
`Multiple statements are forbidden`,
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!/\bLIMIT\s+\d+/i.test(trimmed)) {
|
|
46
|
+
return `${withoutTrailingSemicolon} LIMIT ${SQL_MAX_ROWS}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return withoutTrailingSemicolon;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export default RegexSqlValidator;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as RegexSqlValidator } from "./RegexSqlValidator.js";
|
package/src/main.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { createContainer } from '@/config/Container.js';
|
|
2
|
+
import { env } from '@/config/env.js';
|
|
3
|
+
import registerAppMentionHandler from '@/infrastructure/slack/handlers/AppMentionHandler.js';
|
|
4
|
+
import registerDirectMessageHandler from '@/infrastructure/slack/handlers/DirectMessageHandler.js';
|
|
5
|
+
|
|
6
|
+
async function main() {
|
|
7
|
+
const { app, answerQuestion, logger } = createContainer();
|
|
8
|
+
|
|
9
|
+
registerAppMentionHandler(app, answerQuestion, logger);
|
|
10
|
+
registerDirectMessageHandler(app, answerQuestion, logger);
|
|
11
|
+
|
|
12
|
+
await app.start(env.PORT);
|
|
13
|
+
logger.info(`Bot started on port ${env.PORT.toString()}`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
main().catch((err: unknown) => {
|
|
17
|
+
console.error(`Fatal error at startup:`, err);
|
|
18
|
+
process.exit(1);
|
|
19
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"noUncheckedIndexedAccess": true,
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"forceConsistentCasingInFileNames": true,
|
|
11
|
+
"resolveJsonModule": true,
|
|
12
|
+
"declaration": true,
|
|
13
|
+
"declarationMap": true,
|
|
14
|
+
"sourceMap": true,
|
|
15
|
+
"paths": {
|
|
16
|
+
"@/*": ["./src/*"]
|
|
17
|
+
},
|
|
18
|
+
"outDir": "dist",
|
|
19
|
+
"rootDir": "src"
|
|
20
|
+
},
|
|
21
|
+
"include": ["src"],
|
|
22
|
+
"exclude": ["node_modules", "dist"]
|
|
23
|
+
}
|
package/vitest.config.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { defineConfig } from 'vitest/config';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
export default defineConfig({
|
|
5
|
+
test: {
|
|
6
|
+
globals: true,
|
|
7
|
+
passWithNoTests: true,
|
|
8
|
+
setupFiles: ['./vitest.setup.ts'],
|
|
9
|
+
},
|
|
10
|
+
resolve: {
|
|
11
|
+
alias: {
|
|
12
|
+
'@': path.resolve(__dirname, './src'),
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
});
|
package/vitest.setup.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { readFileSync } from "fs";
|
|
2
|
+
import { fileURLToPath } from "url";
|
|
3
|
+
import { dirname, join } from "path";
|
|
4
|
+
|
|
5
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
6
|
+
const __dirname = dirname(__filename);
|
|
7
|
+
const envPath = join(__dirname, ".env.test");
|
|
8
|
+
|
|
9
|
+
try {
|
|
10
|
+
const envContent = readFileSync(envPath, "utf-8");
|
|
11
|
+
const lines = envContent.split("\n");
|
|
12
|
+
for (const line of lines) {
|
|
13
|
+
const trimmed = line.trim();
|
|
14
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
15
|
+
const [key, ...valueParts] = trimmed.split("=");
|
|
16
|
+
const value = valueParts.join("=");
|
|
17
|
+
if (key && !process.env[key]) {
|
|
18
|
+
process.env[key] = value;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
} catch {
|
|
22
|
+
console.warn("Could not load .env.test file");
|
|
23
|
+
}
|