@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.
- package/README.md +478 -0
- package/bin/goki-dev.js +452 -0
- package/bin/mcp-server.js +16 -0
- package/bin/secrets-cli.js +302 -0
- package/cli/ComposeOverrideGenerator.js +226 -0
- package/cli/ComposeParser.js +73 -0
- package/cli/ConfigGenerator.js +304 -0
- package/cli/ConfigManager.js +46 -0
- package/cli/DatabaseManager.js +94 -0
- package/cli/DevToolsChecker.js +21 -0
- package/cli/DevToolsDir.js +66 -0
- package/cli/DevToolsManager.js +451 -0
- package/cli/DockerManager.js +138 -0
- package/cli/FunctionManager.js +95 -0
- package/cli/HttpProxyRewriter.js +91 -0
- package/cli/Logger.js +10 -0
- package/cli/McpConfigManager.js +123 -0
- package/cli/NgrokManager.js +431 -0
- package/cli/ProjectCLI.js +2322 -0
- package/cli/PubSubManager.js +129 -0
- package/cli/SnapshotManager.js +88 -0
- package/cli/UiFormatter.js +292 -0
- package/cli/WebhookUrlRewriter.js +32 -0
- package/cli/secrets/BiometricAuth.js +125 -0
- package/cli/secrets/SecretInjector.js +47 -0
- package/cli/secrets/SecretsConfig.js +141 -0
- package/cli/secrets/SecretsDoctor.js +384 -0
- package/cli/secrets/SecretsManager.js +255 -0
- package/client/dist/client.d.ts +332 -0
- package/client/dist/client.js +507 -0
- package/client/dist/helpers.d.ts +62 -0
- package/client/dist/helpers.js +122 -0
- package/client/dist/index.d.ts +59 -0
- package/client/dist/index.js +78 -0
- package/client/dist/package.json +1 -0
- package/client/dist/types.d.ts +280 -0
- package/client/dist/types.js +7 -0
- package/config.development +46 -0
- package/config.test +18 -0
- package/guidelines/CodingStyleGuideline.md +148 -0
- package/guidelines/CommentingGuideline.md +10 -0
- package/guidelines/HttpApiImplementationGuideline.md +137 -0
- package/guidelines/NamingGuideline.md +182 -0
- package/package.json +138 -0
- package/patterns/api/[collectionName]/Controllers.md +62 -0
- package/patterns/api/[collectionName]/Logic.md +154 -0
- package/patterns/api/[collectionName]/Permissions.md +81 -0
- package/patterns/api/[collectionName]/Router.md +83 -0
- package/patterns/api/[collectionName]/Schemas.md +197 -0
- package/patterns/configs/Patterns.md +7 -0
- package/patterns/enums/Patterns.md +24 -0
- package/patterns/errorHandling/Patterns.md +185 -0
- package/patterns/testing/Patterns.md +232 -0
- package/src/Server.js +238 -0
- package/src/api/dashboard/Controllers.js +9 -0
- package/src/api/dashboard/Logic.js +76 -0
- package/src/api/dashboard/Router.js +11 -0
- package/src/api/dashboard/Schemas.js +47 -0
- package/src/api/data/Controllers.js +26 -0
- package/src/api/data/Logic.js +188 -0
- package/src/api/data/Router.js +16 -0
- package/src/api/docker/Controllers.js +33 -0
- package/src/api/docker/Logic.js +268 -0
- package/src/api/docker/Router.js +15 -0
- package/src/api/docker/Schemas.js +80 -0
- package/src/api/docs/Controllers.js +15 -0
- package/src/api/docs/Logic.js +85 -0
- package/src/api/docs/Router.js +12 -0
- package/src/api/export/Controllers.js +30 -0
- package/src/api/export/Logic.js +143 -0
- package/src/api/export/Router.js +18 -0
- package/src/api/export/Schemas.js +104 -0
- package/src/api/firestore/Controllers.js +152 -0
- package/src/api/firestore/Logic.js +474 -0
- package/src/api/firestore/Router.js +23 -0
- package/src/api/functions/Controllers.js +261 -0
- package/src/api/functions/Logic.js +710 -0
- package/src/api/functions/Router.js +50 -0
- package/src/api/functions/Schemas.js +193 -0
- package/src/api/gateway/Controllers.js +72 -0
- package/src/api/gateway/Logic.js +74 -0
- package/src/api/gateway/Router.js +10 -0
- package/src/api/gateway/Schemas.js +19 -0
- package/src/api/health/Controllers.js +14 -0
- package/src/api/health/Logic.js +24 -0
- package/src/api/health/Router.js +12 -0
- package/src/api/httpTraffic/Controllers.js +29 -0
- package/src/api/httpTraffic/Logic.js +33 -0
- package/src/api/httpTraffic/Router.js +9 -0
- package/src/api/httpTraffic/Schemas.js +23 -0
- package/src/api/logging/Controllers.js +80 -0
- package/src/api/logging/Logic.js +461 -0
- package/src/api/logging/Router.js +24 -0
- package/src/api/logging/Schemas.js +43 -0
- package/src/api/mqtt/Controllers.js +17 -0
- package/src/api/mqtt/Logic.js +66 -0
- package/src/api/mqtt/Router.js +12 -0
- package/src/api/postgres/Controllers.js +97 -0
- package/src/api/postgres/Logic.js +221 -0
- package/src/api/postgres/Router.js +21 -0
- package/src/api/pubsub/Controllers.js +236 -0
- package/src/api/pubsub/Logic.js +732 -0
- package/src/api/pubsub/Router.js +41 -0
- package/src/api/pubsub/Schemas.js +355 -0
- package/src/api/redis/Controllers.js +63 -0
- package/src/api/redis/Logic.js +239 -0
- package/src/api/redis/Router.js +21 -0
- package/src/api/scheduler/Controllers.js +27 -0
- package/src/api/scheduler/Logic.js +49 -0
- package/src/api/scheduler/Router.js +16 -0
- package/src/api/services/Controllers.js +26 -0
- package/src/api/services/Logic.js +205 -0
- package/src/api/services/Router.js +14 -0
- package/src/api/services/Schemas.js +66 -0
- package/src/api/snapshots/Controllers.js +37 -0
- package/src/api/snapshots/Logic.js +797 -0
- package/src/api/snapshots/Router.js +15 -0
- package/src/api/snapshots/Schemas.js +23 -0
- package/src/api/webhooks/Controllers.js +49 -0
- package/src/api/webhooks/Logic.js +137 -0
- package/src/api/webhooks/Router.js +12 -0
- package/src/api/webhooks/Schemas.js +31 -0
- package/src/configs/Application.js +147 -0
- package/src/configs/Default.js +13 -0
- package/src/consumers/BlackboxLogsConsumer.js +235 -0
- package/src/consumers/DockerLogsConsumer.js +687 -0
- package/src/db/Tables.js +66 -0
- package/src/db/schemas/firestore.js +18 -0
- package/src/db/schemas/functions.js +65 -0
- package/src/db/schemas/httpTraffic.js +43 -0
- package/src/db/schemas/logging.js +74 -0
- package/src/db/schemas/migrations.js +64 -0
- package/src/db/schemas/mqtt.js +56 -0
- package/src/db/schemas/pubsub.js +90 -0
- package/src/db/schemas/pubsubRegistry.js +22 -0
- package/src/db/schemas/webhooks.js +28 -0
- package/src/emulation/awsiot/Controllers.js +91 -0
- package/src/emulation/awsiot/Logic.js +70 -0
- package/src/emulation/awsiot/Router.js +19 -0
- package/src/emulation/awsiot/Server.js +100 -0
- package/src/emulation/firestore/Server.js +136 -0
- package/src/emulation/logging/Controllers.js +212 -0
- package/src/emulation/logging/Logic.js +416 -0
- package/src/emulation/logging/Router.js +36 -0
- package/src/emulation/logging/Schemas.js +82 -0
- package/src/emulation/logging/Server.js +108 -0
- package/src/emulation/pubsub/Controllers.js +279 -0
- package/src/emulation/pubsub/DefaultTopics.js +162 -0
- package/src/emulation/pubsub/Logic.js +427 -0
- package/src/emulation/pubsub/README.md +309 -0
- package/src/emulation/pubsub/Router.js +33 -0
- package/src/emulation/pubsub/Server.js +104 -0
- package/src/emulation/pubsub/ShadowPoller.js +276 -0
- package/src/emulation/pubsub/ShadowSubscriptionManager.js +199 -0
- package/src/enums/ContainerNames.js +106 -0
- package/src/enums/ErrorReason.js +28 -0
- package/src/enums/FunctionStatuses.js +15 -0
- package/src/enums/FunctionTriggerTypes.js +15 -0
- package/src/enums/GatewayState.js +7 -0
- package/src/enums/ServiceNames.js +68 -0
- package/src/jobs/DatabaseMaintenance.js +184 -0
- package/src/jobs/MessageHistoryCleanup.js +152 -0
- package/src/mcp/ApiClient.js +25 -0
- package/src/mcp/Server.js +52 -0
- package/src/mcp/prompts/debugging.js +104 -0
- package/src/mcp/resources/platform.js +118 -0
- package/src/mcp/tools/data.js +84 -0
- package/src/mcp/tools/docker.js +166 -0
- package/src/mcp/tools/firestore.js +162 -0
- package/src/mcp/tools/functions.js +380 -0
- package/src/mcp/tools/httpTraffic.js +69 -0
- package/src/mcp/tools/logging.js +174 -0
- package/src/mcp/tools/mqtt.js +37 -0
- package/src/mcp/tools/postgres.js +130 -0
- package/src/mcp/tools/pubsub.js +316 -0
- package/src/mcp/tools/redis.js +146 -0
- package/src/mcp/tools/services.js +169 -0
- package/src/mcp/tools/snapshots.js +88 -0
- package/src/mcp/tools/webhooks.js +115 -0
- package/src/middleware/DevProxy.js +67 -0
- package/src/middleware/ErrorCatcher.js +35 -0
- package/src/middleware/HttpProxy.js +215 -0
- package/src/middleware/Reply.js +24 -0
- package/src/middleware/TraceId.js +9 -0
- package/src/middleware/WebhookProxy.js +234 -0
- package/src/protocols/mqtt/Broker.js +92 -0
- package/src/protocols/mqtt/Handlers.js +175 -0
- package/src/protocols/mqtt/PubSubBridge.js +162 -0
- package/src/protocols/mqtt/Server.js +116 -0
- package/src/runtime/FunctionRunner.js +179 -0
- package/src/services/AppGatewayService.js +582 -0
- package/src/singletons/FirestoreBroadcaster.js +367 -0
- package/src/singletons/FunctionTriggerDispatcher.js +456 -0
- package/src/singletons/FunctionsService.js +418 -0
- package/src/singletons/HttpProxy.js +224 -0
- package/src/singletons/LogBroadcaster.js +159 -0
- package/src/singletons/Logger.js +49 -0
- package/src/singletons/MemoryJsonStore.js +175 -0
- package/src/singletons/MessageBroadcaster.js +190 -0
- package/src/singletons/PostgresBroadcaster.js +367 -0
- package/src/singletons/PostgresClient.js +180 -0
- package/src/singletons/RedisClient.js +184 -0
- package/src/singletons/SqliteStore.js +480 -0
- package/src/singletons/TickService.js +151 -0
- 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
|
+
}
|