@keystrokehq/snowflake 0.0.1
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 +247 -0
- package/dist/_official/index.d.mts +2 -0
- package/dist/_official/index.mjs +3 -0
- package/dist/_runtime/index.d.mts +1 -0
- package/dist/_runtime/index.mjs +1 -0
- package/dist/accounts.d.mts +40 -0
- package/dist/accounts.mjs +51 -0
- package/dist/bulk.d.mts +71 -0
- package/dist/bulk.mjs +65 -0
- package/dist/catalog.d.mts +144 -0
- package/dist/catalog.mjs +158 -0
- package/dist/client.d.mts +44 -0
- package/dist/client.mjs +273 -0
- package/dist/common-BnKTPQXc.mjs +92 -0
- package/dist/connection.d.mts +2 -0
- package/dist/connection.mjs +3 -0
- package/dist/databases.d.mts +2 -0
- package/dist/databases.mjs +3 -0
- package/dist/errors-DKnc9o_a.mjs +95 -0
- package/dist/events.d.mts +382 -0
- package/dist/events.mjs +291 -0
- package/dist/file-formats.d.mts +39 -0
- package/dist/file-formats.mjs +48 -0
- package/dist/functions.d.mts +53 -0
- package/dist/functions.mjs +56 -0
- package/dist/grants.d.mts +93 -0
- package/dist/grants.mjs +102 -0
- package/dist/index.d.mts +1 -0
- package/dist/index.mjs +1 -0
- package/dist/integration-CCWMSTlO.d.mts +170 -0
- package/dist/integration-Dptv8L0L.mjs +207 -0
- package/dist/org-admin.d.mts +80 -0
- package/dist/org-admin.mjs +79 -0
- package/dist/pipes.d.mts +119 -0
- package/dist/pipes.mjs +106 -0
- package/dist/procedures.d.mts +61 -0
- package/dist/procedures.mjs +79 -0
- package/dist/results.d.mts +16 -0
- package/dist/results.mjs +64 -0
- package/dist/retry-0hMfb8cC.mjs +164 -0
- package/dist/retry-w7cTp1QL.d.mts +10 -0
- package/dist/roles.d.mts +60 -0
- package/dist/roles.mjs +74 -0
- package/dist/rows.d.mts +106 -0
- package/dist/rows.mjs +223 -0
- package/dist/schemas/index.d.mts +2 -0
- package/dist/schemas/index.mjs +4 -0
- package/dist/schemas-catalog.d.mts +2 -0
- package/dist/schemas-catalog.mjs +3 -0
- package/dist/shares.d.mts +56 -0
- package/dist/shares.mjs +77 -0
- package/dist/sql-options-2k5xQ-oS.d.mts +32 -0
- package/dist/sql-options-DI-OmLU6.mjs +79 -0
- package/dist/sql-safety-CywdR3EP.mjs +56 -0
- package/dist/sql.d.mts +84 -0
- package/dist/sql.mjs +209 -0
- package/dist/stages.d.mts +64 -0
- package/dist/stages.mjs +81 -0
- package/dist/statements-B2ThF_4b.mjs +81 -0
- package/dist/statements-DJL0qVNA.d.mts +238 -0
- package/dist/status-page.d.mts +510 -0
- package/dist/status-page.mjs +261 -0
- package/dist/streaming.d.mts +70 -0
- package/dist/streaming.mjs +56 -0
- package/dist/streams.d.mts +71 -0
- package/dist/streams.mjs +78 -0
- package/dist/tables.d.mts +2 -0
- package/dist/tables.mjs +3 -0
- package/dist/tasks.d.mts +79 -0
- package/dist/tasks.mjs +104 -0
- package/dist/triggers.d.mts +578 -0
- package/dist/triggers.mjs +1363 -0
- package/dist/users.d.mts +61 -0
- package/dist/users.mjs +67 -0
- package/dist/verification.d.mts +201 -0
- package/dist/verification.mjs +512 -0
- package/dist/views.d.mts +2 -0
- package/dist/views.mjs +3 -0
- package/dist/warehouses.d.mts +68 -0
- package/dist/warehouses.mjs +101 -0
- package/package.json +190 -0
|
@@ -0,0 +1,1363 @@
|
|
|
1
|
+
import { c as snowflakeGcsPubSubWebhookCredentialSet, l as snowflakeS3SnsWebhookCredentialSet, n as snowflake, s as snowflakeAzureEventGridWebhookCredentialSet } from "./integration-Dptv8L0L.mjs";
|
|
2
|
+
import { createSnowflakeClient } from "./client.mjs";
|
|
3
|
+
import { azureEventGridEventSchema, gcsPubSubPushSchema, grantChangedEventSchema, pipeLoadEventSchema, scheduledDeltaEventSchema, scheduledSqlEventSchema, shareChangedEventSchema, snowflakeDiffSnapshotEntrySchema, snowflakeObjectLifecycleEventSchema, snsNotificationSchema, statementCompletedEventSchema, statusPageComponentChangeEventSchema, statusPageIncidentEventSchema, streamAppendOnlyEventSchema, streamHasDataEventSchema, taskRunEventSchema, warehouseCreditsEventSchema, warehouseQueueEventSchema } from "./events.mjs";
|
|
4
|
+
import { executeSql } from "./sql.mjs";
|
|
5
|
+
import { a as quoteLiteral, i as quoteIdentifier, t as buildFqn } from "./sql-safety-CywdR3EP.mjs";
|
|
6
|
+
import { BindingBuilder } from "./rows.mjs";
|
|
7
|
+
import { getStatusSummary, getUnresolvedIncidents } from "./status-page.mjs";
|
|
8
|
+
import { EVENT_GRID_VALIDATION_EVENT_TYPE, SnowflakeWebhookVerificationError, extractGcsObjectPayload, extractS3Records, parseAzureBlobLocation, parsePubSubPush, verifyEventGridRequest, verifyPubSubPushAuthorization, verifySnsMessage } from "./verification.mjs";
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
import { createPollingTriggerBindingFactory, createWebhookTriggerBindingFactory } from "@keystrokehq/integration-authoring";
|
|
11
|
+
|
|
12
|
+
//#region src/triggers.ts
|
|
13
|
+
function defineSnowflakePollingTrigger(definition) {
|
|
14
|
+
return definition;
|
|
15
|
+
}
|
|
16
|
+
function splitBindingOptions(input) {
|
|
17
|
+
return {
|
|
18
|
+
schedule: input?.schedule,
|
|
19
|
+
bindOptions: {
|
|
20
|
+
...input?.name ? { name: input.name } : {},
|
|
21
|
+
...input?.description ? { description: input.description } : {},
|
|
22
|
+
...input?.filter ? { filter: input.filter } : {},
|
|
23
|
+
...input?.transform ? { transform: input.transform } : {}
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function resolveSchedule(override, fallback) {
|
|
28
|
+
if (override === void 0) return fallback;
|
|
29
|
+
return override;
|
|
30
|
+
}
|
|
31
|
+
function createSnowflakePollingBindingFactory(definition, params, schedule) {
|
|
32
|
+
return createPollingTriggerBindingFactory({
|
|
33
|
+
defaultName: definition.defaultName,
|
|
34
|
+
defaultDescription: definition.defaultDescription,
|
|
35
|
+
schedule: resolveSchedule(schedule, definition.defaultSchedule),
|
|
36
|
+
credentialSets: [snowflake],
|
|
37
|
+
response: definition.responseSchema,
|
|
38
|
+
poll: (ctx) => {
|
|
39
|
+
const snowflakeCtx = ctx;
|
|
40
|
+
return definition.poll(params, snowflakeCtx.credentials.snowflake, snowflakeCtx);
|
|
41
|
+
},
|
|
42
|
+
idempotencyKey: (response) => definition.idempotencyKey(response),
|
|
43
|
+
definitionFilter: (response) => definition.hasPayload(response)
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
function bindSnowflakePollingTrigger(definition, params, input) {
|
|
47
|
+
const { schedule, bindOptions } = splitBindingOptions(input);
|
|
48
|
+
return createSnowflakePollingBindingFactory(definition, params, schedule)(bindOptions);
|
|
49
|
+
}
|
|
50
|
+
function isoNow() {
|
|
51
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
52
|
+
}
|
|
53
|
+
function readStringField(row, ...keys) {
|
|
54
|
+
for (const key of keys) {
|
|
55
|
+
const value = row[key];
|
|
56
|
+
if (typeof value === "string" && value.length > 0) return value;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function readNumberField(row, ...keys) {
|
|
60
|
+
for (const key of keys) {
|
|
61
|
+
const value = row[key];
|
|
62
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
63
|
+
if (typeof value === "string" && value.length > 0) {
|
|
64
|
+
const parsed = Number(value);
|
|
65
|
+
if (Number.isFinite(parsed)) return parsed;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function hashStatement(statement) {
|
|
70
|
+
let hash = 0;
|
|
71
|
+
for (let i = 0; i < statement.length; i += 1) hash = (hash << 5) - hash + statement.charCodeAt(i) | 0;
|
|
72
|
+
return `sha-${(hash >>> 0).toString(16)}-${statement.length}`;
|
|
73
|
+
}
|
|
74
|
+
const streamHasNewRowsDefinition = defineSnowflakePollingTrigger({
|
|
75
|
+
responseSchema: streamHasDataEventSchema,
|
|
76
|
+
defaultName: "Snowflake Stream Has New Rows",
|
|
77
|
+
defaultDescription: "Fires when SYSTEM$STREAM_HAS_DATA returns TRUE for a Snowflake stream (fires at most once per schedule tick).",
|
|
78
|
+
defaultSchedule: "1m",
|
|
79
|
+
poll: async (params, credentials) => {
|
|
80
|
+
const client = createSnowflakeClient(credentials);
|
|
81
|
+
const fqn = buildFqn(params.database, params.schema, params.streamName);
|
|
82
|
+
const row = (await executeSql(client, {
|
|
83
|
+
statement: `SELECT SYSTEM$STREAM_HAS_DATA(${quoteLiteral(fqn)}) AS HAS_DATA`,
|
|
84
|
+
warehouse: params.warehouse,
|
|
85
|
+
role: params.role
|
|
86
|
+
})).rows[0] ?? {};
|
|
87
|
+
const raw = row.HAS_DATA ?? row.has_data;
|
|
88
|
+
let hasData;
|
|
89
|
+
if (typeof raw === "boolean") hasData = raw;
|
|
90
|
+
else if (typeof raw === "string") hasData = raw.toLowerCase() === "true";
|
|
91
|
+
else hasData = Boolean(raw);
|
|
92
|
+
return {
|
|
93
|
+
streamName: fqn,
|
|
94
|
+
hasData,
|
|
95
|
+
checkedAt: isoNow()
|
|
96
|
+
};
|
|
97
|
+
},
|
|
98
|
+
idempotencyKey: (response) => `${response.streamName}:${response.checkedAt}`,
|
|
99
|
+
hasPayload: (response) => response.hasData
|
|
100
|
+
});
|
|
101
|
+
function streamHasNewRows(input) {
|
|
102
|
+
return bindSnowflakePollingTrigger(streamHasNewRowsDefinition, input, input);
|
|
103
|
+
}
|
|
104
|
+
const streamAppendOnlyDefinition = defineSnowflakePollingTrigger({
|
|
105
|
+
responseSchema: streamAppendOnlyEventSchema,
|
|
106
|
+
defaultName: "Snowflake Stream Append-Only",
|
|
107
|
+
defaultDescription: "Emits new inserted rows from a Snowflake stream on each poll. Consumers must progress the stream via downstream DML in a transaction.",
|
|
108
|
+
defaultSchedule: "1m",
|
|
109
|
+
poll: async (params, credentials) => {
|
|
110
|
+
const client = createSnowflakeClient(credentials);
|
|
111
|
+
const fqn = buildFqn(params.database, params.schema, params.streamName);
|
|
112
|
+
const limit = params.limit ?? 1e3;
|
|
113
|
+
const result = await executeSql(client, {
|
|
114
|
+
statement: `SELECT * FROM ${fqn} WHERE ${`METADATA$ACTION = 'INSERT'${params.where ? ` AND (${params.where})` : ""}`} LIMIT ${Number(limit)}`,
|
|
115
|
+
warehouse: params.warehouse,
|
|
116
|
+
role: params.role
|
|
117
|
+
});
|
|
118
|
+
const rows = result.rows;
|
|
119
|
+
return {
|
|
120
|
+
streamName: fqn,
|
|
121
|
+
rows: [...rows],
|
|
122
|
+
columns: [...result.columns],
|
|
123
|
+
rowCount: rows.length,
|
|
124
|
+
fetchedAt: isoNow()
|
|
125
|
+
};
|
|
126
|
+
},
|
|
127
|
+
idempotencyKey: (response) => `${response.streamName}:${response.fetchedAt}:${response.rowCount}`,
|
|
128
|
+
hasPayload: (response) => response.rowCount > 0
|
|
129
|
+
});
|
|
130
|
+
function streamAppendOnly(input) {
|
|
131
|
+
return bindSnowflakePollingTrigger(streamAppendOnlyDefinition, input, input);
|
|
132
|
+
}
|
|
133
|
+
const scheduledSqlDefinition = defineSnowflakePollingTrigger({
|
|
134
|
+
responseSchema: scheduledSqlEventSchema,
|
|
135
|
+
defaultName: "Snowflake Scheduled SQL",
|
|
136
|
+
defaultDescription: "Runs a caller-supplied SQL statement on each schedule tick and emits the full result set.",
|
|
137
|
+
defaultSchedule: "5m",
|
|
138
|
+
poll: async (params, credentials) => {
|
|
139
|
+
const client = createSnowflakeClient(credentials);
|
|
140
|
+
let bindings;
|
|
141
|
+
if (params.bindings && params.bindings.length > 0) {
|
|
142
|
+
const builder = new BindingBuilder();
|
|
143
|
+
for (const value of params.bindings) builder.add(value);
|
|
144
|
+
bindings = { ...builder.toRecord() };
|
|
145
|
+
}
|
|
146
|
+
const result = await executeSql(client, {
|
|
147
|
+
statement: params.statement,
|
|
148
|
+
warehouse: params.warehouse,
|
|
149
|
+
role: params.role,
|
|
150
|
+
database: params.database,
|
|
151
|
+
schema: params.schema,
|
|
152
|
+
...bindings ? { bindings } : {}
|
|
153
|
+
});
|
|
154
|
+
const rows = result.rows;
|
|
155
|
+
return {
|
|
156
|
+
statementDigest: hashStatement(params.statement),
|
|
157
|
+
rows: [...rows],
|
|
158
|
+
columns: [...result.columns],
|
|
159
|
+
rowCount: rows.length,
|
|
160
|
+
fetchedAt: isoNow()
|
|
161
|
+
};
|
|
162
|
+
},
|
|
163
|
+
idempotencyKey: (response) => `${response.statementDigest}:${response.fetchedAt}`,
|
|
164
|
+
hasPayload: (response) => response.rowCount > 0
|
|
165
|
+
});
|
|
166
|
+
function scheduledSql(input) {
|
|
167
|
+
return bindSnowflakePollingTrigger(input.emitOnEmpty ? {
|
|
168
|
+
...scheduledSqlDefinition,
|
|
169
|
+
hasPayload: () => true
|
|
170
|
+
} : scheduledSqlDefinition, input, input);
|
|
171
|
+
}
|
|
172
|
+
function stringifyCursor(value) {
|
|
173
|
+
if (value === null || value === void 0) return null;
|
|
174
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return String(value);
|
|
175
|
+
if (value instanceof Date) return value.toISOString();
|
|
176
|
+
return String(value);
|
|
177
|
+
}
|
|
178
|
+
const scheduledDeltaDefinition = defineSnowflakePollingTrigger({
|
|
179
|
+
responseSchema: scheduledDeltaEventSchema,
|
|
180
|
+
defaultName: "Snowflake Scheduled Delta",
|
|
181
|
+
defaultDescription: "Emits new rows from a Snowflake table where a cursor column exceeds the last observed high-water mark.",
|
|
182
|
+
defaultSchedule: "1m",
|
|
183
|
+
poll: async (params, credentials, ctx) => {
|
|
184
|
+
const client = createSnowflakeClient(credentials);
|
|
185
|
+
const fqn = buildFqn(params.database, params.schema, params.table);
|
|
186
|
+
const cursor = quoteIdentifier(params.cursorColumn);
|
|
187
|
+
const limit = params.limit ?? 1e3;
|
|
188
|
+
const selectList = params.columns && params.columns.length > 0 ? params.columns.map((c) => quoteIdentifier(c)).join(", ") : "*";
|
|
189
|
+
const priorHighWater = ctx.lastResponse?.highWaterValue ?? (params.initialHighWater !== void 0 ? String(params.initialHighWater) : null);
|
|
190
|
+
const clauses = [];
|
|
191
|
+
let bindings;
|
|
192
|
+
if (priorHighWater !== null) {
|
|
193
|
+
clauses.push(`${cursor} > ?`);
|
|
194
|
+
bindings = { "1": {
|
|
195
|
+
type: "TEXT",
|
|
196
|
+
value: priorHighWater
|
|
197
|
+
} };
|
|
198
|
+
}
|
|
199
|
+
if (params.where) clauses.push(`(${params.where})`);
|
|
200
|
+
const result = await executeSql(client, {
|
|
201
|
+
statement: `SELECT ${selectList} FROM ${fqn}${clauses.length > 0 ? ` WHERE ${clauses.join(" AND ")}` : ""} ORDER BY ${cursor} ASC LIMIT ${Number(limit)}`,
|
|
202
|
+
warehouse: params.warehouse,
|
|
203
|
+
role: params.role,
|
|
204
|
+
...bindings ? { bindings } : {}
|
|
205
|
+
});
|
|
206
|
+
const rows = result.rows;
|
|
207
|
+
const latest = rows[rows.length - 1]?.[params.cursorColumn] ?? rows[rows.length - 1]?.[params.cursorColumn.toUpperCase()];
|
|
208
|
+
const nextHighWater = rows.length > 0 ? stringifyCursor(latest) : priorHighWater;
|
|
209
|
+
return {
|
|
210
|
+
table: fqn,
|
|
211
|
+
cursorColumn: params.cursorColumn,
|
|
212
|
+
highWaterValue: nextHighWater,
|
|
213
|
+
rows: [...rows],
|
|
214
|
+
columns: [...result.columns],
|
|
215
|
+
rowCount: rows.length,
|
|
216
|
+
fetchedAt: isoNow()
|
|
217
|
+
};
|
|
218
|
+
},
|
|
219
|
+
idempotencyKey: (response) => `${response.table}:${response.cursorColumn}:${response.highWaterValue ?? "initial"}`,
|
|
220
|
+
hasPayload: (response) => response.rowCount > 0
|
|
221
|
+
});
|
|
222
|
+
function scheduledDelta(input) {
|
|
223
|
+
return bindSnowflakePollingTrigger(scheduledDeltaDefinition, input, input);
|
|
224
|
+
}
|
|
225
|
+
const DEFAULT_TASK_STATES = [
|
|
226
|
+
"SUCCEEDED",
|
|
227
|
+
"FAILED",
|
|
228
|
+
"FAILED_AND_AUTO_SUSPENDED",
|
|
229
|
+
"CANCELLED"
|
|
230
|
+
];
|
|
231
|
+
const taskRunCompletedDefinition = defineSnowflakePollingTrigger({
|
|
232
|
+
responseSchema: z.object({
|
|
233
|
+
events: z.array(taskRunEventSchema),
|
|
234
|
+
highWaterValue: z.string().nullable(),
|
|
235
|
+
fetchedAt: z.string()
|
|
236
|
+
}).catchall(z.unknown()),
|
|
237
|
+
defaultName: "Snowflake Task Run Completed",
|
|
238
|
+
defaultDescription: "Emits terminal task runs polled from INFORMATION_SCHEMA.TASK_HISTORY (SUCCEEDED / FAILED / CANCELLED).",
|
|
239
|
+
defaultSchedule: "1m",
|
|
240
|
+
poll: async (params, credentials, ctx) => {
|
|
241
|
+
const client = createSnowflakeClient(credentials);
|
|
242
|
+
const limit = params.limit ?? 200;
|
|
243
|
+
const statesPredicate = (params.states ?? DEFAULT_TASK_STATES).map((s) => quoteLiteral(s)).join(", ");
|
|
244
|
+
const priorHighWater = ctx.lastResponse?.highWaterValue ?? null;
|
|
245
|
+
const lookbackMinutes = params.lookbackMinutes ?? 60;
|
|
246
|
+
const since = priorHighWater ?? (/* @__PURE__ */ new Date(Date.now() - lookbackMinutes * 6e4)).toISOString();
|
|
247
|
+
const taskFilter = params.taskName ? `, TASK_NAME => ${quoteLiteral(params.taskName)}` : "";
|
|
248
|
+
const rows = (await executeSql(client, {
|
|
249
|
+
statement: `SELECT * FROM TABLE(INFORMATION_SCHEMA.TASK_HISTORY(${`SCHEDULED_TIME_RANGE_START => TO_TIMESTAMP_LTZ(${quoteLiteral(since)})${taskFilter}, RESULT_LIMIT => ${Number(limit)}`})) WHERE STATE IN (${statesPredicate}) ORDER BY SCHEDULED_TIME ASC`,
|
|
250
|
+
warehouse: params.warehouse,
|
|
251
|
+
role: params.role,
|
|
252
|
+
database: params.database,
|
|
253
|
+
schema: params.schema
|
|
254
|
+
})).rows;
|
|
255
|
+
const events = rows.map((row) => ({
|
|
256
|
+
name: readStringField(row, "NAME", "name") ?? "",
|
|
257
|
+
databaseName: readStringField(row, "DATABASE_NAME", "database_name"),
|
|
258
|
+
schemaName: readStringField(row, "SCHEMA_NAME", "schema_name"),
|
|
259
|
+
state: readStringField(row, "STATE", "state") ?? "UNKNOWN",
|
|
260
|
+
queryId: readStringField(row, "QUERY_ID", "query_id"),
|
|
261
|
+
scheduledTime: readStringField(row, "SCHEDULED_TIME", "scheduled_time"),
|
|
262
|
+
completedTime: readStringField(row, "COMPLETED_TIME", "completed_time"),
|
|
263
|
+
errorCode: readStringField(row, "ERROR_CODE", "error_code") ?? null,
|
|
264
|
+
errorMessage: readStringField(row, "ERROR_MESSAGE", "error_message") ?? null,
|
|
265
|
+
attemptNumber: readNumberField(row, "ATTEMPT_NUMBER", "attempt_number"),
|
|
266
|
+
runId: readStringField(row, "RUN_ID", "run_id"),
|
|
267
|
+
row
|
|
268
|
+
}));
|
|
269
|
+
const last = rows[rows.length - 1];
|
|
270
|
+
return {
|
|
271
|
+
events,
|
|
272
|
+
highWaterValue: readStringField(last ?? {}, "COMPLETED_TIME", "completed_time") ?? readStringField(last ?? {}, "SCHEDULED_TIME", "scheduled_time") ?? priorHighWater,
|
|
273
|
+
fetchedAt: isoNow()
|
|
274
|
+
};
|
|
275
|
+
},
|
|
276
|
+
idempotencyKey: (response) => `task-run:${response.highWaterValue ?? "initial"}:${response.events.length}`,
|
|
277
|
+
hasPayload: (response) => response.events.length > 0
|
|
278
|
+
});
|
|
279
|
+
function taskRunCompleted(input) {
|
|
280
|
+
return bindSnowflakePollingTrigger(taskRunCompletedDefinition, {
|
|
281
|
+
taskName: input?.taskName,
|
|
282
|
+
database: input?.database,
|
|
283
|
+
schema: input?.schema,
|
|
284
|
+
warehouse: input?.warehouse,
|
|
285
|
+
role: input?.role,
|
|
286
|
+
lookbackMinutes: input?.lookbackMinutes,
|
|
287
|
+
limit: input?.limit,
|
|
288
|
+
states: input?.states
|
|
289
|
+
}, input);
|
|
290
|
+
}
|
|
291
|
+
const DEFAULT_STATEMENT_STATES = [
|
|
292
|
+
"SUCCESS",
|
|
293
|
+
"FAIL",
|
|
294
|
+
"FAILED_WITH_ERROR",
|
|
295
|
+
"CANCELLED",
|
|
296
|
+
"INCIDENT"
|
|
297
|
+
];
|
|
298
|
+
const statementCompletedDefinition = defineSnowflakePollingTrigger({
|
|
299
|
+
responseSchema: z.object({
|
|
300
|
+
events: z.array(statementCompletedEventSchema),
|
|
301
|
+
highWaterValue: z.string().nullable(),
|
|
302
|
+
fetchedAt: z.string()
|
|
303
|
+
}).catchall(z.unknown()),
|
|
304
|
+
defaultName: "Snowflake Statement Completed",
|
|
305
|
+
defaultDescription: "Emits terminal statements from INFORMATION_SCHEMA.QUERY_HISTORY, optionally filtered by user / text / warehouse.",
|
|
306
|
+
defaultSchedule: "1m",
|
|
307
|
+
poll: async (params, credentials, ctx) => {
|
|
308
|
+
const client = createSnowflakeClient(credentials);
|
|
309
|
+
const limit = params.limit ?? 200;
|
|
310
|
+
const states = params.executionStatuses ?? DEFAULT_STATEMENT_STATES;
|
|
311
|
+
const priorHighWater = ctx.lastResponse?.highWaterValue ?? null;
|
|
312
|
+
const lookbackMinutes = params.lookbackMinutes ?? 30;
|
|
313
|
+
const args = [`END_TIME_RANGE_START => TO_TIMESTAMP_LTZ(${quoteLiteral(priorHighWater ?? (/* @__PURE__ */ new Date(Date.now() - lookbackMinutes * 6e4)).toISOString())})`, `RESULT_LIMIT => ${Number(limit)}`];
|
|
314
|
+
if (params.userName) args.push(`USER_NAME => ${quoteLiteral(params.userName)}`);
|
|
315
|
+
if (params.warehouse) args.push(`WAREHOUSE_NAME => ${quoteLiteral(params.warehouse)}`);
|
|
316
|
+
const clauses = [];
|
|
317
|
+
if (!params.includeInFlight) {
|
|
318
|
+
const statesPredicate = states.map((s) => quoteLiteral(s)).join(", ");
|
|
319
|
+
clauses.push(`EXECUTION_STATUS IN (${statesPredicate})`);
|
|
320
|
+
}
|
|
321
|
+
if (params.queryTextLike) clauses.push(`QUERY_TEXT ILIKE ${quoteLiteral(params.queryTextLike)}`);
|
|
322
|
+
const where = clauses.length > 0 ? ` WHERE ${clauses.join(" AND ")}` : "";
|
|
323
|
+
const rows = (await executeSql(client, {
|
|
324
|
+
statement: `SELECT * FROM TABLE(INFORMATION_SCHEMA.QUERY_HISTORY(${args.join(", ")}))${where} ORDER BY END_TIME ASC`,
|
|
325
|
+
warehouse: params.warehouse,
|
|
326
|
+
role: params.role,
|
|
327
|
+
database: params.database,
|
|
328
|
+
schema: params.schema
|
|
329
|
+
})).rows;
|
|
330
|
+
const events = rows.map((row) => ({
|
|
331
|
+
queryId: readStringField(row, "QUERY_ID", "query_id") ?? "",
|
|
332
|
+
executionStatus: readStringField(row, "EXECUTION_STATUS", "execution_status") ?? "UNKNOWN",
|
|
333
|
+
queryText: readStringField(row, "QUERY_TEXT", "query_text"),
|
|
334
|
+
errorCode: readStringField(row, "ERROR_CODE", "error_code") ?? null,
|
|
335
|
+
errorMessage: readStringField(row, "ERROR_MESSAGE", "error_message") ?? null,
|
|
336
|
+
startTime: readStringField(row, "START_TIME", "start_time"),
|
|
337
|
+
endTime: readStringField(row, "END_TIME", "end_time"),
|
|
338
|
+
warehouseName: readStringField(row, "WAREHOUSE_NAME", "warehouse_name"),
|
|
339
|
+
databaseName: readStringField(row, "DATABASE_NAME", "database_name"),
|
|
340
|
+
schemaName: readStringField(row, "SCHEMA_NAME", "schema_name"),
|
|
341
|
+
userName: readStringField(row, "USER_NAME", "user_name"),
|
|
342
|
+
roleName: readStringField(row, "ROLE_NAME", "role_name"),
|
|
343
|
+
totalElapsedTimeMs: readNumberField(row, "TOTAL_ELAPSED_TIME", "total_elapsed_time") ?? null,
|
|
344
|
+
row
|
|
345
|
+
}));
|
|
346
|
+
const last = rows[rows.length - 1];
|
|
347
|
+
return {
|
|
348
|
+
events,
|
|
349
|
+
highWaterValue: readStringField(last ?? {}, "END_TIME", "end_time") ?? readStringField(last ?? {}, "START_TIME", "start_time") ?? priorHighWater,
|
|
350
|
+
fetchedAt: isoNow()
|
|
351
|
+
};
|
|
352
|
+
},
|
|
353
|
+
idempotencyKey: (response) => `stmt:${response.highWaterValue ?? "initial"}:${response.events.length}`,
|
|
354
|
+
hasPayload: (response) => response.events.length > 0
|
|
355
|
+
});
|
|
356
|
+
function statementCompleted(input) {
|
|
357
|
+
return bindSnowflakePollingTrigger(statementCompletedDefinition, {
|
|
358
|
+
warehouse: input?.warehouse,
|
|
359
|
+
role: input?.role,
|
|
360
|
+
database: input?.database,
|
|
361
|
+
schema: input?.schema,
|
|
362
|
+
userName: input?.userName,
|
|
363
|
+
queryTextLike: input?.queryTextLike,
|
|
364
|
+
lookbackMinutes: input?.lookbackMinutes,
|
|
365
|
+
limit: input?.limit,
|
|
366
|
+
executionStatuses: input?.executionStatuses,
|
|
367
|
+
includeInFlight: input?.includeInFlight
|
|
368
|
+
}, input);
|
|
369
|
+
}
|
|
370
|
+
const pipeLoadBatchSchema = z.object({
|
|
371
|
+
events: z.array(pipeLoadEventSchema),
|
|
372
|
+
highWaterValue: z.string().nullable(),
|
|
373
|
+
fetchedAt: z.string()
|
|
374
|
+
}).catchall(z.unknown());
|
|
375
|
+
function buildPipeLoadDefinition(defaultName, defaultDescription, statusPredicate) {
|
|
376
|
+
return defineSnowflakePollingTrigger({
|
|
377
|
+
responseSchema: pipeLoadBatchSchema,
|
|
378
|
+
defaultName,
|
|
379
|
+
defaultDescription,
|
|
380
|
+
defaultSchedule: "1m",
|
|
381
|
+
poll: async (params, credentials, ctx) => {
|
|
382
|
+
const client = createSnowflakeClient(credentials);
|
|
383
|
+
const limit = params.limit ?? 200;
|
|
384
|
+
const priorHighWater = ctx.lastResponse?.highWaterValue ?? null;
|
|
385
|
+
const lookbackMinutes = params.lookbackMinutes ?? 60;
|
|
386
|
+
const args = [`START_TIME => TO_TIMESTAMP_LTZ(${quoteLiteral(priorHighWater ?? (/* @__PURE__ */ new Date(Date.now() - lookbackMinutes * 6e4)).toISOString())})`];
|
|
387
|
+
if (params.tableName) {
|
|
388
|
+
const tableFqn = buildFqn(params.database, params.schema, params.tableName);
|
|
389
|
+
args.push(`TABLE_NAME => ${quoteLiteral(tableFqn)}`);
|
|
390
|
+
}
|
|
391
|
+
const pipeFilter = params.pipeName ? ` AND PIPE_NAME = ${quoteLiteral(buildFqn(params.database, params.schema, params.pipeName))}` : "";
|
|
392
|
+
const rows = (await executeSql(client, {
|
|
393
|
+
statement: `SELECT * FROM TABLE(INFORMATION_SCHEMA.COPY_HISTORY(${args.join(", ")})) WHERE ${statusPredicate}${pipeFilter} ORDER BY LAST_LOAD_TIME ASC LIMIT ${Number(limit)}`,
|
|
394
|
+
warehouse: params.warehouse,
|
|
395
|
+
role: params.role,
|
|
396
|
+
database: params.database,
|
|
397
|
+
schema: params.schema
|
|
398
|
+
})).rows;
|
|
399
|
+
const events = rows.map((row) => ({
|
|
400
|
+
pipeName: readStringField(row, "PIPE_NAME", "pipe_name") ?? "",
|
|
401
|
+
filePath: readStringField(row, "FILE_NAME", "file_name") ?? "",
|
|
402
|
+
status: readStringField(row, "STATUS", "status") ?? "UNKNOWN",
|
|
403
|
+
loadTime: readStringField(row, "LAST_LOAD_TIME", "last_load_time"),
|
|
404
|
+
rowCount: readNumberField(row, "ROW_COUNT", "row_count") ?? null,
|
|
405
|
+
rowParsed: readNumberField(row, "ROW_PARSED", "row_parsed") ?? null,
|
|
406
|
+
errorLimit: readNumberField(row, "ERROR_LIMIT", "error_limit") ?? null,
|
|
407
|
+
errorsSeen: readNumberField(row, "ERRORS_SEEN", "errors_seen") ?? null,
|
|
408
|
+
firstErrorMessage: readStringField(row, "FIRST_ERROR_MESSAGE", "first_error_message") ?? null,
|
|
409
|
+
firstErrorLineNumber: readNumberField(row, "FIRST_ERROR_LINE_NUMBER", "first_error_line_number") ?? null,
|
|
410
|
+
firstErrorColumnName: readStringField(row, "FIRST_ERROR_COLUMN_NAME", "first_error_column_name") ?? null,
|
|
411
|
+
fileSize: readNumberField(row, "FILE_SIZE", "file_size") ?? null,
|
|
412
|
+
stageLocation: readStringField(row, "STAGE_LOCATION", "stage_location") ?? null,
|
|
413
|
+
row
|
|
414
|
+
}));
|
|
415
|
+
const last = rows[rows.length - 1];
|
|
416
|
+
return {
|
|
417
|
+
events,
|
|
418
|
+
highWaterValue: readStringField(last ?? {}, "LAST_LOAD_TIME", "last_load_time") ?? priorHighWater,
|
|
419
|
+
fetchedAt: isoNow()
|
|
420
|
+
};
|
|
421
|
+
},
|
|
422
|
+
idempotencyKey: (response) => `pipe-load:${response.highWaterValue ?? "initial"}:${response.events.length}`,
|
|
423
|
+
hasPayload: (response) => response.events.length > 0
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
const pipeLoadCompletedDefinition = buildPipeLoadDefinition("Snowflake Pipe Load Completed", "Emits successful Snowpipe loads polled from INFORMATION_SCHEMA.COPY_HISTORY.", `STATUS = 'LOADED'`);
|
|
427
|
+
const pipeLoadFailedDefinition = buildPipeLoadDefinition("Snowflake Pipe Load Failed", "Emits failed Snowpipe loads polled from INFORMATION_SCHEMA.COPY_HISTORY.", `STATUS IN ('LOAD_FAILED', 'PARTIALLY_LOADED')`);
|
|
428
|
+
function pipeLoadCompleted(input) {
|
|
429
|
+
return bindSnowflakePollingTrigger(pipeLoadCompletedDefinition, {
|
|
430
|
+
pipeName: input?.pipeName,
|
|
431
|
+
database: input?.database,
|
|
432
|
+
schema: input?.schema,
|
|
433
|
+
tableName: input?.tableName,
|
|
434
|
+
warehouse: input?.warehouse,
|
|
435
|
+
role: input?.role,
|
|
436
|
+
lookbackMinutes: input?.lookbackMinutes,
|
|
437
|
+
limit: input?.limit
|
|
438
|
+
}, input);
|
|
439
|
+
}
|
|
440
|
+
function pipeLoadFailed(input) {
|
|
441
|
+
return bindSnowflakePollingTrigger(pipeLoadFailedDefinition, {
|
|
442
|
+
pipeName: input?.pipeName,
|
|
443
|
+
database: input?.database,
|
|
444
|
+
schema: input?.schema,
|
|
445
|
+
tableName: input?.tableName,
|
|
446
|
+
warehouse: input?.warehouse,
|
|
447
|
+
role: input?.role,
|
|
448
|
+
lookbackMinutes: input?.lookbackMinutes,
|
|
449
|
+
limit: input?.limit
|
|
450
|
+
}, input);
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* Diff-trigger response envelope carried across polls. The emitted shape
|
|
454
|
+
* is `{ events: TEvent[], snapshot: Entry[], fetchedAt: string }`. The
|
|
455
|
+
* `snapshot` field is the identity+signature list that the NEXT poll
|
|
456
|
+
* will diff against — it is stored in `ctx.lastResponse` and replayed
|
|
457
|
+
* into the trigger automatically.
|
|
458
|
+
*/
|
|
459
|
+
const diffSnapshotSchema = z.array(snowflakeDiffSnapshotEntrySchema);
|
|
460
|
+
function buildDiffBatchSchema(event) {
|
|
461
|
+
return z.object({
|
|
462
|
+
events: z.array(event),
|
|
463
|
+
snapshot: diffSnapshotSchema,
|
|
464
|
+
fetchedAt: z.string()
|
|
465
|
+
}).catchall(z.unknown());
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* Compute the diff between two snapshots. For `created`: entries in
|
|
469
|
+
* `current` whose identity is not in `previous`. For `dropped`: entries
|
|
470
|
+
* in `previous` whose identity is not in `current`. For `changed`:
|
|
471
|
+
* entries in `current` whose signature differs from `previous`'s
|
|
472
|
+
* (including entries that are newly present).
|
|
473
|
+
*/
|
|
474
|
+
function diffEntries(current, previous, mode) {
|
|
475
|
+
if (previous === void 0) return [];
|
|
476
|
+
const prevMap = new Map(previous.map((p) => [p.identity, p]));
|
|
477
|
+
const currMap = new Map(current.map((c) => [c.identity, c]));
|
|
478
|
+
if (mode === "created") return current.filter((c) => !prevMap.has(c.identity));
|
|
479
|
+
if (mode === "dropped") return previous.filter((p) => !currMap.has(p.identity)).map((p) => ({
|
|
480
|
+
identity: p.identity,
|
|
481
|
+
...p.signature !== void 0 ? { signature: p.signature } : {},
|
|
482
|
+
row: p.row ?? {}
|
|
483
|
+
}));
|
|
484
|
+
return current.filter((c) => {
|
|
485
|
+
const prev = prevMap.get(c.identity);
|
|
486
|
+
if (prev === void 0) return true;
|
|
487
|
+
return prev.signature !== c.signature;
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
function toSnapshotEntries(entries) {
|
|
491
|
+
return entries.map((e) => ({
|
|
492
|
+
identity: e.identity,
|
|
493
|
+
...e.signature !== void 0 ? { signature: e.signature } : {}
|
|
494
|
+
}));
|
|
495
|
+
}
|
|
496
|
+
function sortedIdempotencyKey(prefix, events, getIdentity = (e) => e.identity ?? "") {
|
|
497
|
+
return `${prefix}:${events.map(getIdentity).slice().sort().join(",") || "empty"}`;
|
|
498
|
+
}
|
|
499
|
+
const lifecycleBatchSchema = buildDiffBatchSchema(snowflakeObjectLifecycleEventSchema);
|
|
500
|
+
function rowToLifecycleEntry(kind, row) {
|
|
501
|
+
const name = readStringField(row, "name", "NAME") ?? "";
|
|
502
|
+
const database = readStringField(row, "database_name", "DATABASE_NAME");
|
|
503
|
+
const schema = readStringField(row, "schema_name", "SCHEMA_NAME");
|
|
504
|
+
return {
|
|
505
|
+
identity: [
|
|
506
|
+
kind,
|
|
507
|
+
database ?? "",
|
|
508
|
+
schema ?? "",
|
|
509
|
+
name
|
|
510
|
+
].join("::"),
|
|
511
|
+
row
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
function lifecycleEventFromEntry(kind, change, entry, detectedAt) {
|
|
515
|
+
const row = entry.row;
|
|
516
|
+
return {
|
|
517
|
+
kind,
|
|
518
|
+
name: readStringField(row, "name", "NAME") ?? "",
|
|
519
|
+
change,
|
|
520
|
+
identity: entry.identity,
|
|
521
|
+
owner: readStringField(row, "owner", "OWNER"),
|
|
522
|
+
createdOn: readStringField(row, "created_on", "CREATED_ON"),
|
|
523
|
+
detectedAt,
|
|
524
|
+
row
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
function buildLifecycleDefinition(config) {
|
|
528
|
+
return defineSnowflakePollingTrigger({
|
|
529
|
+
responseSchema: lifecycleBatchSchema,
|
|
530
|
+
defaultName: config.defaultName,
|
|
531
|
+
defaultDescription: config.defaultDescription,
|
|
532
|
+
defaultSchedule: "5m",
|
|
533
|
+
poll: async (params, credentials, ctx) => {
|
|
534
|
+
const current = (await executeSql(createSnowflakeClient(credentials), {
|
|
535
|
+
statement: config.buildStatement(params),
|
|
536
|
+
warehouse: params.warehouse,
|
|
537
|
+
role: params.role,
|
|
538
|
+
database: params.database,
|
|
539
|
+
schema: params.schema
|
|
540
|
+
})).rows.map((row) => rowToLifecycleEntry(config.kind, row));
|
|
541
|
+
const previousSnapshot = ctx.lastResponse?.snapshot;
|
|
542
|
+
const detectedAt = isoNow();
|
|
543
|
+
let diffed;
|
|
544
|
+
if (previousSnapshot === void 0) diffed = config.change === "created" && params.emitInitialSnapshot ? current : [];
|
|
545
|
+
else diffed = diffEntries(current, previousSnapshot, config.change);
|
|
546
|
+
return {
|
|
547
|
+
events: diffed.map((entry) => lifecycleEventFromEntry(config.kind, config.change, entry, detectedAt)),
|
|
548
|
+
snapshot: toSnapshotEntries(current),
|
|
549
|
+
fetchedAt: detectedAt
|
|
550
|
+
};
|
|
551
|
+
},
|
|
552
|
+
idempotencyKey: (response) => sortedIdempotencyKey(`${config.kind}:${config.change}`, response.events),
|
|
553
|
+
hasPayload: (response) => response.events.length > 0
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
function likeClause(like) {
|
|
557
|
+
return like ? ` LIKE ${quoteLiteral(like)}` : "";
|
|
558
|
+
}
|
|
559
|
+
function inScope(params) {
|
|
560
|
+
const db = params.in?.database ?? params.database;
|
|
561
|
+
const schema = params.in?.schema ?? params.schema;
|
|
562
|
+
if (db && schema) return ` IN SCHEMA ${quoteIdentifier(db)}.${quoteIdentifier(schema)}`;
|
|
563
|
+
if (db) return ` IN DATABASE ${quoteIdentifier(db)}`;
|
|
564
|
+
return "";
|
|
565
|
+
}
|
|
566
|
+
const userCreatedDefinition = buildLifecycleDefinition({
|
|
567
|
+
kind: "USER",
|
|
568
|
+
change: "created",
|
|
569
|
+
defaultName: "Snowflake User Created",
|
|
570
|
+
defaultDescription: "Emits every new user observed via SHOW USERS since the prior poll.",
|
|
571
|
+
buildStatement: (params) => `SHOW USERS${likeClause(params.like)}`
|
|
572
|
+
});
|
|
573
|
+
const userDroppedDefinition = buildLifecycleDefinition({
|
|
574
|
+
kind: "USER",
|
|
575
|
+
change: "dropped",
|
|
576
|
+
defaultName: "Snowflake User Dropped",
|
|
577
|
+
defaultDescription: "Emits every user that has disappeared from SHOW USERS since the prior poll.",
|
|
578
|
+
buildStatement: (params) => `SHOW USERS${likeClause(params.like)}`
|
|
579
|
+
});
|
|
580
|
+
function userCreated(input) {
|
|
581
|
+
return bindSnowflakePollingTrigger(userCreatedDefinition, {
|
|
582
|
+
database: input?.database,
|
|
583
|
+
schema: input?.schema,
|
|
584
|
+
warehouse: input?.warehouse,
|
|
585
|
+
role: input?.role,
|
|
586
|
+
like: input?.like,
|
|
587
|
+
emitInitialSnapshot: input?.emitInitialSnapshot
|
|
588
|
+
}, input);
|
|
589
|
+
}
|
|
590
|
+
function userDropped(input) {
|
|
591
|
+
return bindSnowflakePollingTrigger(userDroppedDefinition, {
|
|
592
|
+
database: input?.database,
|
|
593
|
+
schema: input?.schema,
|
|
594
|
+
warehouse: input?.warehouse,
|
|
595
|
+
role: input?.role,
|
|
596
|
+
like: input?.like
|
|
597
|
+
}, input);
|
|
598
|
+
}
|
|
599
|
+
const roleCreatedDefinition = buildLifecycleDefinition({
|
|
600
|
+
kind: "ROLE",
|
|
601
|
+
change: "created",
|
|
602
|
+
defaultName: "Snowflake Role Created",
|
|
603
|
+
defaultDescription: "Emits every new role observed via SHOW ROLES since the prior poll.",
|
|
604
|
+
buildStatement: (params) => `SHOW ROLES${likeClause(params.like)}`
|
|
605
|
+
});
|
|
606
|
+
const roleDroppedDefinition = buildLifecycleDefinition({
|
|
607
|
+
kind: "ROLE",
|
|
608
|
+
change: "dropped",
|
|
609
|
+
defaultName: "Snowflake Role Dropped",
|
|
610
|
+
defaultDescription: "Emits every role that has disappeared from SHOW ROLES since the prior poll.",
|
|
611
|
+
buildStatement: (params) => `SHOW ROLES${likeClause(params.like)}`
|
|
612
|
+
});
|
|
613
|
+
function roleCreated(input) {
|
|
614
|
+
return bindSnowflakePollingTrigger(roleCreatedDefinition, {
|
|
615
|
+
warehouse: input?.warehouse,
|
|
616
|
+
role: input?.role,
|
|
617
|
+
like: input?.like,
|
|
618
|
+
emitInitialSnapshot: input?.emitInitialSnapshot
|
|
619
|
+
}, input);
|
|
620
|
+
}
|
|
621
|
+
function roleDropped(input) {
|
|
622
|
+
return bindSnowflakePollingTrigger(roleDroppedDefinition, {
|
|
623
|
+
warehouse: input?.warehouse,
|
|
624
|
+
role: input?.role,
|
|
625
|
+
like: input?.like
|
|
626
|
+
}, input);
|
|
627
|
+
}
|
|
628
|
+
const databaseCreatedDefinition = buildLifecycleDefinition({
|
|
629
|
+
kind: "DATABASE",
|
|
630
|
+
change: "created",
|
|
631
|
+
defaultName: "Snowflake Database Created",
|
|
632
|
+
defaultDescription: "Emits every new database observed via SHOW DATABASES since the prior poll.",
|
|
633
|
+
buildStatement: (params) => `SHOW DATABASES${likeClause(params.like)}`
|
|
634
|
+
});
|
|
635
|
+
const databaseDroppedDefinition = buildLifecycleDefinition({
|
|
636
|
+
kind: "DATABASE",
|
|
637
|
+
change: "dropped",
|
|
638
|
+
defaultName: "Snowflake Database Dropped",
|
|
639
|
+
defaultDescription: "Emits every database that disappeared from SHOW DATABASES since the prior poll.",
|
|
640
|
+
buildStatement: (params) => `SHOW DATABASES${likeClause(params.like)}`
|
|
641
|
+
});
|
|
642
|
+
function databaseCreated(input) {
|
|
643
|
+
return bindSnowflakePollingTrigger(databaseCreatedDefinition, {
|
|
644
|
+
warehouse: input?.warehouse,
|
|
645
|
+
role: input?.role,
|
|
646
|
+
like: input?.like,
|
|
647
|
+
emitInitialSnapshot: input?.emitInitialSnapshot
|
|
648
|
+
}, input);
|
|
649
|
+
}
|
|
650
|
+
function databaseDropped(input) {
|
|
651
|
+
return bindSnowflakePollingTrigger(databaseDroppedDefinition, {
|
|
652
|
+
warehouse: input?.warehouse,
|
|
653
|
+
role: input?.role,
|
|
654
|
+
like: input?.like
|
|
655
|
+
}, input);
|
|
656
|
+
}
|
|
657
|
+
const tableCreatedDefinition = buildLifecycleDefinition({
|
|
658
|
+
kind: "TABLE",
|
|
659
|
+
change: "created",
|
|
660
|
+
defaultName: "Snowflake Table Created",
|
|
661
|
+
defaultDescription: "Emits every new table observed via SHOW TABLES since the prior poll.",
|
|
662
|
+
buildStatement: (params) => `SHOW TABLES${likeClause(params.like)}${inScope(params)}`
|
|
663
|
+
});
|
|
664
|
+
const tableDroppedDefinition = buildLifecycleDefinition({
|
|
665
|
+
kind: "TABLE",
|
|
666
|
+
change: "dropped",
|
|
667
|
+
defaultName: "Snowflake Table Dropped",
|
|
668
|
+
defaultDescription: "Emits every table that disappeared from SHOW TABLES since the prior poll.",
|
|
669
|
+
buildStatement: (params) => `SHOW TABLES${likeClause(params.like)}${inScope(params)}`
|
|
670
|
+
});
|
|
671
|
+
function tableCreated(input) {
|
|
672
|
+
return bindSnowflakePollingTrigger(tableCreatedDefinition, {
|
|
673
|
+
in: input?.in,
|
|
674
|
+
database: input?.database,
|
|
675
|
+
schema: input?.schema,
|
|
676
|
+
warehouse: input?.warehouse,
|
|
677
|
+
role: input?.role,
|
|
678
|
+
like: input?.like,
|
|
679
|
+
emitInitialSnapshot: input?.emitInitialSnapshot
|
|
680
|
+
}, input);
|
|
681
|
+
}
|
|
682
|
+
function tableDropped(input) {
|
|
683
|
+
return bindSnowflakePollingTrigger(tableDroppedDefinition, {
|
|
684
|
+
in: input?.in,
|
|
685
|
+
database: input?.database,
|
|
686
|
+
schema: input?.schema,
|
|
687
|
+
warehouse: input?.warehouse,
|
|
688
|
+
role: input?.role,
|
|
689
|
+
like: input?.like
|
|
690
|
+
}, input);
|
|
691
|
+
}
|
|
692
|
+
const shareChangedBatchSchema = buildDiffBatchSchema(shareChangedEventSchema);
|
|
693
|
+
const SHARE_SIGNATURE_FIELDS = [
|
|
694
|
+
"kind",
|
|
695
|
+
"owner",
|
|
696
|
+
"comment",
|
|
697
|
+
"to",
|
|
698
|
+
"database_name",
|
|
699
|
+
"listing_global_name"
|
|
700
|
+
];
|
|
701
|
+
function signatureOf(row, fields) {
|
|
702
|
+
return fields.map((field) => {
|
|
703
|
+
const upper = field.toUpperCase();
|
|
704
|
+
const v = row[field] ?? row[upper];
|
|
705
|
+
return v === void 0 || v === null ? "" : String(v);
|
|
706
|
+
}).join("|");
|
|
707
|
+
}
|
|
708
|
+
const shareChangedDefinition = defineSnowflakePollingTrigger({
|
|
709
|
+
responseSchema: shareChangedBatchSchema,
|
|
710
|
+
defaultName: "Snowflake Share Changed",
|
|
711
|
+
defaultDescription: "Emits share create / drop / modify events observed via SHOW SHARES. Modifications diff against a signature of kind / owner / comment / accounts / listing.",
|
|
712
|
+
defaultSchedule: "5m",
|
|
713
|
+
poll: async (params, credentials, ctx) => {
|
|
714
|
+
const current = (await executeSql(createSnowflakeClient(credentials), {
|
|
715
|
+
statement: `SHOW SHARES${likeClause(params.like)}`,
|
|
716
|
+
warehouse: params.warehouse,
|
|
717
|
+
role: params.role
|
|
718
|
+
})).rows.map((row) => ({
|
|
719
|
+
identity: `SHARE::${readStringField(row, "name", "NAME") ?? ""}`,
|
|
720
|
+
signature: signatureOf(row, SHARE_SIGNATURE_FIELDS),
|
|
721
|
+
row
|
|
722
|
+
}));
|
|
723
|
+
const previousSnapshot = ctx.lastResponse?.snapshot;
|
|
724
|
+
const detectedAt = isoNow();
|
|
725
|
+
const prevMap = new Map((previousSnapshot ?? []).map((p) => [p.identity, p]));
|
|
726
|
+
const currMap = new Map(current.map((c) => [c.identity, c]));
|
|
727
|
+
const events = [];
|
|
728
|
+
if (previousSnapshot !== void 0) {
|
|
729
|
+
for (const entry of current) {
|
|
730
|
+
const prev = prevMap.get(entry.identity);
|
|
731
|
+
if (prev === void 0) events.push({
|
|
732
|
+
name: readStringField(entry.row, "name", "NAME") ?? "",
|
|
733
|
+
kind: readStringField(entry.row, "kind", "KIND"),
|
|
734
|
+
change: "created",
|
|
735
|
+
identity: entry.identity,
|
|
736
|
+
detectedAt,
|
|
737
|
+
currentRow: entry.row
|
|
738
|
+
});
|
|
739
|
+
else if (prev.signature !== entry.signature) events.push({
|
|
740
|
+
name: readStringField(entry.row, "name", "NAME") ?? "",
|
|
741
|
+
kind: readStringField(entry.row, "kind", "KIND"),
|
|
742
|
+
change: "modified",
|
|
743
|
+
identity: entry.identity,
|
|
744
|
+
detectedAt,
|
|
745
|
+
currentRow: entry.row
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
for (const prev of previousSnapshot) if (!currMap.has(prev.identity)) events.push({
|
|
749
|
+
name: prev.identity.replace(/^SHARE::/u, ""),
|
|
750
|
+
change: "dropped",
|
|
751
|
+
identity: prev.identity,
|
|
752
|
+
detectedAt
|
|
753
|
+
});
|
|
754
|
+
} else if (params.emitInitialSnapshot) for (const entry of current) events.push({
|
|
755
|
+
name: readStringField(entry.row, "name", "NAME") ?? "",
|
|
756
|
+
kind: readStringField(entry.row, "kind", "KIND"),
|
|
757
|
+
change: "created",
|
|
758
|
+
identity: entry.identity,
|
|
759
|
+
detectedAt,
|
|
760
|
+
currentRow: entry.row
|
|
761
|
+
});
|
|
762
|
+
return {
|
|
763
|
+
events,
|
|
764
|
+
snapshot: toSnapshotEntries(current),
|
|
765
|
+
fetchedAt: detectedAt
|
|
766
|
+
};
|
|
767
|
+
},
|
|
768
|
+
idempotencyKey: (response) => sortedIdempotencyKey("share-changed", response.events),
|
|
769
|
+
hasPayload: (response) => response.events.length > 0
|
|
770
|
+
});
|
|
771
|
+
function shareChanged(input) {
|
|
772
|
+
return bindSnowflakePollingTrigger(shareChangedDefinition, {
|
|
773
|
+
warehouse: input?.warehouse,
|
|
774
|
+
role: input?.role,
|
|
775
|
+
like: input?.like,
|
|
776
|
+
emitInitialSnapshot: input?.emitInitialSnapshot
|
|
777
|
+
}, input);
|
|
778
|
+
}
|
|
779
|
+
const grantsChangedBatchSchema = buildDiffBatchSchema(grantChangedEventSchema);
|
|
780
|
+
function grantIdentity(row) {
|
|
781
|
+
return `${readStringField(row, "privilege", "PRIVILEGE") ?? ""}::${readStringField(row, "granted_on", "GRANTED_ON") ?? ""}::${readStringField(row, "name", "NAME") ?? ""}::${readStringField(row, "granted_to", "GRANTED_TO") ?? ""}::${readStringField(row, "grantee_name", "GRANTEE_NAME") ?? ""}`;
|
|
782
|
+
}
|
|
783
|
+
function grantStatement(params) {
|
|
784
|
+
if ("role" in params.grantee) return `SHOW GRANTS TO ROLE ${quoteIdentifier(params.grantee.role)}`;
|
|
785
|
+
if ("user" in params.grantee) return `SHOW GRANTS TO USER ${quoteIdentifier(params.grantee.user)}`;
|
|
786
|
+
return `SHOW GRANTS TO SHARE ${quoteIdentifier(params.grantee.share)}`;
|
|
787
|
+
}
|
|
788
|
+
const grantsChangedDefinition = defineSnowflakePollingTrigger({
|
|
789
|
+
responseSchema: grantsChangedBatchSchema,
|
|
790
|
+
defaultName: "Snowflake Grants Changed",
|
|
791
|
+
defaultDescription: "Emits grant / revoke events observed via SHOW GRANTS TO <ROLE|USER|SHARE>. Diffs against the prior poll by (privilege, granted_on, name, grantee).",
|
|
792
|
+
defaultSchedule: "5m",
|
|
793
|
+
poll: async (params, credentials, ctx) => {
|
|
794
|
+
const current = (await executeSql(createSnowflakeClient(credentials), {
|
|
795
|
+
statement: grantStatement(params),
|
|
796
|
+
warehouse: params.warehouse,
|
|
797
|
+
role: params.sessionRole
|
|
798
|
+
})).rows.map((row) => ({
|
|
799
|
+
identity: grantIdentity(row),
|
|
800
|
+
row
|
|
801
|
+
}));
|
|
802
|
+
const previousSnapshot = ctx.lastResponse?.snapshot;
|
|
803
|
+
const detectedAt = isoNow();
|
|
804
|
+
const events = [];
|
|
805
|
+
if (previousSnapshot !== void 0) {
|
|
806
|
+
const prevSet = new Set(previousSnapshot.map((p) => p.identity));
|
|
807
|
+
const currSet = new Set(current.map((c) => c.identity));
|
|
808
|
+
for (const entry of current) if (!prevSet.has(entry.identity)) events.push(buildGrantEvent(entry.row, "granted", entry.identity, detectedAt));
|
|
809
|
+
for (const prev of previousSnapshot) if (!currSet.has(prev.identity)) {
|
|
810
|
+
const [privilege, grantedOn, name, granteeType, granteeName] = prev.identity.split("::");
|
|
811
|
+
events.push({
|
|
812
|
+
privilege: privilege ?? "",
|
|
813
|
+
grantedOn: grantedOn ?? "",
|
|
814
|
+
objectName: name ?? "",
|
|
815
|
+
granteeType,
|
|
816
|
+
granteeName: granteeName ?? "",
|
|
817
|
+
change: "revoked",
|
|
818
|
+
identity: prev.identity,
|
|
819
|
+
detectedAt,
|
|
820
|
+
row: {}
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
return {
|
|
825
|
+
events,
|
|
826
|
+
snapshot: toSnapshotEntries(current),
|
|
827
|
+
fetchedAt: detectedAt
|
|
828
|
+
};
|
|
829
|
+
},
|
|
830
|
+
idempotencyKey: (response) => sortedIdempotencyKey("grants-changed", response.events),
|
|
831
|
+
hasPayload: (response) => response.events.length > 0
|
|
832
|
+
});
|
|
833
|
+
function buildGrantEvent(row, change, identity, detectedAt) {
|
|
834
|
+
return {
|
|
835
|
+
privilege: readStringField(row, "privilege", "PRIVILEGE") ?? "",
|
|
836
|
+
grantedOn: readStringField(row, "granted_on", "GRANTED_ON") ?? "",
|
|
837
|
+
objectName: readStringField(row, "name", "NAME") ?? "",
|
|
838
|
+
granteeName: readStringField(row, "grantee_name", "GRANTEE_NAME") ?? "",
|
|
839
|
+
granteeType: readStringField(row, "granted_to", "GRANTED_TO"),
|
|
840
|
+
change,
|
|
841
|
+
identity,
|
|
842
|
+
detectedAt,
|
|
843
|
+
row
|
|
844
|
+
};
|
|
845
|
+
}
|
|
846
|
+
function grantsChanged(input) {
|
|
847
|
+
return bindSnowflakePollingTrigger(grantsChangedDefinition, input, input);
|
|
848
|
+
}
|
|
849
|
+
const warehouseCreditsBatchSchema = z.object({
|
|
850
|
+
events: z.array(warehouseCreditsEventSchema),
|
|
851
|
+
highWaterValue: z.string().nullable(),
|
|
852
|
+
fetchedAt: z.string()
|
|
853
|
+
}).catchall(z.unknown());
|
|
854
|
+
const warehouseQueueBatchSchema = z.object({
|
|
855
|
+
events: z.array(warehouseQueueEventSchema),
|
|
856
|
+
highWaterValue: z.string().nullable(),
|
|
857
|
+
fetchedAt: z.string()
|
|
858
|
+
}).catchall(z.unknown());
|
|
859
|
+
const warehouseCreditsUsageDefinition = defineSnowflakePollingTrigger({
|
|
860
|
+
responseSchema: warehouseCreditsBatchSchema,
|
|
861
|
+
defaultName: "Snowflake Warehouse Credits Usage",
|
|
862
|
+
defaultDescription: "Tails INFORMATION_SCHEMA.WAREHOUSE_METERING_HISTORY and emits per-warehouse credit buckets above an optional minimum.",
|
|
863
|
+
defaultSchedule: "5m",
|
|
864
|
+
poll: async (params, credentials, ctx) => {
|
|
865
|
+
const client = createSnowflakeClient(credentials);
|
|
866
|
+
const limit = params.limit ?? 200;
|
|
867
|
+
const priorHighWater = ctx.lastResponse?.highWaterValue ?? null;
|
|
868
|
+
const lookbackMinutes = params.lookbackMinutes ?? 60;
|
|
869
|
+
const args = [`DATE_RANGE_START => TO_TIMESTAMP_LTZ(${quoteLiteral(priorHighWater ?? (/* @__PURE__ */ new Date(Date.now() - lookbackMinutes * 6e4)).toISOString())})`];
|
|
870
|
+
if (params.warehouseName) args.push(`WAREHOUSE_NAME => ${quoteLiteral(params.warehouseName)}`);
|
|
871
|
+
const clauses = [];
|
|
872
|
+
if (params.minimum !== void 0) clauses.push(`CREDITS_USED >= ${Number(params.minimum)}`);
|
|
873
|
+
const where = clauses.length > 0 ? ` WHERE ${clauses.join(" AND ")}` : "";
|
|
874
|
+
const rows = (await executeSql(client, {
|
|
875
|
+
statement: `SELECT * FROM TABLE(INFORMATION_SCHEMA.WAREHOUSE_METERING_HISTORY(${args.join(", ")}))${where} ORDER BY END_TIME ASC LIMIT ${Number(limit)}`,
|
|
876
|
+
warehouse: params.warehouse,
|
|
877
|
+
role: params.role,
|
|
878
|
+
database: params.database,
|
|
879
|
+
schema: params.schema
|
|
880
|
+
})).rows;
|
|
881
|
+
const events = rows.map((row) => ({
|
|
882
|
+
warehouseName: readStringField(row, "WAREHOUSE_NAME", "warehouse_name") ?? "",
|
|
883
|
+
startTime: readStringField(row, "START_TIME", "start_time"),
|
|
884
|
+
endTime: readStringField(row, "END_TIME", "end_time"),
|
|
885
|
+
creditsUsed: readNumberField(row, "CREDITS_USED", "credits_used") ?? null,
|
|
886
|
+
creditsUsedCompute: readNumberField(row, "CREDITS_USED_COMPUTE", "credits_used_compute") ?? null,
|
|
887
|
+
creditsUsedCloudServices: readNumberField(row, "CREDITS_USED_CLOUD_SERVICES", "credits_used_cloud_services") ?? null,
|
|
888
|
+
row
|
|
889
|
+
}));
|
|
890
|
+
const last = rows[rows.length - 1];
|
|
891
|
+
return {
|
|
892
|
+
events,
|
|
893
|
+
highWaterValue: readStringField(last ?? {}, "END_TIME", "end_time") ?? priorHighWater,
|
|
894
|
+
fetchedAt: isoNow()
|
|
895
|
+
};
|
|
896
|
+
},
|
|
897
|
+
idempotencyKey: (response) => `wh-credits:${response.highWaterValue ?? "initial"}:${response.events.length}`,
|
|
898
|
+
hasPayload: (response) => response.events.length > 0
|
|
899
|
+
});
|
|
900
|
+
const warehouseQueueDepthDefinition = defineSnowflakePollingTrigger({
|
|
901
|
+
responseSchema: warehouseQueueBatchSchema,
|
|
902
|
+
defaultName: "Snowflake Warehouse Queue Depth",
|
|
903
|
+
defaultDescription: "Tails INFORMATION_SCHEMA.WAREHOUSE_LOAD_HISTORY and emits per-interval queue depth above an optional minimum.",
|
|
904
|
+
defaultSchedule: "5m",
|
|
905
|
+
poll: async (params, credentials, ctx) => {
|
|
906
|
+
const client = createSnowflakeClient(credentials);
|
|
907
|
+
const limit = params.limit ?? 200;
|
|
908
|
+
const priorHighWater = ctx.lastResponse?.highWaterValue ?? null;
|
|
909
|
+
const lookbackMinutes = params.lookbackMinutes ?? 60;
|
|
910
|
+
const args = [`DATE_RANGE_START => TO_TIMESTAMP_LTZ(${quoteLiteral(priorHighWater ?? (/* @__PURE__ */ new Date(Date.now() - lookbackMinutes * 6e4)).toISOString())})`];
|
|
911
|
+
if (params.warehouseName) args.push(`WAREHOUSE_NAME => ${quoteLiteral(params.warehouseName)}`);
|
|
912
|
+
const clauses = [];
|
|
913
|
+
if (params.minimum !== void 0) clauses.push(`AVG_QUEUED_LOAD >= ${Number(params.minimum)}`);
|
|
914
|
+
const where = clauses.length > 0 ? ` WHERE ${clauses.join(" AND ")}` : "";
|
|
915
|
+
const rows = (await executeSql(client, {
|
|
916
|
+
statement: `SELECT * FROM TABLE(INFORMATION_SCHEMA.WAREHOUSE_LOAD_HISTORY(${args.join(", ")}))${where} ORDER BY END_TIME ASC LIMIT ${Number(limit)}`,
|
|
917
|
+
warehouse: params.warehouse,
|
|
918
|
+
role: params.role,
|
|
919
|
+
database: params.database,
|
|
920
|
+
schema: params.schema
|
|
921
|
+
})).rows;
|
|
922
|
+
const events = rows.map((row) => ({
|
|
923
|
+
warehouseName: readStringField(row, "WAREHOUSE_NAME", "warehouse_name") ?? "",
|
|
924
|
+
startTime: readStringField(row, "START_TIME", "start_time"),
|
|
925
|
+
endTime: readStringField(row, "END_TIME", "end_time"),
|
|
926
|
+
avgRunning: readNumberField(row, "AVG_RUNNING", "avg_running") ?? null,
|
|
927
|
+
avgQueuedLoad: readNumberField(row, "AVG_QUEUED_LOAD", "avg_queued_load") ?? null,
|
|
928
|
+
avgQueuedProvisioning: readNumberField(row, "AVG_QUEUED_PROVISIONING", "avg_queued_provisioning") ?? null,
|
|
929
|
+
avgBlocked: readNumberField(row, "AVG_BLOCKED", "avg_blocked") ?? null,
|
|
930
|
+
row
|
|
931
|
+
}));
|
|
932
|
+
const last = rows[rows.length - 1];
|
|
933
|
+
return {
|
|
934
|
+
events,
|
|
935
|
+
highWaterValue: readStringField(last ?? {}, "END_TIME", "end_time") ?? priorHighWater,
|
|
936
|
+
fetchedAt: isoNow()
|
|
937
|
+
};
|
|
938
|
+
},
|
|
939
|
+
idempotencyKey: (response) => `wh-queue:${response.highWaterValue ?? "initial"}:${response.events.length}`,
|
|
940
|
+
hasPayload: (response) => response.events.length > 0
|
|
941
|
+
});
|
|
942
|
+
function warehouseCreditsUsage(input) {
|
|
943
|
+
return bindSnowflakePollingTrigger(warehouseCreditsUsageDefinition, input ?? {}, input);
|
|
944
|
+
}
|
|
945
|
+
function warehouseQueueDepth(input) {
|
|
946
|
+
return bindSnowflakePollingTrigger(warehouseQueueDepthDefinition, input ?? {}, input);
|
|
947
|
+
}
|
|
948
|
+
const statusPageIncidentBatchSchema = buildDiffBatchSchema(statusPageIncidentEventSchema);
|
|
949
|
+
const statusPageComponentChangeBatchSchema = buildDiffBatchSchema(statusPageComponentChangeEventSchema);
|
|
950
|
+
function incidentToEntry(incident) {
|
|
951
|
+
const row = incident;
|
|
952
|
+
return {
|
|
953
|
+
identity: `INCIDENT::${incident.id}`,
|
|
954
|
+
signature: `${incident.status}|${incident.updated_at}`,
|
|
955
|
+
row
|
|
956
|
+
};
|
|
957
|
+
}
|
|
958
|
+
function componentToEntry(component) {
|
|
959
|
+
const row = component;
|
|
960
|
+
return {
|
|
961
|
+
identity: `COMPONENT::${component.id}`,
|
|
962
|
+
signature: component.status,
|
|
963
|
+
row
|
|
964
|
+
};
|
|
965
|
+
}
|
|
966
|
+
function extractStatusPageOptions(params) {
|
|
967
|
+
const opts = {};
|
|
968
|
+
if (params.baseUrl !== void 0) opts.baseUrl = params.baseUrl;
|
|
969
|
+
if (params.fetchImpl !== void 0) opts.fetchImpl = params.fetchImpl;
|
|
970
|
+
if (params.timeoutMs !== void 0) opts.timeoutMs = params.timeoutMs;
|
|
971
|
+
if (params.headers !== void 0) opts.headers = params.headers;
|
|
972
|
+
if (params.signal !== void 0) opts.signal = params.signal;
|
|
973
|
+
return opts;
|
|
974
|
+
}
|
|
975
|
+
const statusPageIncidentDefinition = {
|
|
976
|
+
responseSchema: statusPageIncidentBatchSchema,
|
|
977
|
+
defaultName: "Snowflake Status Page Incident",
|
|
978
|
+
defaultDescription: "Polls https://status.snowflake.com/api/v2/incidents/unresolved.json and emits newly-appearing or status-changed incidents.",
|
|
979
|
+
defaultSchedule: "5m",
|
|
980
|
+
poll: async (params, _credentials, ctx) => {
|
|
981
|
+
const current = (await getUnresolvedIncidents(extractStatusPageOptions(params))).incidents.map(incidentToEntry);
|
|
982
|
+
const previousSnapshot = ctx.lastResponse?.snapshot;
|
|
983
|
+
const detectedAt = isoNow();
|
|
984
|
+
const events = [];
|
|
985
|
+
const prevMap = new Map((previousSnapshot ?? []).map((p) => [p.identity, p]));
|
|
986
|
+
const emitAll = previousSnapshot === void 0 && params.emitInitialSnapshot;
|
|
987
|
+
if (previousSnapshot === void 0 && !emitAll) {} else for (const entry of current) {
|
|
988
|
+
const prev = prevMap.get(entry.identity);
|
|
989
|
+
if (prev === void 0 || prev.signature !== entry.signature) {
|
|
990
|
+
const incident = entry.row;
|
|
991
|
+
events.push({
|
|
992
|
+
id: incident.id,
|
|
993
|
+
name: incident.name,
|
|
994
|
+
status: incident.status,
|
|
995
|
+
impact: incident.impact,
|
|
996
|
+
createdAt: incident.created_at,
|
|
997
|
+
updatedAt: incident.updated_at,
|
|
998
|
+
resolvedAt: incident.resolved_at ?? null,
|
|
999
|
+
shortlink: incident.shortlink,
|
|
1000
|
+
detectedAt,
|
|
1001
|
+
raw: entry.row
|
|
1002
|
+
});
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
return {
|
|
1006
|
+
events,
|
|
1007
|
+
snapshot: toSnapshotEntries(current),
|
|
1008
|
+
fetchedAt: detectedAt
|
|
1009
|
+
};
|
|
1010
|
+
},
|
|
1011
|
+
idempotencyKey: (response) => sortedIdempotencyKey("status-incident", response.events, (e) => `${e.id}:${e.updatedAt ?? ""}`),
|
|
1012
|
+
hasPayload: (response) => response.events.length > 0
|
|
1013
|
+
};
|
|
1014
|
+
const statusPageComponentChangeDefinition = {
|
|
1015
|
+
responseSchema: statusPageComponentChangeBatchSchema,
|
|
1016
|
+
defaultName: "Snowflake Status Page Component Changed",
|
|
1017
|
+
defaultDescription: "Polls https://status.snowflake.com/api/v2/summary.json and emits a change event when any component transitions status.",
|
|
1018
|
+
defaultSchedule: "2m",
|
|
1019
|
+
poll: async (params, _credentials, ctx) => {
|
|
1020
|
+
const current = ((await getStatusSummary(extractStatusPageOptions(params))).components ?? []).map(componentToEntry);
|
|
1021
|
+
const previousSnapshot = ctx.lastResponse?.snapshot;
|
|
1022
|
+
const detectedAt = isoNow();
|
|
1023
|
+
const events = [];
|
|
1024
|
+
if (previousSnapshot !== void 0) {
|
|
1025
|
+
const prevMap = new Map(previousSnapshot.map((p) => [p.identity, p]));
|
|
1026
|
+
for (const entry of current) {
|
|
1027
|
+
const prevStatus = prevMap.get(entry.identity)?.signature ?? null;
|
|
1028
|
+
if (prevStatus !== entry.signature) {
|
|
1029
|
+
const component = entry.row;
|
|
1030
|
+
events.push({
|
|
1031
|
+
id: component.id,
|
|
1032
|
+
name: component.name,
|
|
1033
|
+
previousStatus: prevStatus,
|
|
1034
|
+
currentStatus: component.status,
|
|
1035
|
+
description: component.description ?? void 0,
|
|
1036
|
+
changedAt: component.updated_at ?? detectedAt,
|
|
1037
|
+
detectedAt,
|
|
1038
|
+
raw: entry.row
|
|
1039
|
+
});
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
return {
|
|
1044
|
+
events,
|
|
1045
|
+
snapshot: toSnapshotEntries(current),
|
|
1046
|
+
fetchedAt: detectedAt
|
|
1047
|
+
};
|
|
1048
|
+
},
|
|
1049
|
+
idempotencyKey: (response) => sortedIdempotencyKey("status-component", response.events, (e) => `${e.id}:${e.currentStatus}`),
|
|
1050
|
+
hasPayload: (response) => response.events.length > 0
|
|
1051
|
+
};
|
|
1052
|
+
/**
|
|
1053
|
+
* Status-page pollers don't need Snowflake credentials, but they still
|
|
1054
|
+
* flow through the Snowflake binding factory so they show up in the
|
|
1055
|
+
* polling manifest alongside the rest. Credentials are declared but
|
|
1056
|
+
* unused — the poll callback ignores `ctx.credentials.snowflake`.
|
|
1057
|
+
*/
|
|
1058
|
+
function statusPageIncident(input) {
|
|
1059
|
+
return bindSnowflakePollingTrigger(statusPageIncidentDefinition, input ?? {}, input);
|
|
1060
|
+
}
|
|
1061
|
+
function statusPageComponentChanged(input) {
|
|
1062
|
+
return bindSnowflakePollingTrigger(statusPageComponentChangeDefinition, input ?? {}, input);
|
|
1063
|
+
}
|
|
1064
|
+
function resolveWebhookPath(override, fallback) {
|
|
1065
|
+
return override ?? fallback;
|
|
1066
|
+
}
|
|
1067
|
+
function isoNowUtc() {
|
|
1068
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
1069
|
+
}
|
|
1070
|
+
/**
|
|
1071
|
+
* The public SNS payload schema accepted by this trigger. SNS delivers
|
|
1072
|
+
* an application/json body; we validate it before signature-checking.
|
|
1073
|
+
*/
|
|
1074
|
+
const snsWebhookPayloadSchema = snsNotificationSchema;
|
|
1075
|
+
function s3RecordsToBatch(snsMessage, rawBody) {
|
|
1076
|
+
const receivedAt = isoNowUtc();
|
|
1077
|
+
return {
|
|
1078
|
+
events: extractS3Records(snsMessage).map((record) => {
|
|
1079
|
+
const bucket = record.s3?.bucket?.name ?? "";
|
|
1080
|
+
const key = record.s3?.object?.key ?? "";
|
|
1081
|
+
return {
|
|
1082
|
+
provider: "aws-s3",
|
|
1083
|
+
eventType: record.eventName ?? "",
|
|
1084
|
+
eventTime: record.eventTime ?? receivedAt,
|
|
1085
|
+
bucket,
|
|
1086
|
+
key,
|
|
1087
|
+
size: typeof record.s3?.object?.size === "number" ? record.s3.object.size : null,
|
|
1088
|
+
etag: record.s3?.object?.eTag ?? null,
|
|
1089
|
+
region: record.awsRegion ?? null,
|
|
1090
|
+
sourceIp: record.requestParameters?.sourceIPAddress ?? null,
|
|
1091
|
+
messageId: snsMessage.MessageId,
|
|
1092
|
+
receivedAt,
|
|
1093
|
+
raw: record
|
|
1094
|
+
};
|
|
1095
|
+
}),
|
|
1096
|
+
provider: "aws-s3",
|
|
1097
|
+
receivedAt,
|
|
1098
|
+
messageId: snsMessage.MessageId,
|
|
1099
|
+
topic: snsMessage.TopicArn,
|
|
1100
|
+
raw: JSON.parse(rawBody)
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
1103
|
+
/**
|
|
1104
|
+
* `triggers.autoIngestS3Sns` — receives AWS SNS HTTPS deliveries,
|
|
1105
|
+
* verifies the SNS signature against the signing certificate, and
|
|
1106
|
+
* emits normalized S3 object events. Subscription-confirmation
|
|
1107
|
+
* messages are surfaced via a sentinel event of type
|
|
1108
|
+
* `SubscriptionConfirmation` so operators can auto-confirm in
|
|
1109
|
+
* downstream logic.
|
|
1110
|
+
*/
|
|
1111
|
+
function autoIngestS3Sns(input) {
|
|
1112
|
+
const { verifierOptions, path, ...rest } = input ?? {};
|
|
1113
|
+
return createWebhookTriggerBindingFactory({
|
|
1114
|
+
defaultName: "Snowflake Auto-Ingest — AWS SNS",
|
|
1115
|
+
defaultDescription: "Receives Snowpipe auto-ingest notifications via AWS SNS HTTPS delivery. Verifies SNS signature against the SigningCertURL, pins TopicArn, and normalizes S3 object events.",
|
|
1116
|
+
path: resolveWebhookPath(path, "/snowflake/auto-ingest/s3-sns"),
|
|
1117
|
+
method: "POST",
|
|
1118
|
+
payload: snsWebhookPayloadSchema,
|
|
1119
|
+
credentialSets: [snowflake, snowflakeS3SnsWebhookCredentialSet],
|
|
1120
|
+
response: { successStatus: 200 },
|
|
1121
|
+
verify: async (request, ctx) => {
|
|
1122
|
+
const creds = ctx.credentials["snowflake-s3-sns-webhook"];
|
|
1123
|
+
try {
|
|
1124
|
+
await verifySnsMessage(request.rawBody, {
|
|
1125
|
+
...creds.expectedTopicArn ? { expectedTopicArn: creds.expectedTopicArn } : {},
|
|
1126
|
+
...creds.allowedSigningCertHostPattern ? { allowedSigningCertHostPattern: creds.allowedSigningCertHostPattern } : {},
|
|
1127
|
+
...verifierOptions ?? {}
|
|
1128
|
+
});
|
|
1129
|
+
} catch (err) {
|
|
1130
|
+
if (err instanceof SnowflakeWebhookVerificationError) throw err;
|
|
1131
|
+
throw new SnowflakeWebhookVerificationError({
|
|
1132
|
+
message: "Unexpected error verifying SNS signature.",
|
|
1133
|
+
reason: "unknown",
|
|
1134
|
+
provider: "aws-s3",
|
|
1135
|
+
cause: err
|
|
1136
|
+
});
|
|
1137
|
+
}
|
|
1138
|
+
},
|
|
1139
|
+
mapPayload: (payload) => {
|
|
1140
|
+
if (payload.Type !== "Notification") return null;
|
|
1141
|
+
const batch = s3RecordsToBatch(payload, JSON.stringify(payload));
|
|
1142
|
+
return batch.events.length > 0 ? batch : null;
|
|
1143
|
+
},
|
|
1144
|
+
idempotencyKey: (_payload, mapped) => mapped ? `sns:${mapped.messageId ?? "no-id"}` : void 0
|
|
1145
|
+
})(rest);
|
|
1146
|
+
}
|
|
1147
|
+
const gcsWebhookPayloadSchema = gcsPubSubPushSchema;
|
|
1148
|
+
function gcsPushToBatch(push) {
|
|
1149
|
+
const receivedAt = isoNowUtc();
|
|
1150
|
+
const payload = extractGcsObjectPayload(push);
|
|
1151
|
+
const attributes = push.message.attributes ?? {};
|
|
1152
|
+
if (!payload || !payload.bucket || !payload.name) return {
|
|
1153
|
+
events: [],
|
|
1154
|
+
provider: "gcp-gcs",
|
|
1155
|
+
receivedAt,
|
|
1156
|
+
messageId: push.message.messageId ?? null,
|
|
1157
|
+
topic: push.subscription ?? null,
|
|
1158
|
+
raw: push
|
|
1159
|
+
};
|
|
1160
|
+
const eventType = attributes.eventType ?? "OBJECT_FINALIZE";
|
|
1161
|
+
const sizeNumber = typeof payload.size === "number" ? payload.size : typeof payload.size === "string" ? Number(payload.size) : null;
|
|
1162
|
+
return {
|
|
1163
|
+
events: [{
|
|
1164
|
+
provider: "gcp-gcs",
|
|
1165
|
+
eventType,
|
|
1166
|
+
eventTime: payload.updated ?? payload.timeCreated ?? receivedAt,
|
|
1167
|
+
bucket: payload.bucket,
|
|
1168
|
+
key: payload.name,
|
|
1169
|
+
size: sizeNumber !== null && Number.isFinite(sizeNumber) ? sizeNumber : null,
|
|
1170
|
+
etag: payload.etag ?? null,
|
|
1171
|
+
region: null,
|
|
1172
|
+
sourceIp: null,
|
|
1173
|
+
messageId: push.message.messageId ?? null,
|
|
1174
|
+
receivedAt,
|
|
1175
|
+
raw: payload
|
|
1176
|
+
}],
|
|
1177
|
+
provider: "gcp-gcs",
|
|
1178
|
+
receivedAt,
|
|
1179
|
+
messageId: push.message.messageId ?? null,
|
|
1180
|
+
topic: push.subscription ?? null,
|
|
1181
|
+
raw: push
|
|
1182
|
+
};
|
|
1183
|
+
}
|
|
1184
|
+
/**
|
|
1185
|
+
* `triggers.autoIngestGcsPubSub` — receives GCP Pub/Sub push deliveries,
|
|
1186
|
+
* verifies the Google-signed OIDC JWT against Google's JWKS, and
|
|
1187
|
+
* emits normalized GCS object events extracted from the Pub/Sub
|
|
1188
|
+
* message body.
|
|
1189
|
+
*/
|
|
1190
|
+
function autoIngestGcsPubSub(input) {
|
|
1191
|
+
const { verifierOptions, path, ...rest } = input ?? {};
|
|
1192
|
+
return createWebhookTriggerBindingFactory({
|
|
1193
|
+
defaultName: "Snowflake Auto-Ingest — GCS Pub/Sub",
|
|
1194
|
+
defaultDescription: "Receives Snowpipe auto-ingest notifications via GCP Pub/Sub push. Verifies the Google-signed OIDC JWT in the Authorization header and normalizes GCS object events.",
|
|
1195
|
+
path: resolveWebhookPath(path, "/snowflake/auto-ingest/gcs-pubsub"),
|
|
1196
|
+
method: "POST",
|
|
1197
|
+
payload: gcsWebhookPayloadSchema,
|
|
1198
|
+
credentialSets: [snowflake, snowflakeGcsPubSubWebhookCredentialSet],
|
|
1199
|
+
response: { successStatus: 204 },
|
|
1200
|
+
verify: async (request, ctx) => {
|
|
1201
|
+
const creds = ctx.credentials["snowflake-gcs-pubsub-webhook"];
|
|
1202
|
+
try {
|
|
1203
|
+
parsePubSubPush(request.rawBody);
|
|
1204
|
+
await verifyPubSubPushAuthorization(request.headers.authorization, {
|
|
1205
|
+
expectedAudience: creds.expectedAudience,
|
|
1206
|
+
expectedServiceAccount: creds.expectedServiceAccount,
|
|
1207
|
+
...creds.issuer ? { issuer: creds.issuer } : {},
|
|
1208
|
+
...creds.jwksUri ? { jwksUri: creds.jwksUri } : {},
|
|
1209
|
+
...verifierOptions ?? {}
|
|
1210
|
+
});
|
|
1211
|
+
} catch (err) {
|
|
1212
|
+
if (err instanceof SnowflakeWebhookVerificationError) throw err;
|
|
1213
|
+
throw new SnowflakeWebhookVerificationError({
|
|
1214
|
+
message: "Unexpected error verifying Pub/Sub push.",
|
|
1215
|
+
reason: "unknown",
|
|
1216
|
+
provider: "gcp-gcs",
|
|
1217
|
+
cause: err
|
|
1218
|
+
});
|
|
1219
|
+
}
|
|
1220
|
+
},
|
|
1221
|
+
mapPayload: (payload) => {
|
|
1222
|
+
const batch = gcsPushToBatch(payload);
|
|
1223
|
+
return batch.events.length > 0 ? batch : null;
|
|
1224
|
+
},
|
|
1225
|
+
idempotencyKey: (_payload, mapped) => mapped ? `pubsub:${mapped.messageId ?? "no-id"}` : void 0
|
|
1226
|
+
})(rest);
|
|
1227
|
+
}
|
|
1228
|
+
/**
|
|
1229
|
+
* Event Grid wire format is `AzureEventGridEvent[]`. Keystroke's
|
|
1230
|
+
* webhook primitive validates a single payload schema — so we accept
|
|
1231
|
+
* either a single event or a wrapper object. The runtime reparses the
|
|
1232
|
+
* raw body to preserve the array shape.
|
|
1233
|
+
*/
|
|
1234
|
+
const azureWebhookPayloadSchema = z.union([z.array(azureEventGridEventSchema), azureEventGridEventSchema]);
|
|
1235
|
+
function azureBatchToBatch(events, rawBody) {
|
|
1236
|
+
const receivedAt = isoNowUtc();
|
|
1237
|
+
const mapped = [];
|
|
1238
|
+
let topic = null;
|
|
1239
|
+
for (const event of events) {
|
|
1240
|
+
if (event.eventType === EVENT_GRID_VALIDATION_EVENT_TYPE) continue;
|
|
1241
|
+
topic = event.topic ?? topic;
|
|
1242
|
+
const loc = parseAzureBlobLocation(event);
|
|
1243
|
+
const data = event.data;
|
|
1244
|
+
mapped.push({
|
|
1245
|
+
provider: "azure-blob",
|
|
1246
|
+
eventType: event.eventType,
|
|
1247
|
+
eventTime: event.eventTime,
|
|
1248
|
+
bucket: loc.bucket,
|
|
1249
|
+
key: loc.key,
|
|
1250
|
+
size: typeof data.contentLength === "number" ? data.contentLength : null,
|
|
1251
|
+
etag: typeof data.eTag === "string" ? data.eTag : null,
|
|
1252
|
+
region: null,
|
|
1253
|
+
sourceIp: null,
|
|
1254
|
+
messageId: event.id,
|
|
1255
|
+
receivedAt,
|
|
1256
|
+
raw: event
|
|
1257
|
+
});
|
|
1258
|
+
}
|
|
1259
|
+
return {
|
|
1260
|
+
events: mapped,
|
|
1261
|
+
provider: "azure-blob",
|
|
1262
|
+
receivedAt,
|
|
1263
|
+
messageId: events[0]?.id ?? null,
|
|
1264
|
+
topic,
|
|
1265
|
+
raw: JSON.parse(rawBody)
|
|
1266
|
+
};
|
|
1267
|
+
}
|
|
1268
|
+
/**
|
|
1269
|
+
* `triggers.autoIngestAzureEventGrid` — receives Azure Event Grid
|
|
1270
|
+
* deliveries. Validates shared-secret header (if configured), parses
|
|
1271
|
+
* the batch, and emits normalized Azure Blob object events. The
|
|
1272
|
+
* synchronous subscription-validation handshake is NOT supported
|
|
1273
|
+
* through the Keystroke static-response primitive; operators must
|
|
1274
|
+
* use manual validation URLs (see IMPLEMENTATION_NOTES.md).
|
|
1275
|
+
*/
|
|
1276
|
+
function autoIngestAzureEventGrid(input) {
|
|
1277
|
+
const { verifierOptions, path, ...rest } = input ?? {};
|
|
1278
|
+
return createWebhookTriggerBindingFactory({
|
|
1279
|
+
defaultName: "Snowflake Auto-Ingest — Azure Event Grid",
|
|
1280
|
+
defaultDescription: "Receives Snowpipe auto-ingest notifications via Azure Event Grid. Validates shared-secret header, drops subscription-validation events (manual handshake required), and normalizes Azure Blob object events.",
|
|
1281
|
+
path: resolveWebhookPath(path, "/snowflake/auto-ingest/azure-eventgrid"),
|
|
1282
|
+
method: "POST",
|
|
1283
|
+
payload: azureWebhookPayloadSchema,
|
|
1284
|
+
credentialSets: [snowflake, snowflakeAzureEventGridWebhookCredentialSet],
|
|
1285
|
+
response: { successStatus: 200 },
|
|
1286
|
+
verify: async (request, ctx) => {
|
|
1287
|
+
const creds = ctx.credentials["snowflake-azure-eventgrid-webhook"];
|
|
1288
|
+
try {
|
|
1289
|
+
verifyEventGridRequest({
|
|
1290
|
+
rawBody: request.rawBody,
|
|
1291
|
+
headers: request.headers
|
|
1292
|
+
}, {
|
|
1293
|
+
...creds.sharedSecret ? { sharedSecret: creds.sharedSecret } : {},
|
|
1294
|
+
...creds.sharedSecretHeader ? { sharedSecretHeader: creds.sharedSecretHeader } : {},
|
|
1295
|
+
...creds.expectedTopicArn ? { expectedTopic: creds.expectedTopicArn } : {},
|
|
1296
|
+
...verifierOptions ?? {}
|
|
1297
|
+
});
|
|
1298
|
+
} catch (err) {
|
|
1299
|
+
if (err instanceof SnowflakeWebhookVerificationError) throw err;
|
|
1300
|
+
throw new SnowflakeWebhookVerificationError({
|
|
1301
|
+
message: "Unexpected error verifying Event Grid request.",
|
|
1302
|
+
reason: "unknown",
|
|
1303
|
+
provider: "azure-blob",
|
|
1304
|
+
cause: err
|
|
1305
|
+
});
|
|
1306
|
+
}
|
|
1307
|
+
},
|
|
1308
|
+
mapPayload: (payload) => {
|
|
1309
|
+
const events = Array.isArray(payload) ? payload : [payload];
|
|
1310
|
+
const batch = azureBatchToBatch(events, JSON.stringify(events));
|
|
1311
|
+
return batch.events.length > 0 ? batch : null;
|
|
1312
|
+
},
|
|
1313
|
+
idempotencyKey: (_payload, mapped) => mapped ? `eventgrid:${mapped.messageId ?? "no-id"}` : void 0
|
|
1314
|
+
})(rest);
|
|
1315
|
+
}
|
|
1316
|
+
/**
|
|
1317
|
+
* Convenience namespace grouping every polling trigger in this package.
|
|
1318
|
+
* Equivalent to calling the individual factory functions directly.
|
|
1319
|
+
*/
|
|
1320
|
+
const polling = Object.freeze({
|
|
1321
|
+
streamHasNewRows,
|
|
1322
|
+
streamAppendOnly,
|
|
1323
|
+
scheduledSql,
|
|
1324
|
+
scheduledDelta,
|
|
1325
|
+
taskRunCompleted,
|
|
1326
|
+
statementCompleted,
|
|
1327
|
+
pipeLoadCompleted,
|
|
1328
|
+
pipeLoadFailed,
|
|
1329
|
+
userCreated,
|
|
1330
|
+
userDropped,
|
|
1331
|
+
roleCreated,
|
|
1332
|
+
roleDropped,
|
|
1333
|
+
databaseCreated,
|
|
1334
|
+
databaseDropped,
|
|
1335
|
+
tableCreated,
|
|
1336
|
+
tableDropped,
|
|
1337
|
+
shareChanged,
|
|
1338
|
+
grantsChanged,
|
|
1339
|
+
warehouseCreditsUsage,
|
|
1340
|
+
warehouseQueueDepth,
|
|
1341
|
+
statusPageIncident,
|
|
1342
|
+
statusPageComponentChanged
|
|
1343
|
+
});
|
|
1344
|
+
/**
|
|
1345
|
+
* Convenience namespace grouping every auto-ingest webhook trigger.
|
|
1346
|
+
*/
|
|
1347
|
+
const webhooks = Object.freeze({
|
|
1348
|
+
autoIngestS3Sns,
|
|
1349
|
+
autoIngestGcsPubSub,
|
|
1350
|
+
autoIngestAzureEventGrid
|
|
1351
|
+
});
|
|
1352
|
+
/**
|
|
1353
|
+
* Nested `autoIngest` namespace per the spec (§7 PLAN.md) so callers
|
|
1354
|
+
* can reach the three webhook triggers via `triggers.autoIngest.<cloud>`.
|
|
1355
|
+
*/
|
|
1356
|
+
const autoIngest = Object.freeze({
|
|
1357
|
+
s3Sns: autoIngestS3Sns,
|
|
1358
|
+
gcsPubSub: autoIngestGcsPubSub,
|
|
1359
|
+
azureEventGrid: autoIngestAzureEventGrid
|
|
1360
|
+
});
|
|
1361
|
+
|
|
1362
|
+
//#endregion
|
|
1363
|
+
export { autoIngest, autoIngestAzureEventGrid, autoIngestGcsPubSub, autoIngestS3Sns, databaseCreated, databaseDropped, grantsChanged, pipeLoadCompleted, pipeLoadFailed, polling, roleCreated, roleDropped, scheduledDelta, scheduledSql, shareChanged, statementCompleted, statusPageComponentChanged, statusPageIncident, streamAppendOnly, streamHasNewRows, tableCreated, tableDropped, taskRunCompleted, userCreated, userDropped, warehouseCreditsUsage, warehouseQueueDepth, webhooks };
|