@gokiteam/goki-dev 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (205) hide show
  1. package/README.md +478 -0
  2. package/bin/goki-dev.js +452 -0
  3. package/bin/mcp-server.js +16 -0
  4. package/bin/secrets-cli.js +302 -0
  5. package/cli/ComposeOverrideGenerator.js +226 -0
  6. package/cli/ComposeParser.js +73 -0
  7. package/cli/ConfigGenerator.js +304 -0
  8. package/cli/ConfigManager.js +46 -0
  9. package/cli/DatabaseManager.js +94 -0
  10. package/cli/DevToolsChecker.js +21 -0
  11. package/cli/DevToolsDir.js +66 -0
  12. package/cli/DevToolsManager.js +451 -0
  13. package/cli/DockerManager.js +138 -0
  14. package/cli/FunctionManager.js +95 -0
  15. package/cli/HttpProxyRewriter.js +91 -0
  16. package/cli/Logger.js +10 -0
  17. package/cli/McpConfigManager.js +123 -0
  18. package/cli/NgrokManager.js +431 -0
  19. package/cli/ProjectCLI.js +2322 -0
  20. package/cli/PubSubManager.js +129 -0
  21. package/cli/SnapshotManager.js +88 -0
  22. package/cli/UiFormatter.js +292 -0
  23. package/cli/WebhookUrlRewriter.js +32 -0
  24. package/cli/secrets/BiometricAuth.js +125 -0
  25. package/cli/secrets/SecretInjector.js +47 -0
  26. package/cli/secrets/SecretsConfig.js +141 -0
  27. package/cli/secrets/SecretsDoctor.js +384 -0
  28. package/cli/secrets/SecretsManager.js +255 -0
  29. package/client/dist/client.d.ts +332 -0
  30. package/client/dist/client.js +507 -0
  31. package/client/dist/helpers.d.ts +62 -0
  32. package/client/dist/helpers.js +122 -0
  33. package/client/dist/index.d.ts +59 -0
  34. package/client/dist/index.js +78 -0
  35. package/client/dist/package.json +1 -0
  36. package/client/dist/types.d.ts +280 -0
  37. package/client/dist/types.js +7 -0
  38. package/config.development +46 -0
  39. package/config.test +18 -0
  40. package/guidelines/CodingStyleGuideline.md +148 -0
  41. package/guidelines/CommentingGuideline.md +10 -0
  42. package/guidelines/HttpApiImplementationGuideline.md +137 -0
  43. package/guidelines/NamingGuideline.md +182 -0
  44. package/package.json +138 -0
  45. package/patterns/api/[collectionName]/Controllers.md +62 -0
  46. package/patterns/api/[collectionName]/Logic.md +154 -0
  47. package/patterns/api/[collectionName]/Permissions.md +81 -0
  48. package/patterns/api/[collectionName]/Router.md +83 -0
  49. package/patterns/api/[collectionName]/Schemas.md +197 -0
  50. package/patterns/configs/Patterns.md +7 -0
  51. package/patterns/enums/Patterns.md +24 -0
  52. package/patterns/errorHandling/Patterns.md +185 -0
  53. package/patterns/testing/Patterns.md +232 -0
  54. package/src/Server.js +238 -0
  55. package/src/api/dashboard/Controllers.js +9 -0
  56. package/src/api/dashboard/Logic.js +76 -0
  57. package/src/api/dashboard/Router.js +11 -0
  58. package/src/api/dashboard/Schemas.js +47 -0
  59. package/src/api/data/Controllers.js +26 -0
  60. package/src/api/data/Logic.js +188 -0
  61. package/src/api/data/Router.js +16 -0
  62. package/src/api/docker/Controllers.js +33 -0
  63. package/src/api/docker/Logic.js +268 -0
  64. package/src/api/docker/Router.js +15 -0
  65. package/src/api/docker/Schemas.js +80 -0
  66. package/src/api/docs/Controllers.js +15 -0
  67. package/src/api/docs/Logic.js +85 -0
  68. package/src/api/docs/Router.js +12 -0
  69. package/src/api/export/Controllers.js +30 -0
  70. package/src/api/export/Logic.js +143 -0
  71. package/src/api/export/Router.js +18 -0
  72. package/src/api/export/Schemas.js +104 -0
  73. package/src/api/firestore/Controllers.js +152 -0
  74. package/src/api/firestore/Logic.js +474 -0
  75. package/src/api/firestore/Router.js +23 -0
  76. package/src/api/functions/Controllers.js +261 -0
  77. package/src/api/functions/Logic.js +710 -0
  78. package/src/api/functions/Router.js +50 -0
  79. package/src/api/functions/Schemas.js +193 -0
  80. package/src/api/gateway/Controllers.js +72 -0
  81. package/src/api/gateway/Logic.js +74 -0
  82. package/src/api/gateway/Router.js +10 -0
  83. package/src/api/gateway/Schemas.js +19 -0
  84. package/src/api/health/Controllers.js +14 -0
  85. package/src/api/health/Logic.js +24 -0
  86. package/src/api/health/Router.js +12 -0
  87. package/src/api/httpTraffic/Controllers.js +29 -0
  88. package/src/api/httpTraffic/Logic.js +33 -0
  89. package/src/api/httpTraffic/Router.js +9 -0
  90. package/src/api/httpTraffic/Schemas.js +23 -0
  91. package/src/api/logging/Controllers.js +80 -0
  92. package/src/api/logging/Logic.js +461 -0
  93. package/src/api/logging/Router.js +24 -0
  94. package/src/api/logging/Schemas.js +43 -0
  95. package/src/api/mqtt/Controllers.js +17 -0
  96. package/src/api/mqtt/Logic.js +66 -0
  97. package/src/api/mqtt/Router.js +12 -0
  98. package/src/api/postgres/Controllers.js +97 -0
  99. package/src/api/postgres/Logic.js +221 -0
  100. package/src/api/postgres/Router.js +21 -0
  101. package/src/api/pubsub/Controllers.js +236 -0
  102. package/src/api/pubsub/Logic.js +732 -0
  103. package/src/api/pubsub/Router.js +41 -0
  104. package/src/api/pubsub/Schemas.js +355 -0
  105. package/src/api/redis/Controllers.js +63 -0
  106. package/src/api/redis/Logic.js +239 -0
  107. package/src/api/redis/Router.js +21 -0
  108. package/src/api/scheduler/Controllers.js +27 -0
  109. package/src/api/scheduler/Logic.js +49 -0
  110. package/src/api/scheduler/Router.js +16 -0
  111. package/src/api/services/Controllers.js +26 -0
  112. package/src/api/services/Logic.js +205 -0
  113. package/src/api/services/Router.js +14 -0
  114. package/src/api/services/Schemas.js +66 -0
  115. package/src/api/snapshots/Controllers.js +37 -0
  116. package/src/api/snapshots/Logic.js +797 -0
  117. package/src/api/snapshots/Router.js +15 -0
  118. package/src/api/snapshots/Schemas.js +23 -0
  119. package/src/api/webhooks/Controllers.js +49 -0
  120. package/src/api/webhooks/Logic.js +137 -0
  121. package/src/api/webhooks/Router.js +12 -0
  122. package/src/api/webhooks/Schemas.js +31 -0
  123. package/src/configs/Application.js +147 -0
  124. package/src/configs/Default.js +13 -0
  125. package/src/consumers/BlackboxLogsConsumer.js +235 -0
  126. package/src/consumers/DockerLogsConsumer.js +687 -0
  127. package/src/db/Tables.js +66 -0
  128. package/src/db/schemas/firestore.js +18 -0
  129. package/src/db/schemas/functions.js +65 -0
  130. package/src/db/schemas/httpTraffic.js +43 -0
  131. package/src/db/schemas/logging.js +74 -0
  132. package/src/db/schemas/migrations.js +64 -0
  133. package/src/db/schemas/mqtt.js +56 -0
  134. package/src/db/schemas/pubsub.js +90 -0
  135. package/src/db/schemas/pubsubRegistry.js +22 -0
  136. package/src/db/schemas/webhooks.js +28 -0
  137. package/src/emulation/awsiot/Controllers.js +91 -0
  138. package/src/emulation/awsiot/Logic.js +70 -0
  139. package/src/emulation/awsiot/Router.js +19 -0
  140. package/src/emulation/awsiot/Server.js +100 -0
  141. package/src/emulation/firestore/Server.js +136 -0
  142. package/src/emulation/logging/Controllers.js +212 -0
  143. package/src/emulation/logging/Logic.js +416 -0
  144. package/src/emulation/logging/Router.js +36 -0
  145. package/src/emulation/logging/Schemas.js +82 -0
  146. package/src/emulation/logging/Server.js +108 -0
  147. package/src/emulation/pubsub/Controllers.js +279 -0
  148. package/src/emulation/pubsub/DefaultTopics.js +162 -0
  149. package/src/emulation/pubsub/Logic.js +427 -0
  150. package/src/emulation/pubsub/README.md +309 -0
  151. package/src/emulation/pubsub/Router.js +33 -0
  152. package/src/emulation/pubsub/Server.js +104 -0
  153. package/src/emulation/pubsub/ShadowPoller.js +276 -0
  154. package/src/emulation/pubsub/ShadowSubscriptionManager.js +199 -0
  155. package/src/enums/ContainerNames.js +106 -0
  156. package/src/enums/ErrorReason.js +28 -0
  157. package/src/enums/FunctionStatuses.js +15 -0
  158. package/src/enums/FunctionTriggerTypes.js +15 -0
  159. package/src/enums/GatewayState.js +7 -0
  160. package/src/enums/ServiceNames.js +68 -0
  161. package/src/jobs/DatabaseMaintenance.js +184 -0
  162. package/src/jobs/MessageHistoryCleanup.js +152 -0
  163. package/src/mcp/ApiClient.js +25 -0
  164. package/src/mcp/Server.js +52 -0
  165. package/src/mcp/prompts/debugging.js +104 -0
  166. package/src/mcp/resources/platform.js +118 -0
  167. package/src/mcp/tools/data.js +84 -0
  168. package/src/mcp/tools/docker.js +166 -0
  169. package/src/mcp/tools/firestore.js +162 -0
  170. package/src/mcp/tools/functions.js +380 -0
  171. package/src/mcp/tools/httpTraffic.js +69 -0
  172. package/src/mcp/tools/logging.js +174 -0
  173. package/src/mcp/tools/mqtt.js +37 -0
  174. package/src/mcp/tools/postgres.js +130 -0
  175. package/src/mcp/tools/pubsub.js +316 -0
  176. package/src/mcp/tools/redis.js +146 -0
  177. package/src/mcp/tools/services.js +169 -0
  178. package/src/mcp/tools/snapshots.js +88 -0
  179. package/src/mcp/tools/webhooks.js +115 -0
  180. package/src/middleware/DevProxy.js +67 -0
  181. package/src/middleware/ErrorCatcher.js +35 -0
  182. package/src/middleware/HttpProxy.js +215 -0
  183. package/src/middleware/Reply.js +24 -0
  184. package/src/middleware/TraceId.js +9 -0
  185. package/src/middleware/WebhookProxy.js +234 -0
  186. package/src/protocols/mqtt/Broker.js +92 -0
  187. package/src/protocols/mqtt/Handlers.js +175 -0
  188. package/src/protocols/mqtt/PubSubBridge.js +162 -0
  189. package/src/protocols/mqtt/Server.js +116 -0
  190. package/src/runtime/FunctionRunner.js +179 -0
  191. package/src/services/AppGatewayService.js +582 -0
  192. package/src/singletons/FirestoreBroadcaster.js +367 -0
  193. package/src/singletons/FunctionTriggerDispatcher.js +456 -0
  194. package/src/singletons/FunctionsService.js +418 -0
  195. package/src/singletons/HttpProxy.js +224 -0
  196. package/src/singletons/LogBroadcaster.js +159 -0
  197. package/src/singletons/Logger.js +49 -0
  198. package/src/singletons/MemoryJsonStore.js +175 -0
  199. package/src/singletons/MessageBroadcaster.js +190 -0
  200. package/src/singletons/PostgresBroadcaster.js +367 -0
  201. package/src/singletons/PostgresClient.js +180 -0
  202. package/src/singletons/RedisClient.js +184 -0
  203. package/src/singletons/SqliteStore.js +480 -0
  204. package/src/singletons/TickService.js +151 -0
  205. package/src/singletons/WebhookProxy.js +223 -0
@@ -0,0 +1,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()