@gokiteam/goki-dev 0.2.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 (205) hide show
  1. package/README.md +478 -0
  2. package/bin/goki-dev.js +452 -0
  3. package/bin/mcp-server.js +16 -0
  4. package/bin/secrets-cli.js +302 -0
  5. package/cli/ComposeOverrideGenerator.js +226 -0
  6. package/cli/ComposeParser.js +73 -0
  7. package/cli/ConfigGenerator.js +304 -0
  8. package/cli/ConfigManager.js +46 -0
  9. package/cli/DatabaseManager.js +94 -0
  10. package/cli/DevToolsChecker.js +21 -0
  11. package/cli/DevToolsDir.js +66 -0
  12. package/cli/DevToolsManager.js +451 -0
  13. package/cli/DockerManager.js +138 -0
  14. package/cli/FunctionManager.js +95 -0
  15. package/cli/HttpProxyRewriter.js +91 -0
  16. package/cli/Logger.js +10 -0
  17. package/cli/McpConfigManager.js +123 -0
  18. package/cli/NgrokManager.js +431 -0
  19. package/cli/ProjectCLI.js +2322 -0
  20. package/cli/PubSubManager.js +129 -0
  21. package/cli/SnapshotManager.js +88 -0
  22. package/cli/UiFormatter.js +292 -0
  23. package/cli/WebhookUrlRewriter.js +32 -0
  24. package/cli/secrets/BiometricAuth.js +125 -0
  25. package/cli/secrets/SecretInjector.js +47 -0
  26. package/cli/secrets/SecretsConfig.js +141 -0
  27. package/cli/secrets/SecretsDoctor.js +384 -0
  28. package/cli/secrets/SecretsManager.js +255 -0
  29. package/client/dist/client.d.ts +332 -0
  30. package/client/dist/client.js +507 -0
  31. package/client/dist/helpers.d.ts +62 -0
  32. package/client/dist/helpers.js +122 -0
  33. package/client/dist/index.d.ts +59 -0
  34. package/client/dist/index.js +78 -0
  35. package/client/dist/package.json +1 -0
  36. package/client/dist/types.d.ts +280 -0
  37. package/client/dist/types.js +7 -0
  38. package/config.development +46 -0
  39. package/config.test +18 -0
  40. package/guidelines/CodingStyleGuideline.md +148 -0
  41. package/guidelines/CommentingGuideline.md +10 -0
  42. package/guidelines/HttpApiImplementationGuideline.md +137 -0
  43. package/guidelines/NamingGuideline.md +182 -0
  44. package/package.json +138 -0
  45. package/patterns/api/[collectionName]/Controllers.md +62 -0
  46. package/patterns/api/[collectionName]/Logic.md +154 -0
  47. package/patterns/api/[collectionName]/Permissions.md +81 -0
  48. package/patterns/api/[collectionName]/Router.md +83 -0
  49. package/patterns/api/[collectionName]/Schemas.md +197 -0
  50. package/patterns/configs/Patterns.md +7 -0
  51. package/patterns/enums/Patterns.md +24 -0
  52. package/patterns/errorHandling/Patterns.md +185 -0
  53. package/patterns/testing/Patterns.md +232 -0
  54. package/src/Server.js +238 -0
  55. package/src/api/dashboard/Controllers.js +9 -0
  56. package/src/api/dashboard/Logic.js +76 -0
  57. package/src/api/dashboard/Router.js +11 -0
  58. package/src/api/dashboard/Schemas.js +47 -0
  59. package/src/api/data/Controllers.js +26 -0
  60. package/src/api/data/Logic.js +188 -0
  61. package/src/api/data/Router.js +16 -0
  62. package/src/api/docker/Controllers.js +33 -0
  63. package/src/api/docker/Logic.js +268 -0
  64. package/src/api/docker/Router.js +15 -0
  65. package/src/api/docker/Schemas.js +80 -0
  66. package/src/api/docs/Controllers.js +15 -0
  67. package/src/api/docs/Logic.js +85 -0
  68. package/src/api/docs/Router.js +12 -0
  69. package/src/api/export/Controllers.js +30 -0
  70. package/src/api/export/Logic.js +143 -0
  71. package/src/api/export/Router.js +18 -0
  72. package/src/api/export/Schemas.js +104 -0
  73. package/src/api/firestore/Controllers.js +152 -0
  74. package/src/api/firestore/Logic.js +474 -0
  75. package/src/api/firestore/Router.js +23 -0
  76. package/src/api/functions/Controllers.js +261 -0
  77. package/src/api/functions/Logic.js +710 -0
  78. package/src/api/functions/Router.js +50 -0
  79. package/src/api/functions/Schemas.js +193 -0
  80. package/src/api/gateway/Controllers.js +72 -0
  81. package/src/api/gateway/Logic.js +74 -0
  82. package/src/api/gateway/Router.js +10 -0
  83. package/src/api/gateway/Schemas.js +19 -0
  84. package/src/api/health/Controllers.js +14 -0
  85. package/src/api/health/Logic.js +24 -0
  86. package/src/api/health/Router.js +12 -0
  87. package/src/api/httpTraffic/Controllers.js +29 -0
  88. package/src/api/httpTraffic/Logic.js +33 -0
  89. package/src/api/httpTraffic/Router.js +9 -0
  90. package/src/api/httpTraffic/Schemas.js +23 -0
  91. package/src/api/logging/Controllers.js +80 -0
  92. package/src/api/logging/Logic.js +461 -0
  93. package/src/api/logging/Router.js +24 -0
  94. package/src/api/logging/Schemas.js +43 -0
  95. package/src/api/mqtt/Controllers.js +17 -0
  96. package/src/api/mqtt/Logic.js +66 -0
  97. package/src/api/mqtt/Router.js +12 -0
  98. package/src/api/postgres/Controllers.js +97 -0
  99. package/src/api/postgres/Logic.js +221 -0
  100. package/src/api/postgres/Router.js +21 -0
  101. package/src/api/pubsub/Controllers.js +236 -0
  102. package/src/api/pubsub/Logic.js +732 -0
  103. package/src/api/pubsub/Router.js +41 -0
  104. package/src/api/pubsub/Schemas.js +355 -0
  105. package/src/api/redis/Controllers.js +63 -0
  106. package/src/api/redis/Logic.js +239 -0
  107. package/src/api/redis/Router.js +21 -0
  108. package/src/api/scheduler/Controllers.js +27 -0
  109. package/src/api/scheduler/Logic.js +49 -0
  110. package/src/api/scheduler/Router.js +16 -0
  111. package/src/api/services/Controllers.js +26 -0
  112. package/src/api/services/Logic.js +205 -0
  113. package/src/api/services/Router.js +14 -0
  114. package/src/api/services/Schemas.js +66 -0
  115. package/src/api/snapshots/Controllers.js +37 -0
  116. package/src/api/snapshots/Logic.js +797 -0
  117. package/src/api/snapshots/Router.js +15 -0
  118. package/src/api/snapshots/Schemas.js +23 -0
  119. package/src/api/webhooks/Controllers.js +49 -0
  120. package/src/api/webhooks/Logic.js +137 -0
  121. package/src/api/webhooks/Router.js +12 -0
  122. package/src/api/webhooks/Schemas.js +31 -0
  123. package/src/configs/Application.js +147 -0
  124. package/src/configs/Default.js +13 -0
  125. package/src/consumers/BlackboxLogsConsumer.js +235 -0
  126. package/src/consumers/DockerLogsConsumer.js +687 -0
  127. package/src/db/Tables.js +66 -0
  128. package/src/db/schemas/firestore.js +18 -0
  129. package/src/db/schemas/functions.js +65 -0
  130. package/src/db/schemas/httpTraffic.js +43 -0
  131. package/src/db/schemas/logging.js +74 -0
  132. package/src/db/schemas/migrations.js +64 -0
  133. package/src/db/schemas/mqtt.js +56 -0
  134. package/src/db/schemas/pubsub.js +90 -0
  135. package/src/db/schemas/pubsubRegistry.js +22 -0
  136. package/src/db/schemas/webhooks.js +28 -0
  137. package/src/emulation/awsiot/Controllers.js +91 -0
  138. package/src/emulation/awsiot/Logic.js +70 -0
  139. package/src/emulation/awsiot/Router.js +19 -0
  140. package/src/emulation/awsiot/Server.js +100 -0
  141. package/src/emulation/firestore/Server.js +136 -0
  142. package/src/emulation/logging/Controllers.js +212 -0
  143. package/src/emulation/logging/Logic.js +416 -0
  144. package/src/emulation/logging/Router.js +36 -0
  145. package/src/emulation/logging/Schemas.js +82 -0
  146. package/src/emulation/logging/Server.js +108 -0
  147. package/src/emulation/pubsub/Controllers.js +279 -0
  148. package/src/emulation/pubsub/DefaultTopics.js +162 -0
  149. package/src/emulation/pubsub/Logic.js +427 -0
  150. package/src/emulation/pubsub/README.md +309 -0
  151. package/src/emulation/pubsub/Router.js +33 -0
  152. package/src/emulation/pubsub/Server.js +104 -0
  153. package/src/emulation/pubsub/ShadowPoller.js +276 -0
  154. package/src/emulation/pubsub/ShadowSubscriptionManager.js +199 -0
  155. package/src/enums/ContainerNames.js +106 -0
  156. package/src/enums/ErrorReason.js +28 -0
  157. package/src/enums/FunctionStatuses.js +15 -0
  158. package/src/enums/FunctionTriggerTypes.js +15 -0
  159. package/src/enums/GatewayState.js +7 -0
  160. package/src/enums/ServiceNames.js +68 -0
  161. package/src/jobs/DatabaseMaintenance.js +184 -0
  162. package/src/jobs/MessageHistoryCleanup.js +152 -0
  163. package/src/mcp/ApiClient.js +25 -0
  164. package/src/mcp/Server.js +52 -0
  165. package/src/mcp/prompts/debugging.js +104 -0
  166. package/src/mcp/resources/platform.js +118 -0
  167. package/src/mcp/tools/data.js +84 -0
  168. package/src/mcp/tools/docker.js +166 -0
  169. package/src/mcp/tools/firestore.js +162 -0
  170. package/src/mcp/tools/functions.js +380 -0
  171. package/src/mcp/tools/httpTraffic.js +69 -0
  172. package/src/mcp/tools/logging.js +174 -0
  173. package/src/mcp/tools/mqtt.js +37 -0
  174. package/src/mcp/tools/postgres.js +130 -0
  175. package/src/mcp/tools/pubsub.js +316 -0
  176. package/src/mcp/tools/redis.js +146 -0
  177. package/src/mcp/tools/services.js +169 -0
  178. package/src/mcp/tools/snapshots.js +88 -0
  179. package/src/mcp/tools/webhooks.js +115 -0
  180. package/src/middleware/DevProxy.js +67 -0
  181. package/src/middleware/ErrorCatcher.js +35 -0
  182. package/src/middleware/HttpProxy.js +215 -0
  183. package/src/middleware/Reply.js +24 -0
  184. package/src/middleware/TraceId.js +9 -0
  185. package/src/middleware/WebhookProxy.js +234 -0
  186. package/src/protocols/mqtt/Broker.js +92 -0
  187. package/src/protocols/mqtt/Handlers.js +175 -0
  188. package/src/protocols/mqtt/PubSubBridge.js +162 -0
  189. package/src/protocols/mqtt/Server.js +116 -0
  190. package/src/runtime/FunctionRunner.js +179 -0
  191. package/src/services/AppGatewayService.js +582 -0
  192. package/src/singletons/FirestoreBroadcaster.js +367 -0
  193. package/src/singletons/FunctionTriggerDispatcher.js +456 -0
  194. package/src/singletons/FunctionsService.js +418 -0
  195. package/src/singletons/HttpProxy.js +224 -0
  196. package/src/singletons/LogBroadcaster.js +159 -0
  197. package/src/singletons/Logger.js +49 -0
  198. package/src/singletons/MemoryJsonStore.js +175 -0
  199. package/src/singletons/MessageBroadcaster.js +190 -0
  200. package/src/singletons/PostgresBroadcaster.js +367 -0
  201. package/src/singletons/PostgresClient.js +180 -0
  202. package/src/singletons/RedisClient.js +184 -0
  203. package/src/singletons/SqliteStore.js +480 -0
  204. package/src/singletons/TickService.js +151 -0
  205. package/src/singletons/WebhookProxy.js +223 -0
