@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,456 @@
|
|
|
1
|
+
import { v4 as uuid } from 'uuid'
|
|
2
|
+
import cron from 'node-cron'
|
|
3
|
+
import { SqliteStore } from './SqliteStore.js'
|
|
4
|
+
import { FunctionsService } from './FunctionsService.js'
|
|
5
|
+
import { Logger } from './Logger.js'
|
|
6
|
+
import { CLOUD_FUNCTIONS, CLOUD_FUNCTION_INVOCATIONS } from '../db/Tables.js'
|
|
7
|
+
import { FunctionTriggerTypes } from '../enums/FunctionTriggerTypes.js'
|
|
8
|
+
|
|
9
|
+
// Lazy import to avoid circular dependency (FirestoreBroadcaster → FunctionTriggerDispatcher → FirestoreBroadcaster)
|
|
10
|
+
let _firestoreBroadcaster = null
|
|
11
|
+
async function getFirestoreBroadcaster () {
|
|
12
|
+
if (!_firestoreBroadcaster) {
|
|
13
|
+
const mod = await import('./FirestoreBroadcaster.js')
|
|
14
|
+
_firestoreBroadcaster = mod.FirestoreBroadcaster
|
|
15
|
+
}
|
|
16
|
+
return _firestoreBroadcaster
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Event router singleton. Hooks into event sources (Pub/Sub, Firestore, Scheduler)
|
|
21
|
+
* and forwards CloudEvents to matching Cloud Functions.
|
|
22
|
+
*/
|
|
23
|
+
class FunctionTriggerDispatcherClass {
|
|
24
|
+
constructor () {
|
|
25
|
+
this.cronJobs = new Map()
|
|
26
|
+
this.isRunning = false
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async start () {
|
|
30
|
+
const schedulerFunctions = SqliteStore.find(CLOUD_FUNCTIONS, {
|
|
31
|
+
triggerType: FunctionTriggerTypes.SCHEDULER,
|
|
32
|
+
enabled: 1
|
|
33
|
+
})
|
|
34
|
+
for (const fn of schedulerFunctions) {
|
|
35
|
+
try {
|
|
36
|
+
this.startCronJob(fn)
|
|
37
|
+
} catch (error) {
|
|
38
|
+
Logger.log({
|
|
39
|
+
level: 'error',
|
|
40
|
+
message: `Failed to start cron job for function: ${fn.name}`,
|
|
41
|
+
data: { error: error.message }
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const firestoreFunctions = SqliteStore.find(CLOUD_FUNCTIONS, {
|
|
46
|
+
triggerType: FunctionTriggerTypes.FIRESTORE,
|
|
47
|
+
enabled: 1
|
|
48
|
+
})
|
|
49
|
+
for (const fn of firestoreFunctions) {
|
|
50
|
+
try {
|
|
51
|
+
this.startFirestoreListener(fn)
|
|
52
|
+
} catch (error) {
|
|
53
|
+
Logger.log({
|
|
54
|
+
level: 'error',
|
|
55
|
+
message: `Failed to start Firestore listener for function: ${fn.name}`,
|
|
56
|
+
data: { error: error.message }
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
this.isRunning = true
|
|
61
|
+
Logger.log({
|
|
62
|
+
level: 'info',
|
|
63
|
+
message: 'FunctionTriggerDispatcher started',
|
|
64
|
+
data: { cronJobs: this.cronJobs.size, firestoreListeners: firestoreFunctions.length }
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
stop () {
|
|
69
|
+
for (const [, task] of this.cronJobs) {
|
|
70
|
+
task.stop()
|
|
71
|
+
}
|
|
72
|
+
this.cronJobs.clear()
|
|
73
|
+
this.isRunning = false
|
|
74
|
+
Logger.log({ level: 'info', message: 'FunctionTriggerDispatcher stopped' })
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
registerFunction (fn) {
|
|
78
|
+
if (fn.triggerType === FunctionTriggerTypes.SCHEDULER) {
|
|
79
|
+
this.startCronJob(fn)
|
|
80
|
+
}
|
|
81
|
+
if (fn.triggerType === FunctionTriggerTypes.FIRESTORE) {
|
|
82
|
+
this.startFirestoreListener(fn)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
unregisterFunction (fn) {
|
|
87
|
+
if (fn.triggerType === FunctionTriggerTypes.SCHEDULER) {
|
|
88
|
+
this.stopCronJob(fn.id)
|
|
89
|
+
}
|
|
90
|
+
if (fn.triggerType === FunctionTriggerTypes.FIRESTORE) {
|
|
91
|
+
this.stopFirestoreListener(fn)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// --- Event Handlers ---
|
|
96
|
+
|
|
97
|
+
async onPubSubMessage (message, topicFullName) {
|
|
98
|
+
if (!this.isRunning) return
|
|
99
|
+
try {
|
|
100
|
+
const functions = SqliteStore.find(CLOUD_FUNCTIONS, {
|
|
101
|
+
triggerType: FunctionTriggerTypes.PUBSUB,
|
|
102
|
+
enabled: 1
|
|
103
|
+
})
|
|
104
|
+
const shortName = topicFullName.split('/').pop()
|
|
105
|
+
for (const fn of functions) {
|
|
106
|
+
const config = typeof fn.triggerConfig === 'string'
|
|
107
|
+
? JSON.parse(fn.triggerConfig)
|
|
108
|
+
: fn.triggerConfig || {}
|
|
109
|
+
if (!config.topic) continue
|
|
110
|
+
if (config.topic !== topicFullName && config.topic !== shortName) continue
|
|
111
|
+
const cloudEvent = this.buildPubSubCloudEvent(fn, message, topicFullName)
|
|
112
|
+
this.invokeFunction(fn, cloudEvent, `pubsub:${topicFullName}`).catch(err => {
|
|
113
|
+
Logger.log({
|
|
114
|
+
level: 'error',
|
|
115
|
+
message: `Pub/Sub invocation failed for ${fn.name}`,
|
|
116
|
+
data: { error: err.message }
|
|
117
|
+
})
|
|
118
|
+
})
|
|
119
|
+
}
|
|
120
|
+
} catch (error) {
|
|
121
|
+
Logger.log({
|
|
122
|
+
level: 'error',
|
|
123
|
+
message: 'Error processing Pub/Sub message for Cloud Functions',
|
|
124
|
+
data: { error: error.message, topic: topicFullName }
|
|
125
|
+
})
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async onFirestoreChange (changeEvent) {
|
|
130
|
+
if (!this.isRunning) return
|
|
131
|
+
try {
|
|
132
|
+
const functions = SqliteStore.find(CLOUD_FUNCTIONS, {
|
|
133
|
+
triggerType: FunctionTriggerTypes.FIRESTORE,
|
|
134
|
+
enabled: 1
|
|
135
|
+
})
|
|
136
|
+
for (const fn of functions) {
|
|
137
|
+
const config = typeof fn.triggerConfig === 'string'
|
|
138
|
+
? JSON.parse(fn.triggerConfig)
|
|
139
|
+
: fn.triggerConfig || {}
|
|
140
|
+
if (!this.matchesFirestoreTrigger(config, changeEvent)) continue
|
|
141
|
+
const cloudEvent = this.buildFirestoreCloudEvent(fn, changeEvent)
|
|
142
|
+
this.invokeFunction(fn, cloudEvent, `firestore:${changeEvent.collectionPath}/${changeEvent.documentId}`).catch(err => {
|
|
143
|
+
Logger.log({
|
|
144
|
+
level: 'error',
|
|
145
|
+
message: `Firestore invocation failed for ${fn.name}`,
|
|
146
|
+
data: { error: err.message }
|
|
147
|
+
})
|
|
148
|
+
})
|
|
149
|
+
}
|
|
150
|
+
} catch (error) {
|
|
151
|
+
Logger.log({
|
|
152
|
+
level: 'error',
|
|
153
|
+
message: 'Error processing Firestore change for Cloud Functions',
|
|
154
|
+
data: { error: error.message }
|
|
155
|
+
})
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// --- CloudEvent Builders ---
|
|
160
|
+
|
|
161
|
+
buildPubSubCloudEvent (fn, message, topic) {
|
|
162
|
+
return {
|
|
163
|
+
specversion: '1.0',
|
|
164
|
+
id: uuid(),
|
|
165
|
+
type: 'google.cloud.pubsub.topic.v1.messagePublished',
|
|
166
|
+
source: `//pubsub.googleapis.com/${topic}`,
|
|
167
|
+
time: new Date().toISOString(),
|
|
168
|
+
data: {
|
|
169
|
+
message: {
|
|
170
|
+
messageId: message.messageId,
|
|
171
|
+
publishTime: message.publishTime || new Date().toISOString(),
|
|
172
|
+
data: message.data || '',
|
|
173
|
+
attributes: message.attributes || {}
|
|
174
|
+
},
|
|
175
|
+
subscription: `projects/dev-tools/subscriptions/${fn.name}-sub`
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
buildFirestoreCloudEvent (fn, changeEvent) {
|
|
181
|
+
const changeTypeMap = {
|
|
182
|
+
added: 'google.cloud.firestore.document.v1.created',
|
|
183
|
+
modified: 'google.cloud.firestore.document.v1.updated',
|
|
184
|
+
removed: 'google.cloud.firestore.document.v1.deleted'
|
|
185
|
+
}
|
|
186
|
+
const config = typeof fn.triggerConfig === 'string'
|
|
187
|
+
? JSON.parse(fn.triggerConfig)
|
|
188
|
+
: fn.triggerConfig || {}
|
|
189
|
+
let eventType = changeTypeMap[changeEvent.changeType]
|
|
190
|
+
if (config.eventType === 'write') {
|
|
191
|
+
eventType = 'google.cloud.firestore.document.v1.written'
|
|
192
|
+
}
|
|
193
|
+
const projectId = changeEvent.projectId || 'dev-tools'
|
|
194
|
+
return {
|
|
195
|
+
specversion: '1.0',
|
|
196
|
+
id: uuid(),
|
|
197
|
+
type: eventType,
|
|
198
|
+
source: `//firestore.googleapis.com/projects/${projectId}/databases/(default)`,
|
|
199
|
+
time: changeEvent.timestamp || new Date().toISOString(),
|
|
200
|
+
data: {
|
|
201
|
+
value: changeEvent.document || null,
|
|
202
|
+
oldValue: null,
|
|
203
|
+
updateMask: null
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
buildSchedulerCloudEvent (fn) {
|
|
209
|
+
const config = typeof fn.triggerConfig === 'string'
|
|
210
|
+
? JSON.parse(fn.triggerConfig)
|
|
211
|
+
: fn.triggerConfig || {}
|
|
212
|
+
return {
|
|
213
|
+
specversion: '1.0',
|
|
214
|
+
id: uuid(),
|
|
215
|
+
type: 'google.cloud.scheduler.job.v1.executed',
|
|
216
|
+
source: `//cloudscheduler.googleapis.com/projects/dev-tools/locations/local/jobs/${fn.name}`,
|
|
217
|
+
time: new Date().toISOString(),
|
|
218
|
+
data: config.payload || {}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
buildManualCloudEvent (payload) {
|
|
223
|
+
return {
|
|
224
|
+
specversion: '1.0',
|
|
225
|
+
id: uuid(),
|
|
226
|
+
type: 'dev-tools.manual.invoke',
|
|
227
|
+
source: '//dev-tools/manual',
|
|
228
|
+
time: new Date().toISOString(),
|
|
229
|
+
data: payload || {}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// --- Core Invocation ---
|
|
234
|
+
|
|
235
|
+
async invokeFunction (fn, cloudEvent, triggerSource) {
|
|
236
|
+
const endpointUrl = FunctionsService.getEndpointUrl(fn.name)
|
|
237
|
+
if (!endpointUrl) {
|
|
238
|
+
Logger.log({
|
|
239
|
+
level: 'warn',
|
|
240
|
+
message: `Function not running, skipping invocation: ${fn.name}`,
|
|
241
|
+
data: { triggerSource }
|
|
242
|
+
})
|
|
243
|
+
return
|
|
244
|
+
}
|
|
245
|
+
const startedAt = new Date().toISOString()
|
|
246
|
+
const invocationId = uuid()
|
|
247
|
+
let responseStatus = null
|
|
248
|
+
let responseBody = null
|
|
249
|
+
let responseTimeMs = null
|
|
250
|
+
let errorText = null
|
|
251
|
+
const startMs = Date.now()
|
|
252
|
+
try {
|
|
253
|
+
const timeoutMs = (fn.timeoutSeconds || 60) * 1000
|
|
254
|
+
const response = await fetch(endpointUrl, {
|
|
255
|
+
method: 'POST',
|
|
256
|
+
headers: {
|
|
257
|
+
'Content-Type': 'application/cloudevents+json',
|
|
258
|
+
'ce-id': cloudEvent.id,
|
|
259
|
+
'ce-type': cloudEvent.type,
|
|
260
|
+
'ce-source': cloudEvent.source,
|
|
261
|
+
'ce-specversion': cloudEvent.specversion,
|
|
262
|
+
'ce-time': cloudEvent.time
|
|
263
|
+
},
|
|
264
|
+
body: JSON.stringify(cloudEvent),
|
|
265
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
266
|
+
})
|
|
267
|
+
responseTimeMs = Date.now() - startMs
|
|
268
|
+
responseStatus = response.status
|
|
269
|
+
try {
|
|
270
|
+
responseBody = await response.text()
|
|
271
|
+
} catch {
|
|
272
|
+
responseBody = null
|
|
273
|
+
}
|
|
274
|
+
} catch (error) {
|
|
275
|
+
responseTimeMs = Date.now() - startMs
|
|
276
|
+
errorText = error.message
|
|
277
|
+
Logger.log({
|
|
278
|
+
level: 'error',
|
|
279
|
+
message: `Function invocation failed: ${fn.name}`,
|
|
280
|
+
data: { error: error.message, triggerSource }
|
|
281
|
+
})
|
|
282
|
+
}
|
|
283
|
+
// Record invocation
|
|
284
|
+
try {
|
|
285
|
+
SqliteStore.create(CLOUD_FUNCTION_INVOCATIONS, {
|
|
286
|
+
id: invocationId,
|
|
287
|
+
functionId: fn.id,
|
|
288
|
+
functionName: fn.name,
|
|
289
|
+
triggerType: fn.triggerType,
|
|
290
|
+
triggerSource: triggerSource || null,
|
|
291
|
+
cloudEvent: JSON.stringify(cloudEvent),
|
|
292
|
+
responseStatus,
|
|
293
|
+
responseBody,
|
|
294
|
+
responseTimeMs,
|
|
295
|
+
error: errorText,
|
|
296
|
+
startedAt,
|
|
297
|
+
completedAt: new Date().toISOString()
|
|
298
|
+
})
|
|
299
|
+
} catch (error) {
|
|
300
|
+
Logger.log({
|
|
301
|
+
level: 'warn',
|
|
302
|
+
message: 'Failed to record function invocation',
|
|
303
|
+
data: { functionName: fn.name, error: error.message }
|
|
304
|
+
})
|
|
305
|
+
}
|
|
306
|
+
// Update function stats
|
|
307
|
+
try {
|
|
308
|
+
SqliteStore.update(CLOUD_FUNCTIONS, fn.id, {
|
|
309
|
+
invocationCount: (fn.invocationCount || 0) + 1,
|
|
310
|
+
lastInvokedAt: new Date().toISOString(),
|
|
311
|
+
lastStatus: responseStatus
|
|
312
|
+
})
|
|
313
|
+
} catch (error) {
|
|
314
|
+
Logger.log({
|
|
315
|
+
level: 'warn',
|
|
316
|
+
message: 'Failed to update function stats',
|
|
317
|
+
data: { functionName: fn.name, error: error.message }
|
|
318
|
+
})
|
|
319
|
+
}
|
|
320
|
+
// Broadcast invocation to SSE clients
|
|
321
|
+
const invocation = {
|
|
322
|
+
id: invocationId,
|
|
323
|
+
functionId: fn.id,
|
|
324
|
+
functionName: fn.name,
|
|
325
|
+
triggerType: fn.triggerType,
|
|
326
|
+
triggerSource,
|
|
327
|
+
responseStatus,
|
|
328
|
+
responseTimeMs,
|
|
329
|
+
error: errorText,
|
|
330
|
+
startedAt,
|
|
331
|
+
completedAt: new Date().toISOString()
|
|
332
|
+
}
|
|
333
|
+
FunctionsService.broadcastInvocation(invocation)
|
|
334
|
+
return invocation
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// --- Cron Methods ---
|
|
338
|
+
|
|
339
|
+
startCronJob (fn) {
|
|
340
|
+
const config = typeof fn.triggerConfig === 'string'
|
|
341
|
+
? JSON.parse(fn.triggerConfig)
|
|
342
|
+
: fn.triggerConfig || {}
|
|
343
|
+
const cronExpression = config.schedule || config.cronExpression
|
|
344
|
+
if (!cronExpression) {
|
|
345
|
+
Logger.log({
|
|
346
|
+
level: 'warn',
|
|
347
|
+
message: `No cron expression for scheduler function: ${fn.name}`
|
|
348
|
+
})
|
|
349
|
+
return
|
|
350
|
+
}
|
|
351
|
+
if (!cron.validate(cronExpression)) {
|
|
352
|
+
Logger.log({
|
|
353
|
+
level: 'error',
|
|
354
|
+
message: `Invalid cron expression for function: ${fn.name}`,
|
|
355
|
+
data: { cronExpression }
|
|
356
|
+
})
|
|
357
|
+
return
|
|
358
|
+
}
|
|
359
|
+
if (this.cronJobs.has(fn.id)) {
|
|
360
|
+
this.stopCronJob(fn.id)
|
|
361
|
+
}
|
|
362
|
+
const task = cron.schedule(cronExpression, () => {
|
|
363
|
+
const cloudEvent = this.buildSchedulerCloudEvent(fn)
|
|
364
|
+
this.invokeFunction(fn, cloudEvent, `scheduler:${cronExpression}`).catch(err => {
|
|
365
|
+
Logger.log({
|
|
366
|
+
level: 'error',
|
|
367
|
+
message: `Scheduled invocation failed for ${fn.name}`,
|
|
368
|
+
data: { error: err.message }
|
|
369
|
+
})
|
|
370
|
+
})
|
|
371
|
+
})
|
|
372
|
+
this.cronJobs.set(fn.id, task)
|
|
373
|
+
Logger.log({
|
|
374
|
+
level: 'info',
|
|
375
|
+
message: `Cron job started for function: ${fn.name}`,
|
|
376
|
+
data: { cronExpression }
|
|
377
|
+
})
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
stopCronJob (functionId) {
|
|
381
|
+
const task = this.cronJobs.get(functionId)
|
|
382
|
+
if (task) {
|
|
383
|
+
task.stop()
|
|
384
|
+
this.cronJobs.delete(functionId)
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// --- Firestore Listener Methods ---
|
|
389
|
+
|
|
390
|
+
async startFirestoreListener (fn) {
|
|
391
|
+
const config = typeof fn.triggerConfig === 'string'
|
|
392
|
+
? JSON.parse(fn.triggerConfig)
|
|
393
|
+
: fn.triggerConfig || {}
|
|
394
|
+
if (!config.collectionPath) {
|
|
395
|
+
Logger.log({
|
|
396
|
+
level: 'warn',
|
|
397
|
+
message: `No collectionPath for Firestore function: ${fn.name}`
|
|
398
|
+
})
|
|
399
|
+
return
|
|
400
|
+
}
|
|
401
|
+
const projectId = config.projectId || 'tipi-development'
|
|
402
|
+
try {
|
|
403
|
+
const broadcaster = await getFirestoreBroadcaster()
|
|
404
|
+
broadcaster.startCollectionListener(projectId, config.collectionPath)
|
|
405
|
+
Logger.log({
|
|
406
|
+
level: 'info',
|
|
407
|
+
message: `Firestore listener started for function: ${fn.name}`,
|
|
408
|
+
data: { projectId, collectionPath: config.collectionPath }
|
|
409
|
+
})
|
|
410
|
+
} catch (error) {
|
|
411
|
+
Logger.log({
|
|
412
|
+
level: 'error',
|
|
413
|
+
message: `Failed to start Firestore listener for function: ${fn.name}`,
|
|
414
|
+
data: { error: error.message }
|
|
415
|
+
})
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
async stopFirestoreListener (fn) {
|
|
420
|
+
const config = typeof fn.triggerConfig === 'string'
|
|
421
|
+
? JSON.parse(fn.triggerConfig)
|
|
422
|
+
: fn.triggerConfig || {}
|
|
423
|
+
if (!config.collectionPath) return
|
|
424
|
+
const projectId = config.projectId || 'tipi-development'
|
|
425
|
+
try {
|
|
426
|
+
const broadcaster = await getFirestoreBroadcaster()
|
|
427
|
+
broadcaster.stopCollectionListener(projectId, config.collectionPath)
|
|
428
|
+
} catch (error) {
|
|
429
|
+
Logger.log({
|
|
430
|
+
level: 'warn',
|
|
431
|
+
message: `Failed to stop Firestore listener for function: ${fn.name}`,
|
|
432
|
+
data: { error: error.message }
|
|
433
|
+
})
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// --- Helpers ---
|
|
438
|
+
|
|
439
|
+
matchesFirestoreTrigger (config, changeEvent) {
|
|
440
|
+
if (!config.collectionPath) return false
|
|
441
|
+
if (config.collectionPath !== changeEvent.collectionPath) return false
|
|
442
|
+
if (config.projectId && config.projectId !== changeEvent.projectId) return false
|
|
443
|
+
if (config.eventType && config.eventType !== 'write') {
|
|
444
|
+
const eventTypeMap = {
|
|
445
|
+
created: 'added',
|
|
446
|
+
updated: 'modified',
|
|
447
|
+
deleted: 'removed'
|
|
448
|
+
}
|
|
449
|
+
const expectedChangeType = eventTypeMap[config.eventType]
|
|
450
|
+
if (expectedChangeType && expectedChangeType !== changeEvent.changeType) return false
|
|
451
|
+
}
|
|
452
|
+
return true
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
export const FunctionTriggerDispatcher = new FunctionTriggerDispatcherClass()
|