@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,232 @@
|
|
|
1
|
+
# Testing Patterns
|
|
2
|
+
|
|
3
|
+
## Test structure
|
|
4
|
+
|
|
5
|
+
Coding pattern:
|
|
6
|
+
```javascript
|
|
7
|
+
import { describe, it, beforeEach } from 'mocha'
|
|
8
|
+
import { expect } from 'chai'
|
|
9
|
+
|
|
10
|
+
describe('Feature Name', () => {
|
|
11
|
+
beforeEach(async function () {
|
|
12
|
+
const { id } = this.currentTest.ctx
|
|
13
|
+
this.testId = id
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
describe('Happy paths', () => {
|
|
17
|
+
it('should work with valid input', async function () {
|
|
18
|
+
// Test implementation
|
|
19
|
+
})
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
describe('Extended positive tests', () => {
|
|
23
|
+
// Additional success cases
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
describe('Edge cases', () => {
|
|
27
|
+
// Boundary conditions
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
describe('Validation', () => {
|
|
31
|
+
// Invalid input handling
|
|
32
|
+
})
|
|
33
|
+
})
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## API endpoint testing
|
|
37
|
+
|
|
38
|
+
Coding pattern:
|
|
39
|
+
```javascript
|
|
40
|
+
import { describe, it, beforeEach } from 'mocha'
|
|
41
|
+
import Chai, { expect } from 'chai'
|
|
42
|
+
import ChaiHttp from 'chai-http'
|
|
43
|
+
import { Application } from '../../../configs/Application.js'
|
|
44
|
+
|
|
45
|
+
Chai.use(ChaiHttp)
|
|
46
|
+
|
|
47
|
+
const endpoint = {
|
|
48
|
+
method: 'post',
|
|
49
|
+
url: '/v1/devices/create'
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
describe(`Testing API endpoint: ${endpoint.method.toUpperCase()} ${endpoint.url}`, () => {
|
|
53
|
+
beforeEach(async function () {
|
|
54
|
+
const { id } = this.currentTest.ctx
|
|
55
|
+
this.request = () => Chai.request(Application.server.base)[endpoint.method](endpoint.url)
|
|
56
|
+
.set('test-id', id)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('should create device successfully', async function () {
|
|
60
|
+
const response = await this.request().send({
|
|
61
|
+
propertyId: 'prop-123',
|
|
62
|
+
model: 1
|
|
63
|
+
})
|
|
64
|
+
expect(response).to.have.status(200)
|
|
65
|
+
expect(response.body.device).to.exist
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('should reject missing required field', async function () {
|
|
69
|
+
const response = await this.request().send({
|
|
70
|
+
model: 1
|
|
71
|
+
// Missing propertyId
|
|
72
|
+
})
|
|
73
|
+
expect(response).to.have.status(400)
|
|
74
|
+
})
|
|
75
|
+
})
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Pub/Sub subscriber testing
|
|
79
|
+
|
|
80
|
+
Coding pattern:
|
|
81
|
+
```javascript
|
|
82
|
+
import { describe, it, beforeEach } from 'mocha'
|
|
83
|
+
import { expect } from 'chai'
|
|
84
|
+
import { PubSubSubscriberProbe } from '@gokiteam/lab'
|
|
85
|
+
import { Application } from '../../../configs/Application.js'
|
|
86
|
+
|
|
87
|
+
describe('EventReceived Subscriber', () => {
|
|
88
|
+
beforeEach(async function () {
|
|
89
|
+
const { id } = this.currentTest.ctx
|
|
90
|
+
this.testId = id
|
|
91
|
+
this.probe = new PubSubSubscriberProbe({
|
|
92
|
+
projectId: Application.pubSub.projectId,
|
|
93
|
+
keyFilename: Application.pubSub.keyFilename
|
|
94
|
+
})
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('should process event successfully', async function () {
|
|
98
|
+
const topicName = Application.pubSub.subscribers.eventReceived.subscriptionName
|
|
99
|
+
await this.probe.publishMessage({
|
|
100
|
+
topicName,
|
|
101
|
+
message: {
|
|
102
|
+
deviceId: 'test-device-id',
|
|
103
|
+
action: 'update'
|
|
104
|
+
},
|
|
105
|
+
attributes: { traceId: this.testId }
|
|
106
|
+
})
|
|
107
|
+
const result = await this.probe.waitForResult({
|
|
108
|
+
topicName,
|
|
109
|
+
traceId: this.testId,
|
|
110
|
+
timeoutMilliseconds: 5000
|
|
111
|
+
})
|
|
112
|
+
expect(result.state).to.equal('processedSuccessfully')
|
|
113
|
+
})
|
|
114
|
+
})
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Logic function testing
|
|
118
|
+
|
|
119
|
+
### Direct testing
|
|
120
|
+
```javascript
|
|
121
|
+
import { describe, it } from 'mocha'
|
|
122
|
+
import { expect } from 'chai'
|
|
123
|
+
import { PrepareDeviceStatusRecord } from '../../../src/logic/PrepareDeviceStatusRecord.js'
|
|
124
|
+
|
|
125
|
+
describe('PrepareDeviceStatusRecord', () => {
|
|
126
|
+
it('should prepare device status record', () => {
|
|
127
|
+
const device = { doc: { propertyId: 'prop-123' } }
|
|
128
|
+
const deviceStatus = { doc: { batteryStatus: { percentage: 75 } } }
|
|
129
|
+
const deviceCurrentTimestamp = 1234567890
|
|
130
|
+
const result = PrepareDeviceStatusRecord({
|
|
131
|
+
device,
|
|
132
|
+
deviceStatus,
|
|
133
|
+
deviceCurrentTimestamp
|
|
134
|
+
})
|
|
135
|
+
expect(result).to.be.instanceOf(Buffer)
|
|
136
|
+
})
|
|
137
|
+
})
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Via HTTP endpoint
|
|
141
|
+
```javascript
|
|
142
|
+
describe('ValidateDeviceControlOperation', () => {
|
|
143
|
+
beforeEach(async function () {
|
|
144
|
+
const { id } = this.currentTest.ctx
|
|
145
|
+
this.request = () => Chai.request(Application.server.base)
|
|
146
|
+
.post('/logic/ValidateDeviceControlOperation')
|
|
147
|
+
.set('test-id', id)
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
it('should validate unlock operation', async function () {
|
|
151
|
+
const response = await this.request().send({
|
|
152
|
+
key: { /* key data */ },
|
|
153
|
+
device: { /* device data */ },
|
|
154
|
+
deviceStatus: { /* status data */ },
|
|
155
|
+
deviceCurrentTimestamp: 1234567890,
|
|
156
|
+
isLockOperation: false
|
|
157
|
+
})
|
|
158
|
+
expect(response).to.have.status(200)
|
|
159
|
+
expect(response.body.errorCode).to.be.null
|
|
160
|
+
})
|
|
161
|
+
})
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Common assertions
|
|
165
|
+
|
|
166
|
+
### Status codes
|
|
167
|
+
```javascript
|
|
168
|
+
expect(response).to.have.status(200)
|
|
169
|
+
expect(response).to.have.status(400)
|
|
170
|
+
expect(response).to.have.status(404)
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Response body
|
|
174
|
+
```javascript
|
|
175
|
+
expect(response.body).to.exist
|
|
176
|
+
expect(response.body.device).to.have.property('macAddress')
|
|
177
|
+
expect(response.body.device.macAddress).to.be.a('string')
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Arrays
|
|
181
|
+
```javascript
|
|
182
|
+
expect(response.body.devices).to.be.an('array')
|
|
183
|
+
expect(response.body.devices).to.have.lengthOf(5)
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Pattern matching
|
|
187
|
+
```javascript
|
|
188
|
+
expect(response.body.device.macAddress).to.match(/^([0-9A-F]{2}:){5}[0-9A-F]{2}$/)
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## Test data management
|
|
192
|
+
|
|
193
|
+
### Using collections
|
|
194
|
+
```javascript
|
|
195
|
+
beforeEach(async function () {
|
|
196
|
+
const { id } = this.currentTest.ctx
|
|
197
|
+
const { collections } = this
|
|
198
|
+
this.device = await collections.devices.create({
|
|
199
|
+
propertyId: 'prop-123',
|
|
200
|
+
macAddress: '00:11:22:33:44:55',
|
|
201
|
+
model: 1
|
|
202
|
+
})
|
|
203
|
+
this.deviceStatus = await collections.deviceStatuses.create({
|
|
204
|
+
deviceId: this.device.id
|
|
205
|
+
})
|
|
206
|
+
})
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## Running tests
|
|
210
|
+
|
|
211
|
+
```bash
|
|
212
|
+
# All tests
|
|
213
|
+
npm test
|
|
214
|
+
|
|
215
|
+
# Parallel execution
|
|
216
|
+
npm run test:parallel
|
|
217
|
+
|
|
218
|
+
# API tests only
|
|
219
|
+
npm test:api
|
|
220
|
+
|
|
221
|
+
# Logic tests only
|
|
222
|
+
npm test:logic
|
|
223
|
+
|
|
224
|
+
# Pub/Sub tests only
|
|
225
|
+
npm test:pubsub
|
|
226
|
+
|
|
227
|
+
# Specific test file
|
|
228
|
+
npm test -- tests/component/app/devices/create.js
|
|
229
|
+
|
|
230
|
+
# Specific test by name
|
|
231
|
+
npm test -- --grep "should create device"
|
|
232
|
+
```
|
package/src/Server.js
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import Koa from 'koa'
|
|
2
|
+
import bodyParser from 'koa-bodyparser'
|
|
3
|
+
import KoaRouter from 'koa-router'
|
|
4
|
+
import cors from '@koa/cors'
|
|
5
|
+
import { Application } from './configs/Application.js'
|
|
6
|
+
import { Logger } from './singletons/Logger.js'
|
|
7
|
+
import { SqliteStore } from './singletons/SqliteStore.js'
|
|
8
|
+
import { ErrorCatcher } from './middleware/ErrorCatcher.js'
|
|
9
|
+
import { Reply } from './middleware/Reply.js'
|
|
10
|
+
import { TraceId } from './middleware/TraceId.js'
|
|
11
|
+
import { Router as HealthRouter } from './api/health/Router.js'
|
|
12
|
+
import { Router as DashboardRouter } from './api/dashboard/Router.js'
|
|
13
|
+
import { Router as ServicesRouter } from './api/services/Router.js'
|
|
14
|
+
import { Router as ExportRouter } from './api/export/Router.js'
|
|
15
|
+
import { Router as PubsubRouter } from './api/pubsub/Router.js'
|
|
16
|
+
import { Router as LoggingRouter } from './api/logging/Router.js'
|
|
17
|
+
import { Router as MqttRouter } from './api/mqtt/Router.js'
|
|
18
|
+
import { Router as DataRouter } from './api/data/Router.js'
|
|
19
|
+
import { Router as FirestoreRouter } from './api/firestore/Router.js'
|
|
20
|
+
import { Router as PostgresRouter } from './api/postgres/Router.js'
|
|
21
|
+
import { PostgresClient } from './singletons/PostgresClient.js'
|
|
22
|
+
import { RedisClient } from './singletons/RedisClient.js'
|
|
23
|
+
import { Router as RedisRouter } from './api/redis/Router.js'
|
|
24
|
+
import { Router as SchedulerRouter } from './api/scheduler/Router.js'
|
|
25
|
+
import { Router as GatewayRouter } from './api/gateway/Router.js'
|
|
26
|
+
import { Router as DockerRouter } from './api/docker/Router.js'
|
|
27
|
+
import { Router as DocsRouter } from './api/docs/Router.js'
|
|
28
|
+
import { Router as WebhooksRouter } from './api/webhooks/Router.js'
|
|
29
|
+
import { Router as HttpTrafficRouter } from './api/httpTraffic/Router.js'
|
|
30
|
+
import { Router as SnapshotsRouter } from './api/snapshots/Router.js'
|
|
31
|
+
import { Router as FunctionsRouter } from './api/functions/Router.js'
|
|
32
|
+
import { WebhookProxy } from './singletons/WebhookProxy.js'
|
|
33
|
+
import { HttpProxy } from './singletons/HttpProxy.js'
|
|
34
|
+
import { WebhookProxyMiddleware } from './middleware/WebhookProxy.js'
|
|
35
|
+
import { HttpProxyMiddleware } from './middleware/HttpProxy.js'
|
|
36
|
+
import { MqttServer } from './protocols/mqtt/Server.js'
|
|
37
|
+
import { start as startLoggingServer } from './emulation/logging/Server.js'
|
|
38
|
+
import { startPubSubServer } from './emulation/pubsub/Server.js'
|
|
39
|
+
import { startAwsIotServer } from './emulation/awsiot/Server.js'
|
|
40
|
+
import { startCleanupJob } from './jobs/MessageHistoryCleanup.js'
|
|
41
|
+
import { startMaintenanceJob } from './jobs/DatabaseMaintenance.js'
|
|
42
|
+
import { initializeDefaultTopics } from './emulation/pubsub/DefaultTopics.js'
|
|
43
|
+
import { BlackboxLogsConsumer } from './consumers/BlackboxLogsConsumer.js'
|
|
44
|
+
import { DockerLogsConsumer } from './consumers/DockerLogsConsumer.js'
|
|
45
|
+
import { AppGatewayService } from './services/AppGatewayService.js'
|
|
46
|
+
import { FunctionsService } from './singletons/FunctionsService.js'
|
|
47
|
+
import { FunctionTriggerDispatcher } from './singletons/FunctionTriggerDispatcher.js'
|
|
48
|
+
|
|
49
|
+
const { environment, runId, ports } = Application
|
|
50
|
+
|
|
51
|
+
const start = async () => {
|
|
52
|
+
try {
|
|
53
|
+
SqliteStore.initialize()
|
|
54
|
+
WebhookProxy.initialize()
|
|
55
|
+
HttpProxy.initialize()
|
|
56
|
+
PostgresClient.initialize()
|
|
57
|
+
RedisClient.initialize()
|
|
58
|
+
const app = new Koa()
|
|
59
|
+
app.use(cors({
|
|
60
|
+
origin: '*', // Allow all origins in development
|
|
61
|
+
credentials: true,
|
|
62
|
+
allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
|
|
63
|
+
allowHeaders: ['Content-Type', 'Authorization', 'X-Trace-Id']
|
|
64
|
+
}))
|
|
65
|
+
app.use(ErrorCatcher())
|
|
66
|
+
app.use(TraceId())
|
|
67
|
+
app.use(WebhookProxyMiddleware())
|
|
68
|
+
app.use(HttpProxyMiddleware())
|
|
69
|
+
app.use(Reply())
|
|
70
|
+
app.use(bodyParser())
|
|
71
|
+
const router = new KoaRouter()
|
|
72
|
+
router.use(HealthRouter.routes())
|
|
73
|
+
router.use(DashboardRouter.routes())
|
|
74
|
+
router.use(ServicesRouter.routes())
|
|
75
|
+
router.use(ExportRouter.routes())
|
|
76
|
+
router.use(PubsubRouter.routes())
|
|
77
|
+
router.use(LoggingRouter.routes())
|
|
78
|
+
router.use(MqttRouter.routes())
|
|
79
|
+
router.use(DataRouter.routes())
|
|
80
|
+
router.use(FirestoreRouter.routes())
|
|
81
|
+
router.use(PostgresRouter.routes())
|
|
82
|
+
router.use(RedisRouter.routes())
|
|
83
|
+
router.use(SchedulerRouter.routes())
|
|
84
|
+
router.use(GatewayRouter.routes())
|
|
85
|
+
router.use(DockerRouter.routes())
|
|
86
|
+
router.use(DocsRouter.routes())
|
|
87
|
+
router.use(WebhooksRouter.routes())
|
|
88
|
+
router.use(HttpTrafficRouter.routes())
|
|
89
|
+
router.use(SnapshotsRouter.routes())
|
|
90
|
+
router.use(FunctionsRouter.routes())
|
|
91
|
+
app.use(router.routes())
|
|
92
|
+
app.use(router.allowedMethods())
|
|
93
|
+
|
|
94
|
+
// Frontend is served separately by goki-dev-tools-frontend container (nginx)
|
|
95
|
+
// Backend only handles API routes starting with /v1
|
|
96
|
+
Logger.log({
|
|
97
|
+
level: 'info',
|
|
98
|
+
message: 'Backend server ready - API endpoints only (frontend served separately)'
|
|
99
|
+
})
|
|
100
|
+
const server = app.listen(ports.webUi, () => {
|
|
101
|
+
Logger.log({
|
|
102
|
+
level: 'info',
|
|
103
|
+
message: `Server started on port ${ports.webUi}`,
|
|
104
|
+
data: { environment, runId, port: ports.webUi }
|
|
105
|
+
})
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
// Start MQTT broker
|
|
109
|
+
MqttServer.start()
|
|
110
|
+
|
|
111
|
+
// Start AWS IoT Core HTTPS API emulation server
|
|
112
|
+
await startAwsIotServer()
|
|
113
|
+
|
|
114
|
+
// Start Pub/Sub emulation server
|
|
115
|
+
await startPubSubServer()
|
|
116
|
+
|
|
117
|
+
// Initialize default Pub/Sub topics (systemOneMinuteTick, etc.)
|
|
118
|
+
await initializeDefaultTopics()
|
|
119
|
+
|
|
120
|
+
// Start Cloud Logging emulation server
|
|
121
|
+
await startLoggingServer()
|
|
122
|
+
|
|
123
|
+
// Initialize Bull queue consumer for logs
|
|
124
|
+
const redisConfigs = []
|
|
125
|
+
redisConfigs.push({
|
|
126
|
+
host: Application.redis.host,
|
|
127
|
+
port: Application.redis.port,
|
|
128
|
+
db: Application.redis.db
|
|
129
|
+
})
|
|
130
|
+
if (
|
|
131
|
+
Application.redisLogs.host !== Application.redis.host ||
|
|
132
|
+
Application.redisLogs.port !== Application.redis.port
|
|
133
|
+
) {
|
|
134
|
+
redisConfigs.push({
|
|
135
|
+
host: Application.redisLogs.host,
|
|
136
|
+
port: Application.redisLogs.port,
|
|
137
|
+
db: Application.redisLogs.db
|
|
138
|
+
})
|
|
139
|
+
Logger.log({
|
|
140
|
+
level: 'info',
|
|
141
|
+
message: 'Using separate Redis instance for logs',
|
|
142
|
+
data: {
|
|
143
|
+
dataRedis: `${Application.redis.host}:${Application.redis.port}`,
|
|
144
|
+
logsRedis: `${Application.redisLogs.host}:${Application.redisLogs.port}`
|
|
145
|
+
}
|
|
146
|
+
})
|
|
147
|
+
} else {
|
|
148
|
+
Logger.log({
|
|
149
|
+
level: 'info',
|
|
150
|
+
message: 'Using shared Redis instance for data and logs',
|
|
151
|
+
data: { redis: `${Application.redis.host}:${Application.redis.port}` }
|
|
152
|
+
})
|
|
153
|
+
}
|
|
154
|
+
global.logsConsumer = new BlackboxLogsConsumer(redisConfigs)
|
|
155
|
+
|
|
156
|
+
// Start Docker console log capture — all containers on goki-network
|
|
157
|
+
global.dockerLogsConsumer = new DockerLogsConsumer({
|
|
158
|
+
enabled: Application.logging?.dockerCapture !== false,
|
|
159
|
+
networkName: Application.logging?.dockerNetwork || 'goki-network',
|
|
160
|
+
...(Application.logging?.dockerExcludedContainers && {
|
|
161
|
+
excludedContainers: Application.logging.dockerExcludedContainers
|
|
162
|
+
})
|
|
163
|
+
})
|
|
164
|
+
await global.dockerLogsConsumer.start()
|
|
165
|
+
|
|
166
|
+
// Start message history cleanup job
|
|
167
|
+
if (Application.messageHistory.enabled) {
|
|
168
|
+
startCleanupJob()
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Start database maintenance job (table trimming, WAL checkpoint, ANALYZE)
|
|
172
|
+
startMaintenanceJob()
|
|
173
|
+
|
|
174
|
+
// Auto-start App Gateway (non-blocking)
|
|
175
|
+
AppGatewayService.autoStart().catch(error => {
|
|
176
|
+
Logger.log({
|
|
177
|
+
level: 'error',
|
|
178
|
+
message: 'Gateway auto-start error',
|
|
179
|
+
data: { error: error.message }
|
|
180
|
+
})
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
// Initialize Cloud Functions service
|
|
184
|
+
await FunctionsService.initialize()
|
|
185
|
+
await FunctionTriggerDispatcher.start()
|
|
186
|
+
|
|
187
|
+
return server
|
|
188
|
+
} catch (error) {
|
|
189
|
+
Logger.log({
|
|
190
|
+
level: 'error',
|
|
191
|
+
message: 'Failed to start server',
|
|
192
|
+
data: { error: error.message, stack: error.stack }
|
|
193
|
+
})
|
|
194
|
+
process.exit(1)
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const shutdown = async () => {
|
|
199
|
+
Logger.log({
|
|
200
|
+
level: 'info',
|
|
201
|
+
message: 'Shutting down server'
|
|
202
|
+
})
|
|
203
|
+
await AppGatewayService.stop()
|
|
204
|
+
FunctionTriggerDispatcher.stop()
|
|
205
|
+
await FunctionsService.shutdown()
|
|
206
|
+
MqttServer.stop()
|
|
207
|
+
if (global.logsConsumer) {
|
|
208
|
+
await global.logsConsumer.close()
|
|
209
|
+
}
|
|
210
|
+
if (global.dockerLogsConsumer) {
|
|
211
|
+
await global.dockerLogsConsumer.stop()
|
|
212
|
+
}
|
|
213
|
+
await PostgresClient.shutdown()
|
|
214
|
+
await RedisClient.shutdown()
|
|
215
|
+
SqliteStore.shutdown()
|
|
216
|
+
process.exit(0)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
start()
|
|
220
|
+
|
|
221
|
+
process.on('SIGINT', shutdown)
|
|
222
|
+
process.on('SIGTERM', shutdown)
|
|
223
|
+
|
|
224
|
+
process.on('unhandledRejection', error => {
|
|
225
|
+
Logger.log({
|
|
226
|
+
level: 'error',
|
|
227
|
+
message: 'Unhandled rejection detected',
|
|
228
|
+
data: { error: error.message, stack: error.stack }
|
|
229
|
+
})
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
process.on('uncaughtException', error => {
|
|
233
|
+
Logger.log({
|
|
234
|
+
level: 'error',
|
|
235
|
+
message: 'Uncaught exception detected',
|
|
236
|
+
data: { error: error.message, stack: error.stack }
|
|
237
|
+
})
|
|
238
|
+
})
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { SqliteStore } from '../../singletons/SqliteStore.js'
|
|
2
|
+
import {
|
|
3
|
+
PUBSUB_TOPICS,
|
|
4
|
+
PUBSUB_SUBSCRIPTIONS,
|
|
5
|
+
PUBSUB_MESSAGES,
|
|
6
|
+
LOGGING_ENTRIES,
|
|
7
|
+
MQTT_CLIENTS,
|
|
8
|
+
MQTT_MESSAGES
|
|
9
|
+
} from '../../db/Tables.js'
|
|
10
|
+
import { Logic as FirestoreLogic } from '../firestore/Logic.js'
|
|
11
|
+
|
|
12
|
+
export const Logic = {
|
|
13
|
+
async getStats (params) {
|
|
14
|
+
const { traceId } = params
|
|
15
|
+
try {
|
|
16
|
+
const topics = SqliteStore.list(PUBSUB_TOPICS)
|
|
17
|
+
const subscriptions = SqliteStore.list(PUBSUB_SUBSCRIPTIONS)
|
|
18
|
+
const messages = SqliteStore.list(PUBSUB_MESSAGES)
|
|
19
|
+
const logEntries = SqliteStore.list(LOGGING_ENTRIES)
|
|
20
|
+
const loggingByLevel = {}
|
|
21
|
+
logEntries.data.forEach(entry => {
|
|
22
|
+
const level = entry.severity || 'UNKNOWN'
|
|
23
|
+
loggingByLevel[level] = (loggingByLevel[level] || 0) + 1
|
|
24
|
+
})
|
|
25
|
+
const mqttClients = SqliteStore.list(MQTT_CLIENTS)
|
|
26
|
+
const mqttMessages = SqliteStore.list(MQTT_MESSAGES)
|
|
27
|
+
let firestoreStats = { collections: 0, documents: 0 }
|
|
28
|
+
try {
|
|
29
|
+
const collectionsResult = await FirestoreLogic.listCollections({ traceId })
|
|
30
|
+
if (collectionsResult.status !== 'error' && collectionsResult.collections) {
|
|
31
|
+
const collectionDocCounts = await Promise.all(
|
|
32
|
+
collectionsResult.collections.map(async (collectionPath) => {
|
|
33
|
+
const docsResult = await FirestoreLogic.listDocuments({ collectionPath, page: { limit: 1000 }, traceId })
|
|
34
|
+
return docsResult.status !== 'error' ? docsResult.total : 0
|
|
35
|
+
})
|
|
36
|
+
)
|
|
37
|
+
const totalDocuments = collectionDocCounts.reduce((sum, count) => sum + count, 0)
|
|
38
|
+
firestoreStats = {
|
|
39
|
+
collections: collectionsResult.total,
|
|
40
|
+
documents: totalDocuments
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
} catch (firestoreError) {
|
|
44
|
+
firestoreStats = { collections: 0, documents: 0 }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const stats = {
|
|
48
|
+
pubsub: {
|
|
49
|
+
topics: topics.total,
|
|
50
|
+
subscriptions: subscriptions.total,
|
|
51
|
+
messages: messages.total
|
|
52
|
+
},
|
|
53
|
+
logging: {
|
|
54
|
+
entries: logEntries.total,
|
|
55
|
+
byLevel: loggingByLevel
|
|
56
|
+
},
|
|
57
|
+
mqtt: {
|
|
58
|
+
clients: mqttClients.total,
|
|
59
|
+
messages: mqttMessages.total
|
|
60
|
+
},
|
|
61
|
+
firestore: firestoreStats
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
status: 'ok',
|
|
65
|
+
data: stats,
|
|
66
|
+
traceId
|
|
67
|
+
}
|
|
68
|
+
} catch (error) {
|
|
69
|
+
return {
|
|
70
|
+
status: 'error',
|
|
71
|
+
message: error.message,
|
|
72
|
+
traceId
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import KoaRouter from 'koa-router'
|
|
2
|
+
import { Controllers } from './Controllers.js'
|
|
3
|
+
|
|
4
|
+
const Router = new KoaRouter()
|
|
5
|
+
const v1 = new KoaRouter({ prefix: '/v1/dashboard' })
|
|
6
|
+
|
|
7
|
+
v1.post('/stats', Controllers.getStats)
|
|
8
|
+
|
|
9
|
+
Router.use(v1.routes())
|
|
10
|
+
|
|
11
|
+
export { Router }
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Joi } from '@gokiteam/koa'
|
|
2
|
+
|
|
3
|
+
export const Schemas = {
|
|
4
|
+
stats: {
|
|
5
|
+
request: {
|
|
6
|
+
body: {}
|
|
7
|
+
},
|
|
8
|
+
responses: {
|
|
9
|
+
success: {
|
|
10
|
+
stats: Joi.object({
|
|
11
|
+
topics: Joi.number().integer().min(0).required(),
|
|
12
|
+
subscriptions: Joi.number().integer().min(0).required(),
|
|
13
|
+
messages: Joi.number().integer().min(0).required(),
|
|
14
|
+
logEntries: Joi.number().integer().min(0).required(),
|
|
15
|
+
mqttClients: Joi.number().integer().min(0).required(),
|
|
16
|
+
mqttMessages: Joi.number().integer().min(0).required()
|
|
17
|
+
}).required()
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
recentActivity: {
|
|
22
|
+
request: {
|
|
23
|
+
body: {
|
|
24
|
+
limit: Joi.number().integer().min(1).max(100).default(20)
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
responses: {
|
|
28
|
+
success: {
|
|
29
|
+
activities: Joi.array().items(
|
|
30
|
+
Joi.object({
|
|
31
|
+
type: Joi.string().valid(
|
|
32
|
+
'pubsub.message',
|
|
33
|
+
'pubsub.topic.created',
|
|
34
|
+
'pubsub.subscription.created',
|
|
35
|
+
'mqtt.publish',
|
|
36
|
+
'mqtt.connect',
|
|
37
|
+
'mqtt.disconnect',
|
|
38
|
+
'logging.entry'
|
|
39
|
+
).required(),
|
|
40
|
+
data: Joi.object().required(),
|
|
41
|
+
timestamp: Joi.string().isoDate().required()
|
|
42
|
+
})
|
|
43
|
+
).required()
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Logic } from './Logic.js'
|
|
2
|
+
|
|
3
|
+
export const Controllers = {
|
|
4
|
+
async export (ctx) {
|
|
5
|
+
const { traceId } = ctx.state
|
|
6
|
+
const result = await Logic.export({ traceId })
|
|
7
|
+
ctx.reply(result)
|
|
8
|
+
},
|
|
9
|
+
async import (ctx) {
|
|
10
|
+
const { traceId } = ctx.state
|
|
11
|
+
const { data } = ctx.request.body
|
|
12
|
+
const result = await Logic.import({ data, traceId })
|
|
13
|
+
ctx.reply(result)
|
|
14
|
+
},
|
|
15
|
+
async clear (ctx) {
|
|
16
|
+
const { traceId } = ctx.state
|
|
17
|
+
const result = await Logic.clear({ traceId })
|
|
18
|
+
ctx.reply(result)
|
|
19
|
+
},
|
|
20
|
+
async clearServices (ctx) {
|
|
21
|
+
const { traceId } = ctx.state
|
|
22
|
+
const { services, keepSystemData } = ctx.request.body
|
|
23
|
+
const result = await Logic.clearServices({ services, keepSystemData, traceId })
|
|
24
|
+
ctx.reply(result)
|
|
25
|
+
}
|
|
26
|
+
}
|