@@ -0,0 +1,732 @@
1
+ import { SqliteStore } from '../../singletons/SqliteStore.js'
2
+ import { Application } from '../../configs/Application.js'
3
+ import { PUBSUB_MESSAGE_HISTORY, PUBSUB_TOPIC_REGISTRY } from '../../db/Tables.js'
4
+ import { shadowSubscriptionManager } from '../../emulation/pubsub/ShadowSubscriptionManager.js'
5
+ import { JSONPath } from 'jsonpath-plus'
6
+
7
+ const { pubsub } = Application
8
+ const PUBSUB_API = `http://${pubsub.emulatorHost}:${pubsub.emulatorPort}`
9
+ const PROJECT_ID = pubsub.projectId
10
+
11
+ const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms))
12
+
13
+ /**
14
+ * Decodes base64-encoded PubSub message data to a parsed object or string.
15
+ * The PubSub emulator stores data as base64-encoded JSON strings.
16
+ */
17
+ const decodePubSubData = (data) => {
18
+ if (!data) return null
19
+ let decoded = data
20
+ try {
21
+ decoded = Buffer.from(data, 'base64').toString('utf-8')
22
+ } catch {
23
+ // not base64, use as-is
24
+ }
25
+ try {
26
+ return JSON.parse(decoded)
27
+ } catch {
28
+ return decoded
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Decodes the data field for all messages in an array.
34
+ */
35
+ const decodeMessages = (messages) =>
36
+ messages.map(msg => ({ ...msg, data: decodePubSubData(msg.data) }))
37
+
38
+ const deepEqual = (a, b) => {
39
+ if (a === b) return true
40
+ if (a == null || b == null) return false
41
+ if (typeof a !== 'object' || typeof b !== 'object') return false
42
+ const keysA = Object.keys(a)
43
+ const keysB = Object.keys(b)
44
+ if (keysA.length !== keysB.length) return false
45
+ for (const key of keysA) {
46
+ if (!keysB.includes(key)) return false
47
+ if (!deepEqual(a[key], b[key])) return false
48
+ }
49
+ return true
50
+ }
51
+
52
+ export const Logic = {
53
+ async listMessages (params) {
54
+ return Logic.getHistory(params)
55
+ },
56
+
57
+ async listTopics (params) {
58
+ const { traceId } = params
59
+ try {
60
+ const response = await fetch(`${PUBSUB_API}/v1/projects/${PROJECT_ID}/topics`)
61
+ const result = await response.json()
62
+ const topics = (result.topics || []).filter(t => !t.name.includes('-devtools-shadow'))
63
+ return {
64
+ topics,
65
+ total: topics.length,
66
+ traceId
67
+ }
68
+ } catch (error) {
69
+ return {
70
+ status: 'error',
71
+ message: error.message,
72
+ traceId
73
+ }
74
+ }
75
+ },
76
+
77
+ async listSubscriptions (params) {
78
+ const { traceId } = params
79
+ try {
80
+ const response = await fetch(`${PUBSUB_API}/v1/projects/${PROJECT_ID}/subscriptions`)
81
+ const result = await response.json()
82
+ const subscriptions = (result.subscriptions || []).filter(s => !s.name.includes('-devtools-shadow'))
83
+ return {
84
+ subscriptions,
85
+ total: subscriptions.length,
86
+ traceId
87
+ }
88
+ } catch (error) {
89
+ return {
90
+ status: 'error',
91
+ message: error.message,
92
+ traceId
93
+ }
94
+ }
95
+ },
96
+
97
+ async createTopic (params) {
98
+ const { topicName, traceId } = params
99
+ try {
100
+ const response = await fetch(`${PUBSUB_API}/v1/projects/${PROJECT_ID}/topics/${topicName}`, {
101
+ method: 'PUT',
102
+ headers: { 'Content-Type': 'application/json' },
103
+ body: JSON.stringify({})
104
+ })
105
+ const topic = await response.json()
106
+ return {
107
+ topic,
108
+ traceId
109
+ }
110
+ } catch (error) {
111
+ throw error
112
+ }
113
+ },
114
+
115
+ async deleteTopic (params) {
116
+ const { topicName, traceId } = params
117
+ try {
118
+ await fetch(`${PUBSUB_API}/v1/projects/${PROJECT_ID}/topics/${topicName}`, {
119
+ method: 'DELETE'
120
+ })
121
+ return {
122
+ message: 'Topic deleted',
123
+ traceId
124
+ }
125
+ } catch (error) {
126
+ throw error
127
+ }
128
+ },
129
+
130
+ async createSubscription (params) {
131
+ const { topicName, subscriptionName, traceId } = params
132
+ try {
133
+ const response = await fetch(`${PUBSUB_API}/v1/projects/${PROJECT_ID}/subscriptions/${subscriptionName}`, {
134
+ method: 'PUT',
135
+ headers: { 'Content-Type': 'application/json' },
136
+ body: JSON.stringify({ topic: `projects/${PROJECT_ID}/topics/${topicName}` })
137
+ })
138
+ const subscription = await response.json()
139
+ return {
140
+ subscription,
141
+ traceId
142
+ }
143
+ } catch (error) {
144
+ throw error
145
+ }
146
+ },
147
+
148
+ async deleteSubscription (params) {
149
+ const { subscriptionName, traceId } = params
150
+ try {
151
+ await fetch(`${PUBSUB_API}/v1/projects/${PROJECT_ID}/subscriptions/${subscriptionName}`, {
152
+ method: 'DELETE'
153
+ })
154
+ return {
155
+ message: 'Subscription deleted',
156
+ traceId
157
+ }
158
+ } catch (error) {
159
+ throw error
160
+ }
161
+ },
162
+
163
+ async publishMessage (params) {
164
+ const { topicName, message, attributes, traceId } = params
165
+ try {
166
+ let messageString
167
+ if (typeof message === 'string') {
168
+ messageString = message
169
+ } else if (Buffer.isBuffer(message)) {
170
+ messageString = message.toString()
171
+ } else if (typeof message === 'object') {
172
+ messageString = JSON.stringify(message)
173
+ } else {
174
+ messageString = String(message)
175
+ }
176
+ const messageData = Buffer.from(messageString).toString('base64')
177
+ // Try to publish
178
+ let response = await fetch(`${PUBSUB_API}/v1/projects/${PROJECT_ID}/topics/${topicName}:publish`, {
179
+ method: 'POST',
180
+ headers: { 'Content-Type': 'application/json' },
181
+ body: JSON.stringify({
182
+ messages: [{ data: messageData, attributes: attributes || {} }]
183
+ })
184
+ })
185
+ let result = await response.json()
186
+ // Auto-create topic if not found
187
+ if (response.status === 404 || result.error?.code === 404) {
188
+ const fullTopicName = `projects/${PROJECT_ID}/topics/${topicName}`
189
+ const shadowSubName = `${topicName}-devtools-shadow`
190
+ const fullShadowSubName = `projects/${PROJECT_ID}/subscriptions/${shadowSubName}`
191
+ // Create topic
192
+ await fetch(`${PUBSUB_API}/v1/projects/${PROJECT_ID}/topics/${topicName}`, {
193
+ method: 'PUT',
194
+ headers: { 'Content-Type': 'application/json' },
195
+ body: JSON.stringify({})
196
+ })
197
+ // Create shadow subscription for message capture
198
+ await fetch(`${PUBSUB_API}/v1/projects/${PROJECT_ID}/subscriptions/${shadowSubName}`, {
199
+ method: 'PUT',
200
+ headers: { 'Content-Type': 'application/json' },
201
+ body: JSON.stringify({ topic: fullTopicName })
202
+ })
203
+ // Register with shadow subscription manager so poller captures messages
204
+ shadowSubscriptionManager.shadowSubscriptions.add(fullShadowSubName)
205
+ shadowSubscriptionManager.shadowToTopic.set(fullShadowSubName, fullTopicName)
206
+ // Retry publish
207
+ response = await fetch(`${PUBSUB_API}/v1/projects/${PROJECT_ID}/topics/${topicName}:publish`, {
208
+ method: 'POST',
209
+ headers: { 'Content-Type': 'application/json' },
210
+ body: JSON.stringify({
211
+ messages: [{ data: messageData, attributes: attributes || {} }]
212
+ })
213
+ })
214
+ result = await response.json()
215
+ }
216
+ if (!response.ok || result.error) {
217
+ throw new Error(result.error?.message || `Failed to publish: ${response.status}`)
218
+ }
219
+ return {
220
+ messageIds: result.messageIds,
221
+ traceId
222
+ }
223
+ } catch (error) {
224
+ throw error
225
+ }
226
+ },
227
+
228
+ async pullMessages (params) {
229
+ const { subscriptionName, maxMessages = 10, traceId } = params
230
+ try {
231
+ const response = await fetch(`${PUBSUB_API}/v1/projects/${PROJECT_ID}/subscriptions/${subscriptionName}:pull`, {
232
+ method: 'POST',
233
+ headers: { 'Content-Type': 'application/json' },
234
+ body: JSON.stringify({ maxMessages, returnImmediately: true })
235
+ })
236
+ const result = await response.json()
237
+ return {
238
+ messages: result.receivedMessages || [],
239
+ traceId
240
+ }
241
+ } catch (error) {
242
+ throw error
243
+ }
244
+ },
245
+
246
+ /**
247
+ * Get message history with filters and view tracking
248
+ */
249
+ async getHistory (params) {
250
+ const { filter = {}, page = {}, traceId } = params
251
+ try {
252
+ const limit = page.limit || 50
253
+ const offset = page.offset || 0
254
+
255
+ // Build SQL query with all filters in WHERE clause for correct pagination
256
+ let sql = `SELECT * FROM ${PUBSUB_MESSAGE_HISTORY}`
257
+ let countSql = `SELECT COUNT(*) as count FROM ${PUBSUB_MESSAGE_HISTORY}`
258
+ const conditions = []
259
+ const queryParams = []
260
+
261
+ if (filter.topic) {
262
+ conditions.push('topic = ?')
263
+ queryParams.push(filter.topic)
264
+ }
265
+ if (filter.sender) {
266
+ conditions.push('sender = ?')
267
+ queryParams.push(filter.sender)
268
+ }
269
+ if (filter.before) {
270
+ conditions.push('publish_time < ?')
271
+ queryParams.push(filter.before)
272
+ }
273
+ if (filter.timeRange) {
274
+ if (filter.timeRange.start) {
275
+ conditions.push('publish_time >= ?')
276
+ queryParams.push(filter.timeRange.start)
277
+ }
278
+ if (filter.timeRange.end) {
279
+ conditions.push('publish_time <= ?')
280
+ queryParams.push(filter.timeRange.end)
281
+ }
282
+ }
283
+
284
+ if (conditions.length > 0) {
285
+ const whereClause = ` WHERE ${conditions.join(' AND ')}`
286
+ sql += whereClause
287
+ countSql += whereClause
288
+ }
289
+
290
+ sql += ' ORDER BY publish_time DESC'
291
+ sql += ` LIMIT ${limit} OFFSET ${offset}`
292
+
293
+ const totalResult = SqliteStore.db.prepare(countSql).get(...queryParams)
294
+ const total = totalResult.count
295
+
296
+ let data = SqliteStore.db.prepare(sql).all(...queryParams)
297
+ // Convert snake_case → camelCase and parse JSON fields (same as SqliteStore.list)
298
+ data = data.map(row => {
299
+ const camel = {}
300
+ for (const key in row) {
301
+ const camelKey = key.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase())
302
+ camel[camelKey] = row[key]
303
+ }
304
+ // Parse JSON fields
305
+ if (camel.attributes && typeof camel.attributes === 'string') {
306
+ try { camel.attributes = JSON.parse(camel.attributes) } catch {}
307
+ }
308
+ return camel
309
+ })
310
+
311
+ const result = { data, total }
312
+
313
+ // Increment viewCount for all returned messages
314
+ for (const msg of result.data) {
315
+ SqliteStore.update(PUBSUB_MESSAGE_HISTORY, msg.id, {
316
+ viewCount: (msg.viewCount || 0) + 1,
317
+ lastViewedAt: Date.now()
318
+ })
319
+ }
320
+
321
+ return {
322
+ messages: decodeMessages(result.data),
323
+ total: result.total,
324
+ traceId
325
+ }
326
+ } catch (error) {
327
+ return {
328
+ status: 'error',
329
+ message: error.message,
330
+ traceId
331
+ }
332
+ }
333
+ },
334
+
335
+ /**
336
+ * Auto-suggest topics based on query
337
+ */
338
+ async suggestTopics (params) {
339
+ const { query = '', limit = 10, traceId } = params
340
+ try {
341
+ // Get all distinct topics from history with message counts
342
+ const sql = `
343
+ SELECT
344
+ topic as name,
345
+ COUNT(*) as messageCount,
346
+ MAX(publish_time) as lastPublishTime
347
+ FROM ${PUBSUB_MESSAGE_HISTORY}
348
+ WHERE topic LIKE ?
349
+ GROUP BY topic
350
+ ORDER BY messageCount DESC
351
+ LIMIT ?
352
+ `
353
+
354
+ const topics = SqliteStore.db.prepare(sql).all(`%${query}%`, limit)
355
+
356
+ return {
357
+ topics,
358
+ traceId
359
+ }
360
+ } catch (error) {
361
+ return {
362
+ status: 'error',
363
+ message: error.message,
364
+ traceId
365
+ }
366
+ }
367
+ },
368
+
369
+ /**
370
+ * Search messages by content
371
+ */
372
+ async searchMessages (params) {
373
+ const { query, searchIn = ['data'], filter = {}, page = {}, traceId } = params
374
+ try {
375
+ const options = {
376
+ limit: page.limit || 50,
377
+ offset: page.offset || 0
378
+ }
379
+
380
+ // Build search predicate
381
+ options.filter = (msg) => {
382
+ // Apply additional filters first
383
+ if (filter.topic && msg.topic !== filter.topic) {
384
+ return false
385
+ }
386
+ if (filter.sender && msg.sender !== filter.sender) {
387
+ return false
388
+ }
389
+
390
+ // Search in specified fields
391
+ const queryLower = query.toLowerCase()
392
+
393
+ if (searchIn.includes('data')) {
394
+ const dataStr = typeof msg.data === 'string' ? msg.data : JSON.stringify(msg.data || {})
395
+ if (dataStr.toLowerCase().includes(queryLower)) {
396
+ return true
397
+ }
398
+ }
399
+
400
+ if (searchIn.includes('attributes')) {
401
+ const attrStr = JSON.stringify(msg.attributes || {})
402
+ if (attrStr.toLowerCase().includes(queryLower)) {
403
+ return true
404
+ }
405
+ }
406
+
407
+ return false
408
+ }
409
+
410
+ const result = SqliteStore.list(PUBSUB_MESSAGE_HISTORY, options)
411
+
412
+ return {
413
+ messages: decodeMessages(result.data),
414
+ total: result.total,
415
+ traceId
416
+ }
417
+ } catch (error) {
418
+ return {
419
+ status: 'error',
420
+ message: error.message,
421
+ traceId
422
+ }
423
+ }
424
+ },
425
+
426
+ /**
427
+ * Clear message history (LRU cleanup)
428
+ */
429
+ async clearHistory (params) {
430
+ const { keepCount, topic, traceId } = params
431
+ try {
432
+ let deletedCount = 0
433
+
434
+ if (keepCount) {
435
+ // Delete least viewed messages beyond keepCount
436
+ const minKeep = 100 // Always keep minimum 100 messages
437
+
438
+ // Get current count
439
+ let countSql = `SELECT COUNT(*) as count FROM ${PUBSUB_MESSAGE_HISTORY}`
440
+ if (topic) {
441
+ countSql += ' WHERE topic = ?'
442
+ }
443
+ const stmt = topic ? SqliteStore.db.prepare(countSql).get(topic) : SqliteStore.db.prepare(countSql).get()
444
+ const currentCount = stmt.count
445
+
446
+ if (currentCount > keepCount && currentCount - keepCount >= minKeep) {
447
+ const deleteCount = currentCount - keepCount
448
+
449
+ // Delete least viewed messages (LRU)
450
+ let deleteSql = `
451
+ DELETE FROM ${PUBSUB_MESSAGE_HISTORY}
452
+ WHERE id IN (
453
+ SELECT id FROM ${PUBSUB_MESSAGE_HISTORY}
454
+ `
455
+ if (topic) {
456
+ deleteSql += ' WHERE topic = ?'
457
+ }
458
+ deleteSql += `
459
+ ORDER BY view_count ASC, created_at ASC
460
+ LIMIT ?
461
+ )
462
+ `
463
+
464
+ const result = topic
465
+ ? SqliteStore.db.prepare(deleteSql).run(topic, deleteCount)
466
+ : SqliteStore.db.prepare(deleteSql).run(deleteCount)
467
+ deletedCount = result.changes
468
+ }
469
+ } else if (topic) {
470
+ // Delete all messages for specific topic
471
+ const messages = SqliteStore.find(PUBSUB_MESSAGE_HISTORY, { topic })
472
+ for (const msg of messages) {
473
+ SqliteStore.delete(PUBSUB_MESSAGE_HISTORY, msg.id)
474
+ deletedCount++
475
+ }
476
+ } else {
477
+ // Clear ALL messages when no filters provided
478
+ const deleteSql = `DELETE FROM ${PUBSUB_MESSAGE_HISTORY}`
479
+ const result = SqliteStore.db.prepare(deleteSql).run()
480
+ deletedCount = result.changes
481
+ }
482
+
483
+ return {
484
+ deletedCount,
485
+ traceId
486
+ }
487
+ } catch (error) {
488
+ return {
489
+ status: 'error',
490
+ message: error.message,
491
+ traceId
492
+ }
493
+ }
494
+ },
495
+
496
+ /**
497
+ * Get message history statistics
498
+ */
499
+ async getHistoryStats (params) {
500
+ const { traceId } = params
501
+ try {
502
+ // Total messages
503
+ const totalSql = `SELECT COUNT(*) as count FROM ${PUBSUB_MESSAGE_HISTORY}`
504
+ const totalResult = SqliteStore.db.prepare(totalSql).get()
505
+ const totalMessages = totalResult.count
506
+
507
+ // Total unique topics
508
+ const topicsSql = `SELECT COUNT(DISTINCT topic) as count FROM ${PUBSUB_MESSAGE_HISTORY}`
509
+ const topicsResult = SqliteStore.db.prepare(topicsSql).get()
510
+ const totalTopics = topicsResult.count
511
+
512
+ // Oldest and newest message times
513
+ const timesSql = `
514
+ SELECT
515
+ MIN(publish_time) as oldest,
516
+ MAX(publish_time) as newest
517
+ FROM ${PUBSUB_MESSAGE_HISTORY}
518
+ `
519
+ const timesResult = SqliteStore.db.prepare(timesSql).get()
520
+
521
+ // Top 10 topics by message count
522
+ const topTopicsSql = `
523
+ SELECT
524
+ topic,
525
+ COUNT(*) as count
526
+ FROM ${PUBSUB_MESSAGE_HISTORY}
527
+ GROUP BY topic
528
+ ORDER BY count DESC
529
+ LIMIT 10
530
+ `
531
+ const topTopics = SqliteStore.db.prepare(topTopicsSql).all()
532
+
533
+ return {
534
+ totalMessages,
535
+ totalTopics,
536
+ oldestMessage: timesResult.oldest,
537
+ newestMessage: timesResult.newest,
538
+ topTopics,
539
+ traceId
540
+ }
541
+ } catch (error) {
542
+ return {
543
+ status: 'error',
544
+ message: error.message,
545
+ traceId
546
+ }
547
+ }
548
+ },
549
+
550
+ /**
551
+ * Wait for a message matching filter criteria
552
+ */
553
+ async waitForMessage (params) {
554
+ const { filter, timeout = 5000, traceId } = params
555
+ const startTime = Date.now()
556
+ const pollInterval = 100
557
+ try {
558
+ while (Date.now() - startTime < timeout) {
559
+ const historyResult = await Logic.getHistory({
560
+ filter: {
561
+ topic: filter.topic,
562
+ sender: filter.sender
563
+ },
564
+ page: { limit: 50 },
565
+ traceId
566
+ })
567
+ if (historyResult.messages && historyResult.messages.length > 0) {
568
+ for (const message of historyResult.messages) {
569
+ if (!filter.predicate) {
570
+ return {
571
+ message,
572
+ foundAt: Date.now() - startTime,
573
+ traceId
574
+ }
575
+ }
576
+ const { jsonPath, operator, value } = filter.predicate
577
+ const messageData = typeof message.data === 'object' ? message.data : decodePubSubData(message.data)
578
+ const results = JSONPath({ path: jsonPath, json: messageData })
579
+ if (results && results.length > 0) {
580
+ const actualValue = results[0]
581
+ let matches = false
582
+ switch (operator) {
583
+ case 'equals':
584
+ matches = deepEqual(actualValue, value)
585
+ break
586
+ case 'notEquals':
587
+ matches = !deepEqual(actualValue, value)
588
+ break
589
+ case 'contains':
590
+ matches = String(actualValue).includes(String(value))
591
+ break
592
+ case 'greaterThan':
593
+ matches = actualValue > value
594
+ break
595
+ case 'lessThan':
596
+ matches = actualValue < value
597
+ break
598
+ }
599
+ if (matches) {
600
+ return {
601
+ message,
602
+ foundAt: Date.now() - startTime,
603
+ traceId
604
+ }
605
+ }
606
+ }
607
+ }
608
+ }
609
+ await sleep(pollInterval)
610
+ }
611
+ return {
612
+ status: 'error',
613
+ message: 'Timeout waiting for message',
614
+ traceId
615
+ }
616
+ } catch (error) {
617
+ return {
618
+ status: 'error',
619
+ message: error.message,
620
+ traceId
621
+ }
622
+ }
623
+ },
624
+
625
+ /**
626
+ * Assert that a message was published with specific criteria
627
+ */
628
+ async assertMessagePublished (params) {
629
+ const { filter, traceId } = params
630
+ try {
631
+ const historyFilter = {
632
+ topic: filter.topic
633
+ }
634
+ if (filter.since) {
635
+ historyFilter.timeRange = {
636
+ start: filter.since,
637
+ end: new Date().toISOString()
638
+ }
639
+ }
640
+ const historyResult = await Logic.getHistory({
641
+ filter: historyFilter,
642
+ page: { limit: 1000 },
643
+ traceId
644
+ })
645
+ let matches = []
646
+ if (historyResult.messages && historyResult.messages.length > 0) {
647
+ matches = historyResult.messages.filter(msg => {
648
+ if (filter.dataContains) {
649
+ const dataStr = typeof msg.data === 'string' ? msg.data : JSON.stringify(msg.data)
650
+ if (!dataStr.includes(filter.dataContains)) {
651
+ return false
652
+ }
653
+ }
654
+ if (filter.attributesMatch) {
655
+ for (const [key, value] of Object.entries(filter.attributesMatch)) {
656
+ if (msg.attributes?.[key] !== value) {
657
+ return false
658
+ }
659
+ }
660
+ }
661
+ return true
662
+ })
663
+ }
664
+ return {
665
+ found: matches.length > 0,
666
+ count: matches.length,
667
+ firstMatch: matches[0] || null,
668
+ traceId
669
+ }
670
+ } catch (error) {
671
+ return {
672
+ status: 'error',
673
+ message: error.message,
674
+ traceId
675
+ }
676
+ }
677
+ },
678
+
679
+ /**
680
+ * Register topics and subscriptions in the persistent registry
681
+ */
682
+ async registerTopics (params) {
683
+ const { projectName, topics, traceId } = params
684
+ const normalized = topics.map(t => typeof t === 'string' ? { name: t } : t)
685
+ const registered = []
686
+ for (const entry of normalized) {
687
+ SqliteStore.db.prepare(`
688
+ INSERT OR REPLACE INTO ${PUBSUB_TOPIC_REGISTRY} (topic_name, subscription_name, project_name, registered_at)
689
+ VALUES (?, NULL, ?, unixepoch())
690
+ `).run(entry.name, projectName)
691
+ registered.push({ topicName: entry.name, subscriptionName: null })
692
+ if (entry.subscription) {
693
+ SqliteStore.db.prepare(`
694
+ INSERT OR REPLACE INTO ${PUBSUB_TOPIC_REGISTRY} (topic_name, subscription_name, project_name, registered_at)
695
+ VALUES (?, ?, ?, unixepoch())
696
+ `).run(entry.name, entry.subscription, projectName)
697
+ registered.push({ topicName: entry.name, subscriptionName: entry.subscription })
698
+ }
699
+ }
700
+ return { registered, count: registered.length, traceId }
701
+ },
702
+
703
+ /**
704
+ * List registered topics from the persistent registry
705
+ */
706
+ async listRegistry (params) {
707
+ const { projectName, traceId } = params
708
+ let entries
709
+ if (projectName) {
710
+ entries = SqliteStore.find(PUBSUB_TOPIC_REGISTRY, { projectName })
711
+ } else {
712
+ entries = SqliteStore.list(PUBSUB_TOPIC_REGISTRY, { orderBy: 'topic_name ASC, project_name ASC' }).data
713
+ }
714
+ return { entries, total: entries.length, traceId }
715
+ },
716
+
717
+ /**
718
+ * Unregister topics for a project from the persistent registry
719
+ */
720
+ async unregisterTopics (params) {
721
+ const { projectName, topicNames, traceId } = params
722
+ let removed = 0
723
+ for (const topicName of topicNames) {
724
+ const result = SqliteStore.db.prepare(`
725
+ DELETE FROM ${PUBSUB_TOPIC_REGISTRY}
726
+ WHERE topic_name = ? AND project_name = ?
727
+ `).run(topicName, projectName)
728
+ removed += result.changes
729
+ }
730
+ return { removed, traceId }
731
+ }
732
+ }