@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,199 @@
|
|
|
1
|
+
import { Application } from '../../configs/Application.js'
|
|
2
|
+
import { Logger } from '../../singletons/Logger.js'
|
|
3
|
+
|
|
4
|
+
const { pubsub } = Application
|
|
5
|
+
const SHADOW_SUFFIX = '-devtools-shadow'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Manages shadow subscriptions on the official Pub/Sub emulator.
|
|
9
|
+
* Creates shadow subscriptions for each user subscription to intercept messages.
|
|
10
|
+
*/
|
|
11
|
+
export class ShadowSubscriptionManager {
|
|
12
|
+
constructor () {
|
|
13
|
+
this.baseUrl = `http://${pubsub.emulatorHost}:${pubsub.emulatorPort}`
|
|
14
|
+
this.projectId = pubsub.projectId
|
|
15
|
+
this.checkIntervalMs = pubsub.shadowSubscriptionCheckIntervalMs
|
|
16
|
+
this.knownSubscriptions = new Set()
|
|
17
|
+
this.shadowSubscriptions = new Set()
|
|
18
|
+
this.shadowToTopic = new Map()
|
|
19
|
+
this.intervalId = null
|
|
20
|
+
this.isRunning = false
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async request (method, path, body = null) {
|
|
24
|
+
const url = `${this.baseUrl}${path}`
|
|
25
|
+
const options = {
|
|
26
|
+
method,
|
|
27
|
+
headers: { 'Content-Type': 'application/json' }
|
|
28
|
+
}
|
|
29
|
+
if (body) {
|
|
30
|
+
options.body = JSON.stringify(body)
|
|
31
|
+
}
|
|
32
|
+
const response = await fetch(url, options)
|
|
33
|
+
const text = await response.text()
|
|
34
|
+
if (!text) return { success: response.ok, status: response.status }
|
|
35
|
+
try {
|
|
36
|
+
return JSON.parse(text)
|
|
37
|
+
} catch {
|
|
38
|
+
return { success: response.ok, status: response.status, text }
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async listSubscriptions () {
|
|
43
|
+
try {
|
|
44
|
+
const result = await this.request('GET', `/v1/projects/${this.projectId}/subscriptions`)
|
|
45
|
+
const subscriptions = result.subscriptions || []
|
|
46
|
+
return subscriptions
|
|
47
|
+
} catch (error) {
|
|
48
|
+
Logger.log({
|
|
49
|
+
level: 'error',
|
|
50
|
+
message: 'Failed to list subscriptions from emulator',
|
|
51
|
+
data: { error: error.message, baseUrl: this.baseUrl }
|
|
52
|
+
})
|
|
53
|
+
return []
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async listTopics () {
|
|
58
|
+
try {
|
|
59
|
+
const result = await this.request('GET', `/v1/projects/${this.projectId}/topics`)
|
|
60
|
+
return result.topics || []
|
|
61
|
+
} catch (error) {
|
|
62
|
+
Logger.log({
|
|
63
|
+
level: 'error',
|
|
64
|
+
message: 'Failed to list topics from emulator',
|
|
65
|
+
data: { error: error.message }
|
|
66
|
+
})
|
|
67
|
+
return []
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async createShadowSubscription (topicName, originalSubscriptionName) {
|
|
72
|
+
const shadowSubscriptionId = this.getShadowSubscriptionId(originalSubscriptionName)
|
|
73
|
+
const shadowSubscriptionName = `projects/${this.projectId}/subscriptions/${shadowSubscriptionId}`
|
|
74
|
+
if (this.shadowSubscriptions.has(shadowSubscriptionName)) {
|
|
75
|
+
return null
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
const result = await this.request(
|
|
79
|
+
'PUT',
|
|
80
|
+
`/v1/projects/${this.projectId}/subscriptions/${shadowSubscriptionId}`,
|
|
81
|
+
{
|
|
82
|
+
topic: topicName,
|
|
83
|
+
ackDeadlineSeconds: 60
|
|
84
|
+
}
|
|
85
|
+
)
|
|
86
|
+
if (result.name || result.success) {
|
|
87
|
+
this.shadowSubscriptions.add(shadowSubscriptionName)
|
|
88
|
+
this.shadowToTopic.set(shadowSubscriptionName, topicName)
|
|
89
|
+
Logger.log({
|
|
90
|
+
level: 'info',
|
|
91
|
+
message: 'Shadow subscription created',
|
|
92
|
+
data: { shadowSubscriptionName, forSubscription: originalSubscriptionName, topic: topicName }
|
|
93
|
+
})
|
|
94
|
+
return shadowSubscriptionName
|
|
95
|
+
}
|
|
96
|
+
if (result.error?.code === 409 || result.error?.status === 'ALREADY_EXISTS') {
|
|
97
|
+
this.shadowSubscriptions.add(shadowSubscriptionName)
|
|
98
|
+
this.shadowToTopic.set(shadowSubscriptionName, topicName)
|
|
99
|
+
return shadowSubscriptionName
|
|
100
|
+
}
|
|
101
|
+
Logger.log({
|
|
102
|
+
level: 'warn',
|
|
103
|
+
message: 'Failed to create shadow subscription',
|
|
104
|
+
data: { result, originalSubscriptionName }
|
|
105
|
+
})
|
|
106
|
+
return null
|
|
107
|
+
} catch (error) {
|
|
108
|
+
Logger.log({
|
|
109
|
+
level: 'error',
|
|
110
|
+
message: 'Error creating shadow subscription',
|
|
111
|
+
data: { error: error.message, originalSubscriptionName }
|
|
112
|
+
})
|
|
113
|
+
return null
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
getShadowSubscriptionId (originalSubscriptionName) {
|
|
118
|
+
const parts = originalSubscriptionName.split('/')
|
|
119
|
+
const subscriptionId = parts[parts.length - 1]
|
|
120
|
+
return `${subscriptionId}${SHADOW_SUFFIX}`
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
isShadowSubscription (subscriptionName) {
|
|
124
|
+
return subscriptionName.includes(SHADOW_SUFFIX)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async checkForNewSubscriptions () {
|
|
128
|
+
const subscriptions = await this.listSubscriptions()
|
|
129
|
+
if (subscriptions.length > 0) {
|
|
130
|
+
Logger.log({
|
|
131
|
+
level: 'info',
|
|
132
|
+
message: 'Checking subscriptions',
|
|
133
|
+
data: { count: subscriptions.length, shadowCount: this.shadowSubscriptions.size }
|
|
134
|
+
})
|
|
135
|
+
}
|
|
136
|
+
for (const sub of subscriptions) {
|
|
137
|
+
const subName = sub.name
|
|
138
|
+
if (this.isShadowSubscription(subName)) {
|
|
139
|
+
this.shadowSubscriptions.add(subName)
|
|
140
|
+
// Store topic mapping for discovered shadow subscriptions
|
|
141
|
+
if (sub.topic && !this.shadowToTopic.has(subName)) {
|
|
142
|
+
this.shadowToTopic.set(subName, sub.topic)
|
|
143
|
+
}
|
|
144
|
+
continue
|
|
145
|
+
}
|
|
146
|
+
if (!this.knownSubscriptions.has(subName)) {
|
|
147
|
+
this.knownSubscriptions.add(subName)
|
|
148
|
+
await this.createShadowSubscription(sub.topic, subName)
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async start () {
|
|
154
|
+
if (this.isRunning) return
|
|
155
|
+
this.isRunning = true
|
|
156
|
+
Logger.log({
|
|
157
|
+
level: 'info',
|
|
158
|
+
message: 'Starting Shadow Subscription Manager',
|
|
159
|
+
data: {
|
|
160
|
+
emulatorHost: `${pubsub.emulatorHost}:${pubsub.emulatorPort}`,
|
|
161
|
+
checkIntervalMs: this.checkIntervalMs
|
|
162
|
+
}
|
|
163
|
+
})
|
|
164
|
+
await this.checkForNewSubscriptions()
|
|
165
|
+
this.intervalId = setInterval(async () => {
|
|
166
|
+
try {
|
|
167
|
+
await this.checkForNewSubscriptions()
|
|
168
|
+
} catch (error) {
|
|
169
|
+
Logger.log({
|
|
170
|
+
level: 'error',
|
|
171
|
+
message: 'Error in subscription check interval',
|
|
172
|
+
data: { error: error.message }
|
|
173
|
+
})
|
|
174
|
+
}
|
|
175
|
+
}, this.checkIntervalMs)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
stop () {
|
|
179
|
+
if (this.intervalId) {
|
|
180
|
+
clearInterval(this.intervalId)
|
|
181
|
+
this.intervalId = null
|
|
182
|
+
}
|
|
183
|
+
this.isRunning = false
|
|
184
|
+
Logger.log({
|
|
185
|
+
level: 'info',
|
|
186
|
+
message: 'Shadow Subscription Manager stopped'
|
|
187
|
+
})
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
getShadowSubscriptions () {
|
|
191
|
+
return Array.from(this.shadowSubscriptions)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
getTopicForShadow (shadowSubscriptionName) {
|
|
195
|
+
return this.shadowToTopic.get(shadowSubscriptionName) || null
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export const shadowSubscriptionManager = new ShadowSubscriptionManager()
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Container Names Enum
|
|
3
|
+
* Normalized identifiers for all Docker containers in the platform
|
|
4
|
+
* Use these constants instead of hardcoding container names
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export const ContainerNames = Object.freeze({
|
|
8
|
+
POSTGRES: 'postgres',
|
|
9
|
+
REDIS: 'redis',
|
|
10
|
+
REDIS_LOGS: 'redis-logs',
|
|
11
|
+
PUBSUB: 'pubsub',
|
|
12
|
+
FIRESTORE: 'firestore',
|
|
13
|
+
ELASTICSEARCH: 'elasticsearch',
|
|
14
|
+
KIBANA: 'kibana',
|
|
15
|
+
DEV_TOOLS_BACKEND: 'dev-tools-backend',
|
|
16
|
+
DEV_TOOLS_FRONTEND: 'dev-tools-frontend',
|
|
17
|
+
DEVICE_NATIVE: 'device-native',
|
|
18
|
+
DEVICE_SIMULATOR: 'device-simulator',
|
|
19
|
+
INTEGRATION_APALEO: 'integration-apaleo',
|
|
20
|
+
CORE_PMS: 'core-pms',
|
|
21
|
+
CORE_KEY: 'core-key',
|
|
22
|
+
INTEGRATION_MEWS: 'integration-mews',
|
|
23
|
+
INTEGRATION_OPERA: 'integration-opera'
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
export const ContainerDisplayNames = Object.freeze({
|
|
27
|
+
[ContainerNames.POSTGRES]: 'PostgreSQL',
|
|
28
|
+
[ContainerNames.REDIS]: 'Redis',
|
|
29
|
+
[ContainerNames.REDIS_LOGS]: 'Redis (Logs)',
|
|
30
|
+
[ContainerNames.PUBSUB]: 'GCP Pub/Sub Emulator',
|
|
31
|
+
[ContainerNames.FIRESTORE]: 'Firestore Emulator',
|
|
32
|
+
[ContainerNames.ELASTICSEARCH]: 'Elasticsearch',
|
|
33
|
+
[ContainerNames.KIBANA]: 'Kibana',
|
|
34
|
+
[ContainerNames.DEV_TOOLS_BACKEND]: 'Dev Tools Backend',
|
|
35
|
+
[ContainerNames.DEV_TOOLS_FRONTEND]: 'Dev Tools Frontend',
|
|
36
|
+
[ContainerNames.DEVICE_NATIVE]: 'Device Native',
|
|
37
|
+
[ContainerNames.DEVICE_SIMULATOR]: 'Device Simulator',
|
|
38
|
+
[ContainerNames.INTEGRATION_APALEO]: 'Integration Apaleo',
|
|
39
|
+
[ContainerNames.CORE_PMS]: 'Core PMS',
|
|
40
|
+
[ContainerNames.CORE_KEY]: 'Core Key',
|
|
41
|
+
[ContainerNames.INTEGRATION_MEWS]: 'Integration Mews',
|
|
42
|
+
[ContainerNames.INTEGRATION_OPERA]: 'Integration Opera'
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
export const ContainerDescriptions = Object.freeze({
|
|
46
|
+
[ContainerNames.POSTGRES]: 'PostgreSQL database for application data',
|
|
47
|
+
[ContainerNames.REDIS]: 'Redis cache and data store',
|
|
48
|
+
[ContainerNames.REDIS_LOGS]: 'Redis instance dedicated to log storage (256MB LRU)',
|
|
49
|
+
[ContainerNames.PUBSUB]: 'Google Cloud Pub/Sub emulation for messaging',
|
|
50
|
+
[ContainerNames.FIRESTORE]: 'Google Cloud Firestore NoSQL database emulation',
|
|
51
|
+
[ContainerNames.ELASTICSEARCH]: 'Elasticsearch search and analytics engine',
|
|
52
|
+
[ContainerNames.KIBANA]: 'Kibana visualization dashboard for Elasticsearch',
|
|
53
|
+
[ContainerNames.DEV_TOOLS_BACKEND]: 'Dev Tools API server and emulators',
|
|
54
|
+
[ContainerNames.DEV_TOOLS_FRONTEND]: 'Dev Tools web UI (nginx)',
|
|
55
|
+
[ContainerNames.DEVICE_NATIVE]: 'Device native communication service',
|
|
56
|
+
[ContainerNames.DEVICE_SIMULATOR]: 'Device simulator for testing',
|
|
57
|
+
[ContainerNames.INTEGRATION_APALEO]: 'Apaleo PMS integration',
|
|
58
|
+
[ContainerNames.CORE_PMS]: 'Core PMS service',
|
|
59
|
+
[ContainerNames.CORE_KEY]: 'Core key management service',
|
|
60
|
+
[ContainerNames.INTEGRATION_MEWS]: 'Mews PMS integration',
|
|
61
|
+
[ContainerNames.INTEGRATION_OPERA]: 'Opera PMS integration'
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
export const ContainerPorts = Object.freeze({
|
|
65
|
+
[ContainerNames.POSTGRES]: 5432,
|
|
66
|
+
[ContainerNames.REDIS]: 6379,
|
|
67
|
+
[ContainerNames.REDIS_LOGS]: 6380,
|
|
68
|
+
[ContainerNames.PUBSUB]: 8085,
|
|
69
|
+
[ContainerNames.FIRESTORE]: 8080,
|
|
70
|
+
[ContainerNames.ELASTICSEARCH]: 9200,
|
|
71
|
+
[ContainerNames.KIBANA]: 5601,
|
|
72
|
+
[ContainerNames.DEV_TOOLS_BACKEND]: 9000,
|
|
73
|
+
[ContainerNames.DEV_TOOLS_FRONTEND]: 9001,
|
|
74
|
+
[ContainerNames.DEVICE_NATIVE]: 3000,
|
|
75
|
+
[ContainerNames.DEVICE_SIMULATOR]: 3001,
|
|
76
|
+
[ContainerNames.INTEGRATION_APALEO]: 3002,
|
|
77
|
+
[ContainerNames.CORE_PMS]: 3003,
|
|
78
|
+
[ContainerNames.CORE_KEY]: 3004,
|
|
79
|
+
[ContainerNames.INTEGRATION_MEWS]: 3005,
|
|
80
|
+
[ContainerNames.INTEGRATION_OPERA]: 3006
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
// Default service configuration (which services are on by default)
|
|
84
|
+
export const ContainerDefaults = Object.freeze({
|
|
85
|
+
[ContainerNames.POSTGRES]: false,
|
|
86
|
+
[ContainerNames.REDIS]: true,
|
|
87
|
+
[ContainerNames.REDIS_LOGS]: false,
|
|
88
|
+
[ContainerNames.PUBSUB]: true,
|
|
89
|
+
[ContainerNames.FIRESTORE]: false,
|
|
90
|
+
[ContainerNames.ELASTICSEARCH]: false,
|
|
91
|
+
[ContainerNames.KIBANA]: false
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Get all container names as array
|
|
96
|
+
*/
|
|
97
|
+
export const getAllContainerNames = () => {
|
|
98
|
+
return Object.values(ContainerNames)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Validate container name
|
|
103
|
+
*/
|
|
104
|
+
export const isValidContainerName = (name) => {
|
|
105
|
+
return getAllContainerNames().includes(name)
|
|
106
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export const ErrorReason = Object.freeze({
|
|
2
|
+
// General errors
|
|
3
|
+
validationFailed: 'validationFailed',
|
|
4
|
+
resourceNotFound: 'resourceNotFound',
|
|
5
|
+
resourceAlreadyExists: 'resourceAlreadyExists',
|
|
6
|
+
collectionNotFound: 'collectionNotFound',
|
|
7
|
+
// Pub/Sub errors
|
|
8
|
+
topicNotFound: 'topicNotFound',
|
|
9
|
+
topicAlreadyExists: 'topicAlreadyExists',
|
|
10
|
+
subscriptionNotFound: 'subscriptionNotFound',
|
|
11
|
+
subscriptionAlreadyExists: 'subscriptionAlreadyExists',
|
|
12
|
+
messageNotFound: 'messageNotFound',
|
|
13
|
+
invalidMessageData: 'invalidMessageData',
|
|
14
|
+
// Logging errors
|
|
15
|
+
logEntryNotFound: 'logEntryNotFound',
|
|
16
|
+
invalidLogEntry: 'invalidLogEntry',
|
|
17
|
+
// Service errors
|
|
18
|
+
serviceNotFound: 'serviceNotFound',
|
|
19
|
+
serviceAlreadyRunning: 'serviceAlreadyRunning',
|
|
20
|
+
serviceNotRunning: 'serviceNotRunning',
|
|
21
|
+
serviceStartFailed: 'serviceStartFailed',
|
|
22
|
+
serviceStopFailed: 'serviceStopFailed',
|
|
23
|
+
// Storage errors
|
|
24
|
+
storageError: 'storageError',
|
|
25
|
+
flushFailed: 'flushFailed'
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
export const ErrorReasonValues = Object.values(ErrorReason)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloud Function Runtime Statuses
|
|
3
|
+
* Tracks the lifecycle state of a function process
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const FunctionStatuses = Object.freeze({
|
|
7
|
+
STOPPED: 'stopped',
|
|
8
|
+
STARTING: 'starting',
|
|
9
|
+
RUNNING: 'running',
|
|
10
|
+
ERROR: 'error'
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
export const FunctionStatusList = Object.values(FunctionStatuses)
|
|
14
|
+
|
|
15
|
+
export const isValidFunctionStatus = (status) => FunctionStatusList.includes(status)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloud Function Trigger Types
|
|
3
|
+
* Defines how a function is invoked
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const FunctionTriggerTypes = Object.freeze({
|
|
7
|
+
HTTP: 'http',
|
|
8
|
+
PUBSUB: 'pubsub',
|
|
9
|
+
FIRESTORE: 'firestore',
|
|
10
|
+
SCHEDULER: 'scheduler'
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
export const FunctionTriggerTypeList = Object.values(FunctionTriggerTypes)
|
|
14
|
+
|
|
15
|
+
export const isValidTriggerType = (type) => FunctionTriggerTypeList.includes(type)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Service Names Enum
|
|
3
|
+
* Normalized identifiers for all services in the platform
|
|
4
|
+
* Use these constants instead of hardcoding service names
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export const ServiceNames = Object.freeze({
|
|
8
|
+
PUBSUB: 'pubsub',
|
|
9
|
+
LOGGING: 'logging',
|
|
10
|
+
MQTT: 'mqtt',
|
|
11
|
+
FIRESTORE: 'firestore',
|
|
12
|
+
WEBUI: 'webui',
|
|
13
|
+
APP_GATEWAY: 'appGateway',
|
|
14
|
+
HTTP_PROXY: 'http',
|
|
15
|
+
WEBHOOK_PROXY: 'webhook',
|
|
16
|
+
CLOUD_FUNCTIONS: 'cloudFunctions',
|
|
17
|
+
// Docker container services (managed externally)
|
|
18
|
+
ELASTICSEARCH: 'elasticsearch',
|
|
19
|
+
KIBANA: 'kibana',
|
|
20
|
+
POSTGRES: 'postgres',
|
|
21
|
+
REDIS: 'redis'
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
export const ServiceDisplayNames = Object.freeze({
|
|
25
|
+
[ServiceNames.PUBSUB]: 'GCP Pub/Sub Emulator',
|
|
26
|
+
[ServiceNames.LOGGING]: 'Cloud Logging Emulator',
|
|
27
|
+
[ServiceNames.MQTT]: 'MQTT Broker',
|
|
28
|
+
[ServiceNames.FIRESTORE]: 'Cloud Firestore Emulator',
|
|
29
|
+
[ServiceNames.WEBUI]: 'Web UI & API Server',
|
|
30
|
+
[ServiceNames.APP_GATEWAY]: 'App Gateway',
|
|
31
|
+
[ServiceNames.HTTP_PROXY]: 'HTTP',
|
|
32
|
+
[ServiceNames.WEBHOOK_PROXY]: 'Webhook',
|
|
33
|
+
[ServiceNames.CLOUD_FUNCTIONS]: 'Cloud Functions',
|
|
34
|
+
[ServiceNames.ELASTICSEARCH]: 'Elasticsearch',
|
|
35
|
+
[ServiceNames.KIBANA]: 'Kibana',
|
|
36
|
+
[ServiceNames.POSTGRES]: 'PostgreSQL',
|
|
37
|
+
[ServiceNames.REDIS]: 'Redis'
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
export const ServiceDescriptions = Object.freeze({
|
|
41
|
+
[ServiceNames.PUBSUB]: 'Google Cloud Pub/Sub emulation for topic and subscription management',
|
|
42
|
+
[ServiceNames.LOGGING]: 'Google Cloud Logging emulation for log entry management',
|
|
43
|
+
[ServiceNames.MQTT]: 'MQTT message broker for device communication',
|
|
44
|
+
[ServiceNames.FIRESTORE]: 'Google Cloud Firestore emulation for NoSQL database',
|
|
45
|
+
[ServiceNames.WEBUI]: 'Management dashboard and REST API server',
|
|
46
|
+
[ServiceNames.APP_GATEWAY]: 'Virtual gateway for device-native to device-simulator communication',
|
|
47
|
+
[ServiceNames.HTTP_PROXY]: 'HTTP proxy for inter-service traffic monitoring',
|
|
48
|
+
[ServiceNames.WEBHOOK_PROXY]: 'Webhook proxy for external service callbacks',
|
|
49
|
+
[ServiceNames.CLOUD_FUNCTIONS]: 'Google Cloud Functions emulation for serverless function management',
|
|
50
|
+
[ServiceNames.ELASTICSEARCH]: 'Elasticsearch search and analytics engine',
|
|
51
|
+
[ServiceNames.KIBANA]: 'Kibana visualization dashboard for Elasticsearch',
|
|
52
|
+
[ServiceNames.POSTGRES]: 'PostgreSQL database for application data',
|
|
53
|
+
[ServiceNames.REDIS]: 'Redis cache and data store'
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Get all service names as array
|
|
58
|
+
*/
|
|
59
|
+
export const getAllServiceNames = () => {
|
|
60
|
+
return Object.values(ServiceNames)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Validate service name
|
|
65
|
+
*/
|
|
66
|
+
export const isValidServiceName = (name) => {
|
|
67
|
+
return getAllServiceNames().includes(name)
|
|
68
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
// Periodic database maintenance job
|
|
2
|
+
// Trims unbounded tables, checkpoints WAL, and updates query planner stats
|
|
3
|
+
|
|
4
|
+
import { SqliteStore } from '../singletons/SqliteStore.js'
|
|
5
|
+
import { Application } from '../configs/Application.js'
|
|
6
|
+
import { Logger } from '../singletons/Logger.js'
|
|
7
|
+
import {
|
|
8
|
+
LOGGING_ENTRIES,
|
|
9
|
+
HTTP_TRAFFIC,
|
|
10
|
+
MQTT_MESSAGES,
|
|
11
|
+
PUBSUB_MESSAGES,
|
|
12
|
+
CLOUD_FUNCTION_INVOCATIONS
|
|
13
|
+
} from '../db/Tables.js'
|
|
14
|
+
|
|
15
|
+
const TABLES_TO_TRIM = [
|
|
16
|
+
{ table: LOGGING_ENTRIES, column: 'created_at', configKey: 'loggingEntries' },
|
|
17
|
+
{ table: HTTP_TRAFFIC, column: 'created_at', configKey: 'httpTraffic' },
|
|
18
|
+
{ table: MQTT_MESSAGES, column: 'created_at', configKey: 'mqttMessages' },
|
|
19
|
+
{ table: PUBSUB_MESSAGES, column: 'created_at', configKey: 'pubsubMessages' },
|
|
20
|
+
{ table: CLOUD_FUNCTION_INVOCATIONS, column: 'created_at', configKey: 'functionInvocations' }
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Delete rows older than retention period from a table
|
|
25
|
+
*/
|
|
26
|
+
const trimTable = (table, column, retentionDays) => {
|
|
27
|
+
const cutoff = new Date(Date.now() - retentionDays * 24 * 60 * 60 * 1000).toISOString()
|
|
28
|
+
try {
|
|
29
|
+
const result = SqliteStore.db.prepare(
|
|
30
|
+
`DELETE FROM ${table} WHERE ${column} < ?`
|
|
31
|
+
).run(cutoff)
|
|
32
|
+
return result.changes
|
|
33
|
+
} catch (error) {
|
|
34
|
+
Logger.log({
|
|
35
|
+
level: 'warn',
|
|
36
|
+
message: `Failed to trim ${table}`,
|
|
37
|
+
data: { error: error.message }
|
|
38
|
+
})
|
|
39
|
+
return 0
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Enforce max row count on logging_entries by deleting oldest rows
|
|
45
|
+
*/
|
|
46
|
+
const enforceLoggingLimit = (maxEntries) => {
|
|
47
|
+
try {
|
|
48
|
+
const countResult = SqliteStore.db.prepare(
|
|
49
|
+
`SELECT COUNT(*) as count FROM ${LOGGING_ENTRIES}`
|
|
50
|
+
).get()
|
|
51
|
+
const count = countResult.count
|
|
52
|
+
if (count <= maxEntries) return 0
|
|
53
|
+
const excess = count - maxEntries
|
|
54
|
+
const result = SqliteStore.db.prepare(`
|
|
55
|
+
DELETE FROM ${LOGGING_ENTRIES}
|
|
56
|
+
WHERE _id IN (
|
|
57
|
+
SELECT _id FROM ${LOGGING_ENTRIES}
|
|
58
|
+
ORDER BY created_at ASC
|
|
59
|
+
LIMIT ?
|
|
60
|
+
)
|
|
61
|
+
`).run(excess)
|
|
62
|
+
return result.changes
|
|
63
|
+
} catch (error) {
|
|
64
|
+
Logger.log({
|
|
65
|
+
level: 'warn',
|
|
66
|
+
message: 'Failed to enforce logging entry limit',
|
|
67
|
+
data: { error: error.message }
|
|
68
|
+
})
|
|
69
|
+
return 0
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Run full database maintenance cycle
|
|
75
|
+
*/
|
|
76
|
+
export const runDatabaseMaintenance = () => {
|
|
77
|
+
const { retentionDays, maxLoggingEntries } = Application.dbMaintenance
|
|
78
|
+
const results = {}
|
|
79
|
+
|
|
80
|
+
// Trim tables by retention period
|
|
81
|
+
for (const { table, column, configKey } of TABLES_TO_TRIM) {
|
|
82
|
+
const days = retentionDays[configKey]
|
|
83
|
+
const deleted = trimTable(table, column, days)
|
|
84
|
+
if (deleted > 0) {
|
|
85
|
+
results[table] = { deleted, retentionDays: days }
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Enforce logging entry count limit
|
|
90
|
+
const loggingTrimmed = enforceLoggingLimit(maxLoggingEntries)
|
|
91
|
+
if (loggingTrimmed > 0) {
|
|
92
|
+
results[`${LOGGING_ENTRIES}_limit`] = { deleted: loggingTrimmed, maxEntries: maxLoggingEntries }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// WAL checkpoint
|
|
96
|
+
try {
|
|
97
|
+
SqliteStore.db.pragma('wal_checkpoint(PASSIVE)')
|
|
98
|
+
} catch (error) {
|
|
99
|
+
Logger.log({
|
|
100
|
+
level: 'warn',
|
|
101
|
+
message: 'WAL checkpoint failed',
|
|
102
|
+
data: { error: error.message }
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Update query planner stats
|
|
107
|
+
try {
|
|
108
|
+
SqliteStore.db.exec('ANALYZE')
|
|
109
|
+
} catch (error) {
|
|
110
|
+
Logger.log({
|
|
111
|
+
level: 'warn',
|
|
112
|
+
message: 'ANALYZE failed',
|
|
113
|
+
data: { error: error.message }
|
|
114
|
+
})
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const totalDeleted = Object.values(results).reduce((sum, r) => sum + r.deleted, 0)
|
|
118
|
+
if (totalDeleted > 0) {
|
|
119
|
+
Logger.log({
|
|
120
|
+
level: 'info',
|
|
121
|
+
message: 'Database maintenance completed',
|
|
122
|
+
data: { totalDeleted, tables: results }
|
|
123
|
+
})
|
|
124
|
+
} else {
|
|
125
|
+
Logger.log({
|
|
126
|
+
level: 'debug',
|
|
127
|
+
message: 'Database maintenance completed - nothing to trim'
|
|
128
|
+
})
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return results
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Start the scheduled maintenance job
|
|
136
|
+
*/
|
|
137
|
+
export const startMaintenanceJob = () => {
|
|
138
|
+
const { intervalHours } = Application.dbMaintenance
|
|
139
|
+
const intervalMs = intervalHours * 60 * 60 * 1000
|
|
140
|
+
|
|
141
|
+
Logger.log({
|
|
142
|
+
level: 'info',
|
|
143
|
+
message: 'Starting database maintenance job',
|
|
144
|
+
data: {
|
|
145
|
+
intervalHours,
|
|
146
|
+
retentionDays: Application.dbMaintenance.retentionDays,
|
|
147
|
+
maxLoggingEntries: Application.dbMaintenance.maxLoggingEntries
|
|
148
|
+
}
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
// Delay first run by 30 seconds to let server fully start
|
|
152
|
+
setTimeout(() => {
|
|
153
|
+
try {
|
|
154
|
+
runDatabaseMaintenance()
|
|
155
|
+
} catch (error) {
|
|
156
|
+
Logger.log({
|
|
157
|
+
level: 'error',
|
|
158
|
+
message: 'Initial database maintenance failed',
|
|
159
|
+
data: { error: error.message }
|
|
160
|
+
})
|
|
161
|
+
}
|
|
162
|
+
}, 30000)
|
|
163
|
+
|
|
164
|
+
return setInterval(() => {
|
|
165
|
+
try {
|
|
166
|
+
runDatabaseMaintenance()
|
|
167
|
+
} catch (error) {
|
|
168
|
+
Logger.log({
|
|
169
|
+
level: 'error',
|
|
170
|
+
message: 'Scheduled database maintenance failed',
|
|
171
|
+
data: { error: error.message }
|
|
172
|
+
})
|
|
173
|
+
}
|
|
174
|
+
}, intervalMs)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Stop the maintenance job
|
|
179
|
+
*/
|
|
180
|
+
export const stopMaintenanceJob = (timer) => {
|
|
181
|
+
if (timer) {
|
|
182
|
+
clearInterval(timer)
|
|
183
|
+
}
|
|
184
|
+
}
|