@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,184 @@
|
|
|
1
|
+
import Redis from 'ioredis'
|
|
2
|
+
import { Application } from '../configs/Application.js'
|
|
3
|
+
|
|
4
|
+
class RedisClientClass {
|
|
5
|
+
constructor () {
|
|
6
|
+
this.client = null
|
|
7
|
+
this.isInitialized = false
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
initialize () {
|
|
11
|
+
if (this.isInitialized) return
|
|
12
|
+
const config = Application.redis
|
|
13
|
+
this.client = new Redis({
|
|
14
|
+
host: config.host,
|
|
15
|
+
port: config.port,
|
|
16
|
+
lazyConnect: true,
|
|
17
|
+
retryStrategy: (times) => {
|
|
18
|
+
if (times > 3) return null
|
|
19
|
+
return Math.min(times * 100, 3000)
|
|
20
|
+
}
|
|
21
|
+
})
|
|
22
|
+
this.client.on('error', (err) => {
|
|
23
|
+
console.error('Redis client error:', err.message)
|
|
24
|
+
})
|
|
25
|
+
this.isInitialized = true
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async connect () {
|
|
29
|
+
if (!this.isInitialized) {
|
|
30
|
+
this.initialize()
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
await this.client.connect()
|
|
34
|
+
} catch (error) {
|
|
35
|
+
// Already connected or connection failed
|
|
36
|
+
if (!error.message.includes('already connected')) {
|
|
37
|
+
throw error
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async testConnection () {
|
|
43
|
+
try {
|
|
44
|
+
if (!this.isInitialized) {
|
|
45
|
+
this.initialize()
|
|
46
|
+
}
|
|
47
|
+
const pong = await this.client.ping()
|
|
48
|
+
return { connected: pong === 'PONG' }
|
|
49
|
+
} catch (error) {
|
|
50
|
+
return { connected: false, error: error.message }
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async getInfo () {
|
|
55
|
+
const info = await this.client.info()
|
|
56
|
+
const parsed = {}
|
|
57
|
+
info.split('\n').forEach(line => {
|
|
58
|
+
if (line && !line.startsWith('#')) {
|
|
59
|
+
const [key, value] = line.split(':')
|
|
60
|
+
if (key && value) {
|
|
61
|
+
parsed[key.trim()] = value.trim()
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
})
|
|
65
|
+
return parsed
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async getDbSize () {
|
|
69
|
+
return await this.client.dbsize()
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async scanKeys (pattern = '*', cursor = '0', count = 100) {
|
|
73
|
+
const [nextCursor, keys] = await this.client.scan(cursor, 'MATCH', pattern, 'COUNT', count)
|
|
74
|
+
return {
|
|
75
|
+
cursor: nextCursor,
|
|
76
|
+
keys,
|
|
77
|
+
done: nextCursor === '0'
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async getKeyType (key) {
|
|
82
|
+
return await this.client.type(key)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async getKeyTtl (key) {
|
|
86
|
+
return await this.client.ttl(key)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async getKeyValue (key) {
|
|
90
|
+
const type = await this.getKeyType(key)
|
|
91
|
+
let value
|
|
92
|
+
switch (type) {
|
|
93
|
+
case 'string':
|
|
94
|
+
value = await this.client.get(key)
|
|
95
|
+
break
|
|
96
|
+
case 'list':
|
|
97
|
+
value = await this.client.lrange(key, 0, -1)
|
|
98
|
+
break
|
|
99
|
+
case 'set':
|
|
100
|
+
value = await this.client.smembers(key)
|
|
101
|
+
break
|
|
102
|
+
case 'zset':
|
|
103
|
+
value = await this.client.zrange(key, 0, -1, 'WITHSCORES')
|
|
104
|
+
// Convert flat array to array of [member, score] pairs
|
|
105
|
+
const pairs = []
|
|
106
|
+
for (let i = 0; i < value.length; i += 2) {
|
|
107
|
+
pairs.push({ member: value[i], score: parseFloat(value[i + 1]) })
|
|
108
|
+
}
|
|
109
|
+
value = pairs
|
|
110
|
+
break
|
|
111
|
+
case 'hash':
|
|
112
|
+
value = await this.client.hgetall(key)
|
|
113
|
+
break
|
|
114
|
+
case 'stream':
|
|
115
|
+
value = await this.client.xrange(key, '-', '+', 'COUNT', 100)
|
|
116
|
+
break
|
|
117
|
+
default:
|
|
118
|
+
value = null
|
|
119
|
+
}
|
|
120
|
+
const ttl = await this.getKeyTtl(key)
|
|
121
|
+
const memoryUsage = await this.getKeyMemory(key)
|
|
122
|
+
return {
|
|
123
|
+
key,
|
|
124
|
+
type,
|
|
125
|
+
value,
|
|
126
|
+
ttl,
|
|
127
|
+
memoryUsage
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async getKeyMemory (key) {
|
|
132
|
+
try {
|
|
133
|
+
return await this.client.memory('USAGE', key)
|
|
134
|
+
} catch (error) {
|
|
135
|
+
return null
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async getAllKeys (pattern = '*', maxKeys = 1000) {
|
|
140
|
+
const keys = []
|
|
141
|
+
let cursor = '0'
|
|
142
|
+
do {
|
|
143
|
+
const result = await this.scanKeys(pattern, cursor, 100)
|
|
144
|
+
keys.push(...result.keys)
|
|
145
|
+
cursor = result.cursor
|
|
146
|
+
if (keys.length >= maxKeys) break
|
|
147
|
+
} while (cursor !== '0')
|
|
148
|
+
return keys.slice(0, maxKeys)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async deleteKey (key) {
|
|
152
|
+
if (!this.isInitialized) {
|
|
153
|
+
this.initialize()
|
|
154
|
+
}
|
|
155
|
+
return await this.client.del(key)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async setKey (key, value, ttl = null) {
|
|
159
|
+
if (!this.isInitialized) {
|
|
160
|
+
this.initialize()
|
|
161
|
+
}
|
|
162
|
+
if (ttl && ttl > 0) {
|
|
163
|
+
return await this.client.set(key, value, 'EX', ttl)
|
|
164
|
+
}
|
|
165
|
+
return await this.client.set(key, value)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async deleteAll () {
|
|
169
|
+
if (!this.isInitialized) {
|
|
170
|
+
this.initialize()
|
|
171
|
+
}
|
|
172
|
+
return await this.client.flushall()
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async shutdown () {
|
|
176
|
+
if (this.client) {
|
|
177
|
+
await this.client.quit()
|
|
178
|
+
this.client = null
|
|
179
|
+
this.isInitialized = false
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export const RedisClient = new RedisClientClass()
|
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import Database from 'better-sqlite3'
|
|
4
|
+
import { Application } from '../configs/Application.js'
|
|
5
|
+
import { PUBSUB_SCHEMAS } from '../db/schemas/pubsub.js'
|
|
6
|
+
import { LOGGING_SCHEMAS } from '../db/schemas/logging.js'
|
|
7
|
+
import { MQTT_SCHEMAS } from '../db/schemas/mqtt.js'
|
|
8
|
+
import { FIRESTORE_SCHEMAS } from '../db/schemas/firestore.js'
|
|
9
|
+
import { WEBHOOK_SCHEMAS } from '../db/schemas/webhooks.js'
|
|
10
|
+
import { HTTP_TRAFFIC_SCHEMAS } from '../db/schemas/httpTraffic.js'
|
|
11
|
+
import { PUBSUB_TOPIC_REGISTRY_SCHEMAS } from '../db/schemas/pubsubRegistry.js'
|
|
12
|
+
import { FUNCTIONS_SCHEMAS } from '../db/schemas/functions.js'
|
|
13
|
+
import { MIGRATIONS_SCHEMA, CURRENT_SCHEMA_VERSION } from '../db/schemas/migrations.js'
|
|
14
|
+
|
|
15
|
+
class SqliteStoreClass {
|
|
16
|
+
constructor () {
|
|
17
|
+
this.db = null
|
|
18
|
+
this.isInitialized = false
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
initialize (dbPath = null) {
|
|
22
|
+
if (this.isInitialized) return
|
|
23
|
+
|
|
24
|
+
const dataDir = Application.storage.dataDir || './data'
|
|
25
|
+
if (!fs.existsSync(dataDir)) {
|
|
26
|
+
fs.mkdirSync(dataDir, { recursive: true })
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const finalDbPath = dbPath || path.join(dataDir, 'dev-tools.db')
|
|
30
|
+
this.db = new Database(finalDbPath)
|
|
31
|
+
this.db.pragma('journal_mode = WAL')
|
|
32
|
+
this.db.pragma('synchronous = NORMAL')
|
|
33
|
+
this.db.pragma('busy_timeout = 5000')
|
|
34
|
+
this.db.pragma('wal_autocheckpoint = 1000')
|
|
35
|
+
this.db.pragma('cache_size = -64000')
|
|
36
|
+
this.db.pragma('temp_store = MEMORY')
|
|
37
|
+
this.db.pragma('foreign_keys = ON')
|
|
38
|
+
|
|
39
|
+
this.createTables()
|
|
40
|
+
this.isInitialized = true
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
createTables () {
|
|
44
|
+
this.db.exec(MIGRATIONS_SCHEMA)
|
|
45
|
+
|
|
46
|
+
const schemas = [
|
|
47
|
+
...PUBSUB_SCHEMAS,
|
|
48
|
+
...LOGGING_SCHEMAS,
|
|
49
|
+
...MQTT_SCHEMAS,
|
|
50
|
+
...FIRESTORE_SCHEMAS,
|
|
51
|
+
...WEBHOOK_SCHEMAS,
|
|
52
|
+
...HTTP_TRAFFIC_SCHEMAS,
|
|
53
|
+
...PUBSUB_TOPIC_REGISTRY_SCHEMAS,
|
|
54
|
+
...FUNCTIONS_SCHEMAS
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
const currentVersion = this.getSchemaVersion()
|
|
58
|
+
if (currentVersion < CURRENT_SCHEMA_VERSION) {
|
|
59
|
+
this.db.transaction(() => {
|
|
60
|
+
// Drop and recreate logging tables for clean schema
|
|
61
|
+
this.db.exec('DROP TABLE IF EXISTS logging_entries')
|
|
62
|
+
this.db.exec('DROP TABLE IF EXISTS logging_logs')
|
|
63
|
+
for (const schema of schemas) {
|
|
64
|
+
this.db.exec(schema)
|
|
65
|
+
}
|
|
66
|
+
this.db.prepare('INSERT OR REPLACE INTO schema_migrations (version, name) VALUES (?, ?)').run(
|
|
67
|
+
CURRENT_SCHEMA_VERSION,
|
|
68
|
+
'db_maintenance_indexes'
|
|
69
|
+
)
|
|
70
|
+
})()
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
getSchemaVersion () {
|
|
75
|
+
try {
|
|
76
|
+
const result = this.db.prepare('SELECT MAX(version) as version FROM schema_migrations').get()
|
|
77
|
+
return result?.version || 0
|
|
78
|
+
} catch (error) {
|
|
79
|
+
return 0
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
registerCollection (collectionName, filePath) {
|
|
84
|
+
// No-op for compatibility - tables created at initialization
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
get (tableName, id, idField = 'id') {
|
|
88
|
+
try {
|
|
89
|
+
const snakeIdField = idField.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`)
|
|
90
|
+
const stmt = this.db.prepare(`SELECT * FROM ${tableName} WHERE ${snakeIdField} = ?`)
|
|
91
|
+
const result = stmt.get(id)
|
|
92
|
+
if (!result) return undefined
|
|
93
|
+
const camelResult = this.toCamelCase(result)
|
|
94
|
+
return this.parseJsonFields(tableName, camelResult)
|
|
95
|
+
} catch (error) {
|
|
96
|
+
throw new Error(`Failed to get ${id} from ${tableName}: ${error.message}`)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Check if a row exists matching a SQL WHERE clause
|
|
102
|
+
* @param {string} tableName - Table to query
|
|
103
|
+
* @param {string} whereClause - SQL WHERE conditions (without WHERE keyword)
|
|
104
|
+
* @param {Array} params - Bind parameters
|
|
105
|
+
* @returns {boolean}
|
|
106
|
+
*/
|
|
107
|
+
exists (tableName, whereClause, params = []) {
|
|
108
|
+
try {
|
|
109
|
+
const stmt = this.db.prepare(`SELECT 1 FROM ${tableName} WHERE ${whereClause} LIMIT 1`)
|
|
110
|
+
return !!stmt.get(...params)
|
|
111
|
+
} catch (error) {
|
|
112
|
+
throw new Error(`Failed to check existence in ${tableName}: ${error.message}`)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
find (tableName, predicate) {
|
|
117
|
+
try {
|
|
118
|
+
if (typeof predicate === 'function') {
|
|
119
|
+
const all = this.db.prepare(`SELECT * FROM ${tableName}`).all()
|
|
120
|
+
return all.map(row => this.toCamelCase(row)).map(row => this.parseJsonFields(tableName, row)).filter(predicate)
|
|
121
|
+
} else if (typeof predicate === 'object') {
|
|
122
|
+
const { sql, params } = this.buildWhereClause(predicate)
|
|
123
|
+
const stmt = this.db.prepare(`SELECT * FROM ${tableName} ${sql}`)
|
|
124
|
+
const results = stmt.all(...params)
|
|
125
|
+
return results.map(row => this.toCamelCase(row)).map(row => this.parseJsonFields(tableName, row))
|
|
126
|
+
}
|
|
127
|
+
return []
|
|
128
|
+
} catch (error) {
|
|
129
|
+
throw new Error(`Failed to find in ${tableName}: ${error.message}`)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
list (tableName, options = {}) {
|
|
134
|
+
try {
|
|
135
|
+
let sql = `SELECT * FROM ${tableName}`
|
|
136
|
+
const params = []
|
|
137
|
+
|
|
138
|
+
if (options.filter && typeof options.filter === 'object') {
|
|
139
|
+
const where = this.buildWhereClause(options.filter)
|
|
140
|
+
sql += ` ${where.sql}`
|
|
141
|
+
params.push(...where.params)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (options.where) {
|
|
145
|
+
const where = this.buildWhereClause(options.where)
|
|
146
|
+
sql += ` ${where.sql}`
|
|
147
|
+
params.push(...where.params)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const countSql = `SELECT COUNT(*) as count FROM ${tableName}` +
|
|
151
|
+
(params.length > 0 ? ` ${this.buildWhereClause(options.where || options.filter).sql}` : '')
|
|
152
|
+
const totalResult = this.db.prepare(countSql).get(...params)
|
|
153
|
+
const total = totalResult.count
|
|
154
|
+
|
|
155
|
+
if (options.orderBy) {
|
|
156
|
+
sql += ` ORDER BY ${options.orderBy}`
|
|
157
|
+
} else if (options.sort) {
|
|
158
|
+
sql += ` ORDER BY ${options.sort}`
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (options.limit !== undefined) {
|
|
162
|
+
sql += ' LIMIT ?'
|
|
163
|
+
params.push(options.limit)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (options.offset !== undefined) {
|
|
167
|
+
sql += ' OFFSET ?'
|
|
168
|
+
params.push(options.offset)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const stmt = this.db.prepare(sql)
|
|
172
|
+
let data = stmt.all(...params)
|
|
173
|
+
|
|
174
|
+
data = data.map(row => this.toCamelCase(row)).map(row => this.parseJsonFields(tableName, row))
|
|
175
|
+
|
|
176
|
+
if (options.filter && typeof options.filter === 'function') {
|
|
177
|
+
data = data.filter(options.filter)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (options.sort && typeof options.sort === 'function') {
|
|
181
|
+
data = data.sort(options.sort)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return { data, total }
|
|
185
|
+
} catch (error) {
|
|
186
|
+
throw new Error(`Failed to list ${tableName}: ${error.message}`)
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
create (tableName, item) {
|
|
191
|
+
try {
|
|
192
|
+
const preparedItem = this.stringifyJsonFields(tableName, item)
|
|
193
|
+
const snakeCaseItem = this.toSnakeCase(preparedItem)
|
|
194
|
+
const fields = Object.keys(snakeCaseItem).filter(f => snakeCaseItem[f] !== undefined)
|
|
195
|
+
const placeholders = fields.map(() => '?').join(', ')
|
|
196
|
+
const sql = `INSERT INTO ${tableName} (${fields.join(', ')}) VALUES (${placeholders})`
|
|
197
|
+
const stmt = this.db.prepare(sql)
|
|
198
|
+
const values = fields.map(f => {
|
|
199
|
+
const value = snakeCaseItem[f]
|
|
200
|
+
// Convert boolean to integer for SQLite
|
|
201
|
+
if (typeof value === 'boolean') return value ? 1 : 0
|
|
202
|
+
return value
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
stmt.run(...values)
|
|
206
|
+
return item
|
|
207
|
+
} catch (error) {
|
|
208
|
+
throw new Error(`Failed to create in ${tableName}: ${error.message}`)
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
update (tableName, id, updates, idField = 'id') {
|
|
213
|
+
try {
|
|
214
|
+
const preparedUpdates = this.stringifyJsonFields(tableName, updates)
|
|
215
|
+
const snakeCaseUpdates = this.toSnakeCase(preparedUpdates)
|
|
216
|
+
const snakeIdField = idField.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`)
|
|
217
|
+
const fields = Object.keys(snakeCaseUpdates)
|
|
218
|
+
const setClause = fields.map(f => `${f} = ?`).join(', ')
|
|
219
|
+
const sql = `UPDATE ${tableName} SET ${setClause} WHERE ${snakeIdField} = ?`
|
|
220
|
+
const stmt = this.db.prepare(sql)
|
|
221
|
+
const values = fields.map(f => {
|
|
222
|
+
const value = snakeCaseUpdates[f]
|
|
223
|
+
// Convert boolean to integer for SQLite
|
|
224
|
+
if (typeof value === 'boolean') return value ? 1 : 0
|
|
225
|
+
return value
|
|
226
|
+
})
|
|
227
|
+
const params = [...values, id]
|
|
228
|
+
const result = stmt.run(...params)
|
|
229
|
+
|
|
230
|
+
if (result.changes === 0) {
|
|
231
|
+
throw new Error(`Item with ${idField}=${id} not found in ${tableName}`)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const updated = this.get(tableName, id, idField)
|
|
235
|
+
return updated
|
|
236
|
+
} catch (error) {
|
|
237
|
+
throw new Error(`Failed to update ${tableName}: ${error.message}`)
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
delete (tableName, id, idField = 'id') {
|
|
242
|
+
try {
|
|
243
|
+
const item = this.get(tableName, id, idField)
|
|
244
|
+
if (!item) {
|
|
245
|
+
throw new Error(`Item with ${idField}=${id} not found in ${tableName}`)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const snakeIdField = idField.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`)
|
|
249
|
+
const stmt = this.db.prepare(`DELETE FROM ${tableName} WHERE ${snakeIdField} = ?`)
|
|
250
|
+
stmt.run(id)
|
|
251
|
+
return item
|
|
252
|
+
} catch (error) {
|
|
253
|
+
throw new Error(`Failed to delete from ${tableName}: ${error.message}`)
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
clear (tableName) {
|
|
258
|
+
try {
|
|
259
|
+
this.db.prepare(`DELETE FROM ${tableName}`).run()
|
|
260
|
+
} catch (error) {
|
|
261
|
+
throw new Error(`Failed to clear ${tableName}: ${error.message}`)
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
clearAll () {
|
|
266
|
+
const tables = [
|
|
267
|
+
'pubsub_messages',
|
|
268
|
+
'pubsub_subscriptions',
|
|
269
|
+
'pubsub_topics',
|
|
270
|
+
'logging_entries',
|
|
271
|
+
'logging_logs',
|
|
272
|
+
'mqtt_subscriptions',
|
|
273
|
+
'mqtt_messages',
|
|
274
|
+
'mqtt_clients',
|
|
275
|
+
'firestore_metadata',
|
|
276
|
+
'webhook_routes',
|
|
277
|
+
'webhook_settings',
|
|
278
|
+
'http_traffic',
|
|
279
|
+
'pubsub_topic_registry',
|
|
280
|
+
'cloud_function_invocations',
|
|
281
|
+
'cloud_functions'
|
|
282
|
+
]
|
|
283
|
+
|
|
284
|
+
this.db.transaction(() => {
|
|
285
|
+
for (const table of tables) {
|
|
286
|
+
try {
|
|
287
|
+
this.clear(table)
|
|
288
|
+
} catch (error) {
|
|
289
|
+
// Ignore if table doesn't exist
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
})()
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
transaction (fn) {
|
|
296
|
+
return this.db.transaction(fn)()
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
buildWhereClause (filter) {
|
|
300
|
+
const keys = Object.keys(filter)
|
|
301
|
+
if (keys.length === 0) {
|
|
302
|
+
return { sql: '', params: [] }
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const snakeCaseFilter = this.toSnakeCase(filter)
|
|
306
|
+
const snakeKeys = Object.keys(snakeCaseFilter)
|
|
307
|
+
|
|
308
|
+
const conditions = snakeKeys.map(key => {
|
|
309
|
+
if (snakeCaseFilter[key] === null) {
|
|
310
|
+
return `${key} IS NULL`
|
|
311
|
+
}
|
|
312
|
+
return `${key} = ?`
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
const params = snakeKeys.map(key => snakeCaseFilter[key]).filter(v => v !== null)
|
|
316
|
+
return {
|
|
317
|
+
sql: `WHERE ${conditions.join(' AND ')}`,
|
|
318
|
+
params
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
parseJsonFields (tableName, row) {
|
|
323
|
+
const jsonFields = this.getJsonFields(tableName)
|
|
324
|
+
const parsed = { ...row }
|
|
325
|
+
|
|
326
|
+
for (const field of jsonFields) {
|
|
327
|
+
if (parsed[field] && typeof parsed[field] === 'string') {
|
|
328
|
+
try {
|
|
329
|
+
parsed[field] = JSON.parse(parsed[field])
|
|
330
|
+
} catch (error) {
|
|
331
|
+
// Keep as string if not valid JSON
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return parsed
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
stringifyJsonFields (tableName, item) {
|
|
340
|
+
const stringified = { ...item }
|
|
341
|
+
|
|
342
|
+
// Stringify ALL object fields (except null)
|
|
343
|
+
for (const field in stringified) {
|
|
344
|
+
const value = stringified[field]
|
|
345
|
+
if (value !== null && typeof value === 'object' && !Buffer.isBuffer(value)) {
|
|
346
|
+
stringified[field] = JSON.stringify(value)
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return stringified
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
getJsonFields (tableName) {
|
|
354
|
+
const jsonFieldsMap = {
|
|
355
|
+
pubsub_topics: ['labels', 'messageStoragePolicy', 'schemaSettings'],
|
|
356
|
+
pubsub_subscriptions: ['pushConfig', 'labels', 'expirationPolicy', 'deadLetterPolicy', 'retryPolicy'],
|
|
357
|
+
pubsub_messages: ['attributes'],
|
|
358
|
+
pubsub_message_history: ['attributes'],
|
|
359
|
+
logging_entries: ['resource', 'labels', 'jsonPayload', 'protoPayload', 'httpRequest', 'operation', 'sourceLocation'],
|
|
360
|
+
mqtt_clients: ['will'],
|
|
361
|
+
mqtt_messages: [],
|
|
362
|
+
mqtt_subscriptions: [],
|
|
363
|
+
logging_logs: [],
|
|
364
|
+
firestore_metadata: [],
|
|
365
|
+
webhook_routes: [],
|
|
366
|
+
webhook_settings: [],
|
|
367
|
+
http_traffic: ['queryParams', 'requestHeaders', 'responseHeaders', 'requestCookies', 'mockResponse'],
|
|
368
|
+
pubsub_topic_registry: [],
|
|
369
|
+
cloud_functions: ['triggerConfig', 'environmentVariables'],
|
|
370
|
+
cloud_function_invocations: ['cloudEvent']
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return jsonFieldsMap[tableName] || []
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
toSnakeCase (obj) {
|
|
377
|
+
const result = {}
|
|
378
|
+
for (const key in obj) {
|
|
379
|
+
const snakeKey = key.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`)
|
|
380
|
+
result[snakeKey] = obj[key]
|
|
381
|
+
}
|
|
382
|
+
return result
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
toCamelCase (obj) {
|
|
386
|
+
const result = {}
|
|
387
|
+
for (const key in obj) {
|
|
388
|
+
const camelKey = key.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase())
|
|
389
|
+
result[camelKey] = obj[key]
|
|
390
|
+
}
|
|
391
|
+
return result
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
exportToJson (outputPath = null) {
|
|
395
|
+
const tables = [
|
|
396
|
+
'pubsub_topics',
|
|
397
|
+
'pubsub_subscriptions',
|
|
398
|
+
'pubsub_messages',
|
|
399
|
+
'logging_entries',
|
|
400
|
+
'logging_logs',
|
|
401
|
+
'mqtt_clients',
|
|
402
|
+
'mqtt_messages',
|
|
403
|
+
'mqtt_subscriptions',
|
|
404
|
+
'firestore_metadata',
|
|
405
|
+
'pubsub_topic_registry'
|
|
406
|
+
]
|
|
407
|
+
|
|
408
|
+
const exportData = {}
|
|
409
|
+
|
|
410
|
+
for (const table of tables) {
|
|
411
|
+
try {
|
|
412
|
+
const stmt = this.db.prepare(`SELECT * FROM ${table}`)
|
|
413
|
+
const rows = stmt.all()
|
|
414
|
+
exportData[table] = rows.map(row => this.parseJsonFields(table, row))
|
|
415
|
+
} catch (error) {
|
|
416
|
+
exportData[table] = []
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const finalPath = outputPath || path.join(Application.storage.dataDir, 'dev-tools-export.json')
|
|
421
|
+
fs.writeFileSync(finalPath, JSON.stringify(exportData, null, 2))
|
|
422
|
+
|
|
423
|
+
return {
|
|
424
|
+
success: true,
|
|
425
|
+
path: finalPath,
|
|
426
|
+
tables: Object.keys(exportData).length,
|
|
427
|
+
records: Object.values(exportData).reduce((sum, arr) => sum + arr.length, 0)
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
getStats () {
|
|
432
|
+
try {
|
|
433
|
+
const tables = this.db.prepare(`
|
|
434
|
+
SELECT name FROM sqlite_master
|
|
435
|
+
WHERE type='table' AND name NOT LIKE 'sqlite_%' AND name != 'schema_migrations'
|
|
436
|
+
ORDER BY name
|
|
437
|
+
`).all()
|
|
438
|
+
|
|
439
|
+
const stats = {}
|
|
440
|
+
|
|
441
|
+
for (const { name } of tables) {
|
|
442
|
+
const countResult = this.db.prepare(`SELECT COUNT(*) as count FROM ${name}`).get()
|
|
443
|
+
stats[name] = countResult.count
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const dbPath = this.db.name
|
|
447
|
+
let fileSize = 0
|
|
448
|
+
let fileSizeMb = '0.00'
|
|
449
|
+
|
|
450
|
+
if (dbPath !== ':memory:' && fs.existsSync(dbPath)) {
|
|
451
|
+
fileSize = fs.statSync(dbPath).size
|
|
452
|
+
fileSizeMb = (fileSize / 1024 / 1024).toFixed(2)
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
return {
|
|
456
|
+
file_size: fileSize,
|
|
457
|
+
file_size_mb: fileSizeMb,
|
|
458
|
+
tables: stats,
|
|
459
|
+
total_records: Object.values(stats).reduce((sum, count) => sum + count, 0)
|
|
460
|
+
}
|
|
461
|
+
} catch (error) {
|
|
462
|
+
throw new Error(`Failed to get stats: ${error.message}`)
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
shutdown () {
|
|
467
|
+
if (this.db) {
|
|
468
|
+
try {
|
|
469
|
+
this.db.pragma('wal_checkpoint(TRUNCATE)')
|
|
470
|
+
} catch (error) {
|
|
471
|
+
// Best-effort checkpoint before close
|
|
472
|
+
}
|
|
473
|
+
this.db.close()
|
|
474
|
+
this.db = null
|
|
475
|
+
this.isInitialized = false
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
export const SqliteStore = new SqliteStoreClass()
|