@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,367 @@
1
+ // PostgresBroadcaster singleton for managing SSE connections to PostgreSQL change detection via polling
2
+ // Handles client registration, pg_stat_user_tables polling with reference counting, and change broadcasting
3
+
4
+ import { Logger } from './Logger.js'
5
+ import { PostgresClient } from './PostgresClient.js'
6
+
7
+ class PostgresBroadcasterClass {
8
+ constructor () {
9
+ this.clients = new Map() // connectionId -> { ctx, filters: { database, schema, table } }
10
+ this.watchers = new Map() // watcherKey (database:schema:table) -> { intervalId, lastStats: { inserts, updates, deletes, liveRows }, refCount }
11
+ this.heartbeatId = null
12
+ this.heartbeatIntervalMs = 30000
13
+ this.pollIntervalMs = 2000
14
+ }
15
+
16
+ /**
17
+ * Register a new SSE client and start watcher if needed
18
+ * @param {string} connectionId - Unique identifier for the connection (traceId)
19
+ * @param {Object} ctx - Koa context with res.write for SSE
20
+ * @param {Object} filters - { database, schema, table }
21
+ */
22
+ addClient (connectionId, ctx, filters = {}) {
23
+ this.clients.set(connectionId, { ctx, filters })
24
+ this.startHeartbeat()
25
+ if (filters.database && filters.schema && filters.table) {
26
+ this.startWatcher(filters.database, filters.schema, filters.table)
27
+ }
28
+ Logger.log({
29
+ level: 'info',
30
+ message: 'PostgreSQL SSE client connected',
31
+ data: { connectionId, filters, totalClients: this.clients.size }
32
+ })
33
+ }
34
+
35
+ /**
36
+ * Unregister an SSE client and stop watcher if no more clients watching
37
+ * @param {string} connectionId - Unique identifier for the connection
38
+ */
39
+ removeClient (connectionId) {
40
+ const client = this.clients.get(connectionId)
41
+ if (!client) return
42
+ const removed = this.clients.delete(connectionId)
43
+ if (removed) {
44
+ if (client.filters.database && client.filters.schema && client.filters.table) {
45
+ this.decrementWatcherRefCount(client.filters.database, client.filters.schema, client.filters.table)
46
+ }
47
+ Logger.log({
48
+ level: 'info',
49
+ message: 'PostgreSQL SSE client disconnected',
50
+ data: { connectionId, totalClients: this.clients.size }
51
+ })
52
+ if (this.clients.size === 0) {
53
+ this.stopHeartbeat()
54
+ }
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Start a polling watcher for a specific table
60
+ * Uses reference counting to share one watcher across multiple clients
61
+ * @param {string} database - PostgreSQL database name
62
+ * @param {string} schema - Schema name
63
+ * @param {string} table - Table name
64
+ */
65
+ async startWatcher (database, schema, table) {
66
+ const watcherKey = `${database}:${schema}:${table}`
67
+ if (this.watchers.has(watcherKey)) {
68
+ const watcher = this.watchers.get(watcherKey)
69
+ watcher.refCount++
70
+ Logger.log({
71
+ level: 'debug',
72
+ message: 'PostgreSQL watcher ref count incremented',
73
+ data: { database, schema, table, refCount: watcher.refCount }
74
+ })
75
+ return
76
+ }
77
+ try {
78
+ const result = await PostgresClient.query(
79
+ 'SELECT n_tup_ins, n_tup_upd, n_tup_del, n_live_tup FROM pg_stat_user_tables WHERE schemaname = $1 AND relname = $2',
80
+ [schema, table],
81
+ database
82
+ )
83
+ const lastStats = result.rows.length > 0
84
+ ? {
85
+ inserts: parseInt(result.rows[0].n_tup_ins) || 0,
86
+ updates: parseInt(result.rows[0].n_tup_upd) || 0,
87
+ deletes: parseInt(result.rows[0].n_tup_del) || 0,
88
+ liveRows: parseInt(result.rows[0].n_live_tup) || 0
89
+ }
90
+ : { inserts: 0, updates: 0, deletes: 0, liveRows: 0 }
91
+ const intervalId = setInterval(() => this.pollForChanges(watcherKey, database, schema, table), this.pollIntervalMs)
92
+ this.watchers.set(watcherKey, { intervalId, lastStats, refCount: 1 })
93
+ Logger.log({
94
+ level: 'info',
95
+ message: 'PostgreSQL watcher started',
96
+ data: { database, schema, table, lastStats }
97
+ })
98
+ } catch (error) {
99
+ Logger.log({
100
+ level: 'error',
101
+ message: 'Failed to start PostgreSQL watcher',
102
+ data: { database, schema, table, error: error.message }
103
+ })
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Decrement watcher reference count and stop if reaches zero
109
+ * @param {string} database - PostgreSQL database name
110
+ * @param {string} schema - Schema name
111
+ * @param {string} table - Table name
112
+ */
113
+ decrementWatcherRefCount (database, schema, table) {
114
+ const watcherKey = `${database}:${schema}:${table}`
115
+ const watcher = this.watchers.get(watcherKey)
116
+ if (!watcher) return
117
+ watcher.refCount--
118
+ Logger.log({
119
+ level: 'debug',
120
+ message: 'PostgreSQL watcher ref count decremented',
121
+ data: { database, schema, table, refCount: watcher.refCount }
122
+ })
123
+ if (watcher.refCount <= 0) {
124
+ this.stopWatcher(watcherKey)
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Stop a polling watcher
130
+ * @param {string} watcherKey - Key in format "database:schema:table"
131
+ */
132
+ stopWatcher (watcherKey) {
133
+ const watcher = this.watchers.get(watcherKey)
134
+ if (watcher) {
135
+ clearInterval(watcher.intervalId)
136
+ this.watchers.delete(watcherKey)
137
+ Logger.log({
138
+ level: 'info',
139
+ message: 'PostgreSQL watcher stopped',
140
+ data: { watcherKey }
141
+ })
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Poll pg_stat_user_tables for changes and broadcast if detected
147
+ * @param {string} watcherKey - Key in format "database:schema:table"
148
+ * @param {string} database - PostgreSQL database name
149
+ * @param {string} schema - Schema name
150
+ * @param {string} table - Table name
151
+ */
152
+ async pollForChanges (watcherKey, database, schema, table) {
153
+ const watcher = this.watchers.get(watcherKey)
154
+ if (!watcher) return
155
+ try {
156
+ const result = await PostgresClient.query(
157
+ 'SELECT n_tup_ins, n_tup_upd, n_tup_del, n_live_tup FROM pg_stat_user_tables WHERE schemaname = $1 AND relname = $2',
158
+ [schema, table],
159
+ database
160
+ )
161
+ if (result.rows.length === 0) return
162
+ const currentStats = {
163
+ inserts: parseInt(result.rows[0].n_tup_ins) || 0,
164
+ updates: parseInt(result.rows[0].n_tup_upd) || 0,
165
+ deletes: parseInt(result.rows[0].n_tup_del) || 0,
166
+ liveRows: parseInt(result.rows[0].n_live_tup) || 0
167
+ }
168
+ const hasChanged =
169
+ currentStats.inserts !== watcher.lastStats.inserts ||
170
+ currentStats.updates !== watcher.lastStats.updates ||
171
+ currentStats.deletes !== watcher.lastStats.deletes ||
172
+ currentStats.liveRows !== watcher.lastStats.liveRows
173
+ if (hasChanged) {
174
+ const change = {
175
+ eventId: `evt_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
176
+ timestamp: new Date().toISOString(),
177
+ database,
178
+ schema,
179
+ table,
180
+ changeType: 'data_changed',
181
+ stats: currentStats
182
+ }
183
+ Logger.log({
184
+ level: 'info',
185
+ message: 'PostgreSQL change detected',
186
+ data: { watcherKey, previousStats: watcher.lastStats, currentStats }
187
+ })
188
+ watcher.lastStats = currentStats
189
+ this.broadcast(change)
190
+ }
191
+ } catch (error) {
192
+ Logger.log({
193
+ level: 'error',
194
+ message: 'PostgreSQL poll error',
195
+ data: { watcherKey, error: error.message }
196
+ })
197
+ }
198
+ }
199
+
200
+ /**
201
+ * Broadcast a change event to all connected clients matching filters
202
+ * @param {Object} change - { eventId, timestamp, database, schema, table, changeType, stats }
203
+ */
204
+ broadcast (change) {
205
+ if (this.clients.size === 0) return
206
+ let sentCount = 0
207
+ for (const [connectionId, { ctx, filters }] of this.clients) {
208
+ if (this.matchesFilters(change, filters)) {
209
+ try {
210
+ this.sendSSE(ctx, 'change', change)
211
+ sentCount++
212
+ } catch (error) {
213
+ Logger.log({
214
+ level: 'error',
215
+ message: 'Failed to send SSE change event to client',
216
+ data: { connectionId, error: error.message }
217
+ })
218
+ this.removeClient(connectionId)
219
+ }
220
+ }
221
+ }
222
+ Logger.log({
223
+ level: 'debug',
224
+ message: 'PostgreSQL change broadcasted to SSE clients',
225
+ data: {
226
+ eventId: change.eventId,
227
+ changeType: change.changeType,
228
+ table: change.table,
229
+ totalClients: this.clients.size,
230
+ sentToClients: sentCount
231
+ }
232
+ })
233
+ }
234
+
235
+ /**
236
+ * Check if change matches client filters
237
+ * @param {Object} change - The change event to check
238
+ * @param {Object} filters - { database?, schema?, table? }
239
+ * @returns {boolean}
240
+ */
241
+ matchesFilters (change, filters) {
242
+ if (!filters || Object.keys(filters).length === 0) {
243
+ return true
244
+ }
245
+ if (filters.database && change.database !== filters.database) {
246
+ return false
247
+ }
248
+ if (filters.schema && change.schema !== filters.schema) {
249
+ return false
250
+ }
251
+ if (filters.table && change.table !== filters.table) {
252
+ return false
253
+ }
254
+ return true
255
+ }
256
+
257
+ /**
258
+ * Send SSE formatted message to client
259
+ * @param {Object} ctx - Koa context
260
+ * @param {string} event - Event name
261
+ * @param {Object} data - Data to send
262
+ */
263
+ sendSSE (ctx, event, data) {
264
+ ctx.res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`)
265
+ }
266
+
267
+ /**
268
+ * Start the shared heartbeat interval (one for all clients)
269
+ */
270
+ startHeartbeat () {
271
+ if (this.heartbeatId) return
272
+ this.heartbeatId = setInterval(() => this.sendHeartbeat(), this.heartbeatIntervalMs)
273
+ }
274
+
275
+ /**
276
+ * Stop the shared heartbeat interval when no clients remain
277
+ */
278
+ stopHeartbeat () {
279
+ if (this.heartbeatId) {
280
+ clearInterval(this.heartbeatId)
281
+ this.heartbeatId = null
282
+ }
283
+ }
284
+
285
+ /**
286
+ * Send heartbeat to all connected clients
287
+ * Used to keep connections alive
288
+ */
289
+ sendHeartbeat () {
290
+ for (const [connectionId, { ctx }] of this.clients) {
291
+ try {
292
+ ctx.res.write(': heartbeat\n\n')
293
+ } catch (error) {
294
+ Logger.log({
295
+ level: 'error',
296
+ message: 'Failed to send heartbeat to PostgreSQL SSE client',
297
+ data: { connectionId, error: error.message }
298
+ })
299
+ this.removeClient(connectionId)
300
+ }
301
+ }
302
+ }
303
+
304
+ /**
305
+ * Notify all clients watching a specific database that data has changed
306
+ * Called after write queries (INSERT/UPDATE/DELETE) to trigger immediate refresh
307
+ * @param {string} database - PostgreSQL database name
308
+ * @param {string} schema - Schema name (optional, broadcasts to all schemas if omitted)
309
+ * @param {string} table - Table name (optional, broadcasts to all tables if omitted)
310
+ */
311
+ notifyChange (database, schema, table) {
312
+ if (this.clients.size === 0) return
313
+ const change = {
314
+ eventId: `evt_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
315
+ timestamp: new Date().toISOString(),
316
+ database,
317
+ schema: schema || '*',
318
+ table: table || '*',
319
+ changeType: 'data_changed',
320
+ stats: null
321
+ }
322
+ // Broadcast to all clients watching this database
323
+ let sentCount = 0
324
+ for (const [connectionId, { ctx, filters }] of this.clients) {
325
+ if (filters.database && filters.database !== database) continue
326
+ if (schema && filters.schema && filters.schema !== schema) continue
327
+ if (table && filters.table && filters.table !== table) continue
328
+ try {
329
+ this.sendSSE(ctx, 'change', change)
330
+ sentCount++
331
+ } catch (error) {
332
+ this.removeClient(connectionId)
333
+ }
334
+ }
335
+ if (sentCount > 0) {
336
+ Logger.log({
337
+ level: 'info',
338
+ message: 'PostgreSQL change notification sent',
339
+ data: { database, schema, table, sentToClients: sentCount }
340
+ })
341
+ }
342
+ }
343
+
344
+ /**
345
+ * Get current client and watcher statistics
346
+ * @returns {Object}
347
+ */
348
+ getStats () {
349
+ const stats = {
350
+ totalClients: this.clients.size,
351
+ totalWatchers: this.watchers.size,
352
+ clientsByTable: {},
353
+ watcherRefCounts: {}
354
+ }
355
+ for (const [, { filters }] of this.clients) {
356
+ const key = `${filters.database}:${filters.schema}:${filters.table}`
357
+ stats.clientsByTable[key] = (stats.clientsByTable[key] || 0) + 1
358
+ }
359
+ for (const [watcherKey, { refCount }] of this.watchers) {
360
+ stats.watcherRefCounts[watcherKey] = refCount
361
+ }
362
+ return stats
363
+ }
364
+ }
365
+
366
+ // Export singleton instance
367
+ export const PostgresBroadcaster = new PostgresBroadcasterClass()
@@ -0,0 +1,180 @@
1
+ import pg from 'pg'
2
+ import { Application } from '../configs/Application.js'
3
+
4
+ const { Pool } = pg
5
+
6
+ class PostgresClientClass {
7
+ constructor () {
8
+ this.pools = new Map() // Map of database -> pool
9
+ this.defaultDatabase = null
10
+ }
11
+
12
+ initialize () {
13
+ // Pools are created lazily on first use
14
+ // This method exists for API compatibility with Server.js
15
+ const config = Application.postgres
16
+ this.defaultDatabase = config.database
17
+ }
18
+
19
+ getPool (database = null) {
20
+ const config = Application.postgres
21
+ const dbName = database || config.database
22
+ if (!this.pools.has(dbName)) {
23
+ const pool = new Pool({
24
+ host: config.host,
25
+ port: config.port,
26
+ user: config.user,
27
+ password: config.password,
28
+ database: dbName,
29
+ max: 5,
30
+ idleTimeoutMillis: 30000,
31
+ connectionTimeoutMillis: 5000
32
+ })
33
+ pool.on('error', (err) => {
34
+ console.error(`PostgreSQL pool error (${dbName}):`, err.message)
35
+ })
36
+ this.pools.set(dbName, pool)
37
+ }
38
+ return this.pools.get(dbName)
39
+ }
40
+
41
+ async query (text, params = [], database = null) {
42
+ const pool = this.getPool(database)
43
+ const client = await pool.connect()
44
+ try {
45
+ const result = await client.query(text, params)
46
+ return result
47
+ } finally {
48
+ client.release()
49
+ }
50
+ }
51
+
52
+ async testConnection (database = null) {
53
+ try {
54
+ const result = await this.query('SELECT NOW() as time, current_database() as database', [], database)
55
+ return { connected: true, time: result.rows[0].time, database: result.rows[0].database }
56
+ } catch (error) {
57
+ return { connected: false, error: error.message }
58
+ }
59
+ }
60
+
61
+ async getSchemas (database = null) {
62
+ const result = await this.query(`
63
+ SELECT schema_name
64
+ FROM information_schema.schemata
65
+ WHERE schema_name NOT IN ('pg_catalog', 'information_schema', 'pg_toast')
66
+ ORDER BY schema_name
67
+ `, [], database)
68
+ return result.rows.map(row => row.schema_name)
69
+ }
70
+
71
+ async getTables (schema = 'public', database = null) {
72
+ const result = await this.query(`
73
+ SELECT
74
+ t.table_name,
75
+ (SELECT COUNT(*) FROM information_schema.columns c
76
+ WHERE c.table_schema = t.table_schema AND c.table_name = t.table_name) as column_count
77
+ FROM information_schema.tables t
78
+ WHERE t.table_schema = $1
79
+ AND t.table_type = 'BASE TABLE'
80
+ ORDER BY t.table_name
81
+ `, [schema], database)
82
+ const tables = result.rows.map(row => ({
83
+ name: row.table_name,
84
+ columnCount: parseInt(row.column_count) || 0,
85
+ rowCount: 0
86
+ }))
87
+ // Fetch exact row counts for each table (dev environment, tables are small)
88
+ const countPromises = tables.map(async (table) => {
89
+ try {
90
+ const countResult = await this.query(
91
+ `SELECT COUNT(*) as count FROM "${schema}"."${table.name}"`,
92
+ [], database
93
+ )
94
+ table.rowCount = parseInt(countResult.rows[0].count) || 0
95
+ } catch (error) {
96
+ table.rowCount = 0
97
+ }
98
+ })
99
+ await Promise.all(countPromises)
100
+ return tables
101
+ }
102
+
103
+ async getColumns (schema, table, database = null) {
104
+ const result = await this.query(`
105
+ SELECT
106
+ column_name,
107
+ data_type,
108
+ is_nullable,
109
+ column_default,
110
+ character_maximum_length,
111
+ numeric_precision,
112
+ numeric_scale
113
+ FROM information_schema.columns
114
+ WHERE table_schema = $1 AND table_name = $2
115
+ ORDER BY ordinal_position
116
+ `, [schema, table], database)
117
+ return result.rows.map(row => ({
118
+ name: row.column_name,
119
+ type: row.data_type,
120
+ nullable: row.is_nullable === 'YES',
121
+ default: row.column_default,
122
+ maxLength: row.character_maximum_length,
123
+ precision: row.numeric_precision,
124
+ scale: row.numeric_scale
125
+ }))
126
+ }
127
+
128
+ async getRows (schema, table, limit = 50, offset = 0, database = null) {
129
+ const countResult = await this.query(
130
+ `SELECT COUNT(*) as total FROM "${schema}"."${table}"`,
131
+ [],
132
+ database
133
+ )
134
+ const total = parseInt(countResult.rows[0].total)
135
+ const result = await this.query(
136
+ `SELECT * FROM "${schema}"."${table}" LIMIT $1 OFFSET $2`,
137
+ [limit, offset],
138
+ database
139
+ )
140
+ return {
141
+ rows: result.rows,
142
+ total,
143
+ fields: result.fields.map(f => ({ name: f.name, dataTypeID: f.dataTypeID }))
144
+ }
145
+ }
146
+
147
+ async executeQuery (sql, database = null) {
148
+ const trimmedSql = sql.trim().toUpperCase()
149
+ const allowedStatements = ['SELECT', 'INSERT', 'UPDATE', 'DELETE', 'WITH']
150
+ const firstWord = trimmedSql.split(/\s/)[0]
151
+ if (!allowedStatements.includes(firstWord)) {
152
+ throw new Error('Only SELECT, INSERT, UPDATE, and DELETE queries are allowed')
153
+ }
154
+ const result = await this.query(sql, [], database)
155
+ return {
156
+ rows: result.rows || [],
157
+ rowCount: result.rowCount,
158
+ fields: result.fields ? result.fields.map(f => ({ name: f.name, dataTypeID: f.dataTypeID })) : []
159
+ }
160
+ }
161
+
162
+ async getDatabases () {
163
+ const result = await this.query(`
164
+ SELECT datname as name
165
+ FROM pg_database
166
+ WHERE datistemplate = false
167
+ ORDER BY datname
168
+ `)
169
+ return result.rows.map(row => row.name)
170
+ }
171
+
172
+ async shutdown () {
173
+ for (const [dbName, pool] of this.pools) {
174
+ await pool.end()
175
+ }
176
+ this.pools.clear()
177
+ }
178
+ }
179
+
180
+ export const PostgresClient = new PostgresClientClass()