@stackbone/cli 0.6.1 → 0.6.3
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/CHANGELOG.md +70 -0
- package/main.js +21239 -15728
- package/migrations/0090_trigger_subscriptions_sample_event.sql +14 -0
- package/migrations/0091_echo_guard_checks.sql +32 -0
- package/migrations/0092_trigger_subscriptions_outlook_new_email.sql +522 -0
- package/migrations/0093_trigger_subscriptions_webhook_token.sql +13 -0
- package/migrations/0094_rag_document_versions.sql +61 -0
- package/migrations/0095_rag_document_version_counters.sql +22 -0
- package/migrations/0096_trigger_subscriptions_target_kind.sql +29 -0
- package/migrations/0098_model_selections_provider_id.sql +23 -0
- package/migrations/0099_memory.sql +35 -0
- package/migrations/0100_memory_call_run.sql +9 -0
- package/package.json +1 -1
- package/stackbone-cli-0.6.3.tgz +0 -0
- package/stackbone-cli-0.6.1.tgz +0 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
-- 0090_trigger_subscriptions_sample_event.sql — the first real event a link received (PLA-1900)
|
|
2
|
+
-- A trigger's declared `sampleData` is the only thing the mapper can derive event fields
|
|
3
|
+
-- from when the trigger publishes no `outputSchema`, and 49 Activepieces polling triggers
|
|
4
|
+
-- declare it as `{}` (Gmail's `gmail_new_email_received` among them). Their links offered
|
|
5
|
+
-- no field to wire. The link now keeps the first real event it delivers, and the mapper
|
|
6
|
+
-- reads that instead.
|
|
7
|
+
--
|
|
8
|
+
-- Written by the intake tick, never by an operator, and only while it is still NULL: the
|
|
9
|
+
-- first event wins and a later one never replaces it. The tick bounds it before writing
|
|
10
|
+
-- (long strings and arrays clipped), because `list()` returns this column for every link.
|
|
11
|
+
-- Nullable: NULL is "no sample yet", which every link that existed before this reads as.
|
|
12
|
+
|
|
13
|
+
ALTER TABLE stackbone_platform.trigger_subscriptions
|
|
14
|
+
ADD COLUMN IF NOT EXISTS sample_event jsonb;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
-- 0091_echo_guard_checks.sql — the runs that asked the self-write guard (PLA-1580)
|
|
2
|
+
-- A workflow that answers a connector trigger and writes back through the same
|
|
3
|
+
-- connection raises the event that trigger reads, so it starts itself again.
|
|
4
|
+
-- `isOwnEcho` stops that, and until now nothing recorded that a run called it:
|
|
5
|
+
-- the guard is a read, and it left no trace. The box now writes one row here per
|
|
6
|
+
-- run and connector the first time a run asks, whatever the guard answers, so the
|
|
7
|
+
-- link-level warning can leave a guarded run out.
|
|
8
|
+
--
|
|
9
|
+
-- One row per (run, connector), never one per call: the warning asks "did this run
|
|
10
|
+
-- guard this connector", and a run that asks a hundred times is the same answer.
|
|
11
|
+
-- The connector is compared exactly, like every connector id: a guard on `slack`
|
|
12
|
+
-- does not cover `linear`.
|
|
13
|
+
--
|
|
14
|
+
-- ON DELETE CASCADE from the run, the same as `run_steps`: a marker means nothing
|
|
15
|
+
-- once the run it describes is gone.
|
|
16
|
+
CREATE TABLE IF NOT EXISTS stackbone_platform.echo_guard_checks (
|
|
17
|
+
run_id uuid NOT NULL
|
|
18
|
+
REFERENCES stackbone_platform.runs(id) ON DELETE CASCADE,
|
|
19
|
+
connector text NOT NULL,
|
|
20
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
21
|
+
PRIMARY KEY (run_id, connector)
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
-- The warning reads the runs ONE link started or resumed inside a short window,
|
|
25
|
+
-- once per link on every `GET /api/trigger-subscriptions`. 0075 left `trigger_ref`
|
|
26
|
+
-- unindexed because nothing filtered on it; this is that reader. On `started_at`
|
|
27
|
+
-- rather than `created_at` because a resume re-arms the row and moves `started_at`
|
|
28
|
+
-- to the resume, so a run that woke up and wrote is read by when it woke. Partial,
|
|
29
|
+
-- so only `event` runs (the only ones that carry a ref) pay for it on write.
|
|
30
|
+
CREATE INDEX IF NOT EXISTS runs_trigger_ref_started_at_idx
|
|
31
|
+
ON stackbone_platform.runs (trigger_ref, started_at DESC)
|
|
32
|
+
WHERE trigger_ref IS NOT NULL;
|
|
@@ -0,0 +1,522 @@
|
|
|
1
|
+
-- 0092_trigger_subscriptions_outlook_new_email.sql — the `outlook-app-only` links
|
|
2
|
+
-- move to the `microsoft-outlook` piece's `newEmail` (PLA-1932).
|
|
3
|
+
--
|
|
4
|
+
-- Both Outlook built-ins left the catalog: the one `microsoft-outlook` piece takes
|
|
5
|
+
-- either grant (PLA-1838) and its `newEmail` replaces their `message-received`.
|
|
6
|
+
-- Same folder (the Inbox), same `receivedDateTime gt` filter, every sender. What
|
|
7
|
+
-- differs is the event and where the mailbox lives, so each link is rewritten in
|
|
8
|
+
-- place, on the box that holds it, when that box boots: a box on a laptop is
|
|
9
|
+
-- converted by starting it, with nothing to run by hand.
|
|
10
|
+
--
|
|
11
|
+
-- Per link, `connector = 'outlook-app-only' AND trigger_id = 'message-received'`,
|
|
12
|
+
-- matched exactly (never `LIKE`: `outlook-app-only-2`, `outlook-catalog` and the
|
|
13
|
+
-- delegated `outlook` are other rows):
|
|
14
|
+
--
|
|
15
|
+
-- * the cursor. The built-in kept the ISO stamp of the newest mail it read; the
|
|
16
|
+
-- piece's TIMEBASED poll reads `{ "lastPoll": <epoch ms> }` and throws on
|
|
17
|
+
-- anything else, and filters `receivedDateTime gt` that instant, so the link
|
|
18
|
+
-- resumes where it stopped. Only a stamp with an explicit offset is read: one
|
|
19
|
+
-- without would be read in the session's time zone, and a guess is worse than
|
|
20
|
+
-- a link that refuses to poll. Anything else stays as it was (and is reported
|
|
21
|
+
-- below) rather than becoming epoch 0, which would replay the Inbox. A NULL
|
|
22
|
+
-- cursor stays NULL: arming the link runs the piece's own `onEnable`.
|
|
23
|
+
-- * the mapping. The built-in's event was its own flat map of Graph's message;
|
|
24
|
+
-- the piece's is the message as Graph answers it. A field the mapping reads
|
|
25
|
+
-- OFF THE EVENT is renamed: `messageId` to `id`, `snippet` to `bodyPreview`,
|
|
26
|
+
-- `date` to `receivedDateTime`, and `mailbox`, which the link stamped on, to
|
|
27
|
+
-- the mailbox itself as a string. Strings, later steps of a path, variables
|
|
28
|
+
-- and functions are never touched. The rewrite reads the JSONata token by
|
|
29
|
+
-- token; where it cannot be sure what a name reads (a predicate, a lambda,
|
|
30
|
+
-- the context `$` read whole or by name, an operator it does not model) it
|
|
31
|
+
-- leaves the mapping exactly as it was and reports it. That mapping fails at
|
|
32
|
+
-- the next event, where the operator sees it; a mangled one would keep
|
|
33
|
+
-- running with wrong data. A link with no mapping handed the workflow the
|
|
34
|
+
-- event itself, so it is given one that spells that event out.
|
|
35
|
+
-- * the mailbox goes from the link (`scope` `user-id`) to the connection, where
|
|
36
|
+
-- the piece reads it. The connector row is converted beside this, by the box
|
|
37
|
+
-- (`connect-tables.ts`) and by db-core 0038, before this runs.
|
|
38
|
+
-- * what described the old event goes: `mapping_graph` when the mapping
|
|
39
|
+
-- changed (it would compile back to the old names), and `sample_event`.
|
|
40
|
+
-- * `poll_revision` moves, so a poll result read before this is not saved.
|
|
41
|
+
--
|
|
42
|
+
-- The delivery journal needs nothing: both triggers key a delivery on Graph's
|
|
43
|
+
-- message id, so mail delivered before the move is not delivered again.
|
|
44
|
+
|
|
45
|
+
CREATE FUNCTION stackbone_platform.pla1932_last_poll(stored jsonb) RETURNS jsonb
|
|
46
|
+
LANGUAGE plpgsql AS $fn$
|
|
47
|
+
DECLARE
|
|
48
|
+
stamp text;
|
|
49
|
+
instant timestamptz;
|
|
50
|
+
BEGIN
|
|
51
|
+
IF stored IS NULL OR jsonb_typeof(stored) <> 'string' THEN
|
|
52
|
+
RETURN NULL;
|
|
53
|
+
END IF;
|
|
54
|
+
stamp := stored #>> '{}';
|
|
55
|
+
IF stamp !~ '^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]{1,9})?(Z|[+-][0-9]{2}:[0-9]{2})$' THEN
|
|
56
|
+
RETURN NULL;
|
|
57
|
+
END IF;
|
|
58
|
+
BEGIN
|
|
59
|
+
instant := stamp::timestamptz;
|
|
60
|
+
EXCEPTION WHEN others THEN
|
|
61
|
+
RETURN NULL;
|
|
62
|
+
END;
|
|
63
|
+
RETURN jsonb_build_object('lastPoll', floor(extract(epoch FROM instant) * 1000)::bigint);
|
|
64
|
+
END;
|
|
65
|
+
$fn$;
|
|
66
|
+
|
|
67
|
+
-- The JSONata over `message-received`'s event, rewritten over `newEmail`'s, or
|
|
68
|
+
-- NULL when it cannot be done with certainty. `context_is_event` tracks whether a
|
|
69
|
+
-- bare name at this point reads the event: true at the top, in a call's arguments,
|
|
70
|
+
-- in a group, an array or an object; false inside a predicate, a lambda body, a
|
|
71
|
+
-- block or object applied to a path, and a sort.
|
|
72
|
+
CREATE FUNCTION stackbone_platform.pla1932_new_email_jsonata(expr text, mailbox text)
|
|
73
|
+
RETURNS text
|
|
74
|
+
LANGUAGE plpgsql AS $fn$
|
|
75
|
+
DECLARE
|
|
76
|
+
-- Anything that ends a name: JSONata's operators, quotes and whitespace.
|
|
77
|
+
ends_name constant text := '[][{}().,;:?+*/%|=<>^&!~@#"''`$[:space:]-]';
|
|
78
|
+
n int := length(expr);
|
|
79
|
+
i int := 1;
|
|
80
|
+
j int;
|
|
81
|
+
c text;
|
|
82
|
+
nx text;
|
|
83
|
+
out text := '';
|
|
84
|
+
tok text;
|
|
85
|
+
word text;
|
|
86
|
+
quoted boolean;
|
|
87
|
+
is_field boolean;
|
|
88
|
+
-- The open brackets, innermost last, and what each one means.
|
|
89
|
+
opener text[] := '{}';
|
|
90
|
+
reads_event boolean[] := '{}';
|
|
91
|
+
is_object boolean[] := '{}';
|
|
92
|
+
reading_key boolean[] := '{}';
|
|
93
|
+
questions int[] := '{}';
|
|
94
|
+
is_params boolean[] := '{}';
|
|
95
|
+
depth int := 0;
|
|
96
|
+
top_questions int := 0;
|
|
97
|
+
context_is_event boolean := true;
|
|
98
|
+
closed_params boolean;
|
|
99
|
+
-- What came before: start, value, var, ctx (a bare `$`/`$$`), dot, op, open,
|
|
100
|
+
-- comma, colon, fn (`function`), params (a lambda's closed parameter list).
|
|
101
|
+
prev text := 'start';
|
|
102
|
+
before_dot text := 'other';
|
|
103
|
+
BEGIN
|
|
104
|
+
IF expr IS NULL THEN
|
|
105
|
+
RETURN NULL;
|
|
106
|
+
END IF;
|
|
107
|
+
WHILE i <= n LOOP
|
|
108
|
+
c := substr(expr, i, 1);
|
|
109
|
+
nx := substr(expr, i + 1, 1);
|
|
110
|
+
|
|
111
|
+
IF c ~ '[[:space:]]' THEN
|
|
112
|
+
out := out || c;
|
|
113
|
+
i := i + 1;
|
|
114
|
+
CONTINUE;
|
|
115
|
+
END IF;
|
|
116
|
+
|
|
117
|
+
IF c = '/' AND nx = '*' THEN
|
|
118
|
+
j := strpos(substr(expr, i + 2), '*/');
|
|
119
|
+
IF j = 0 THEN
|
|
120
|
+
RETURN NULL;
|
|
121
|
+
END IF;
|
|
122
|
+
out := out || substr(expr, i, j + 3);
|
|
123
|
+
i := i + j + 3;
|
|
124
|
+
CONTINUE;
|
|
125
|
+
END IF;
|
|
126
|
+
|
|
127
|
+
-- The context itself is understood only as `$.<field>`.
|
|
128
|
+
IF prev = 'ctx' AND c <> '.' THEN
|
|
129
|
+
RETURN NULL;
|
|
130
|
+
END IF;
|
|
131
|
+
|
|
132
|
+
IF c = '"' OR c = '''' THEN
|
|
133
|
+
j := i + 1;
|
|
134
|
+
LOOP
|
|
135
|
+
IF j > n THEN
|
|
136
|
+
RETURN NULL;
|
|
137
|
+
END IF;
|
|
138
|
+
IF substr(expr, j, 1) = '\' THEN
|
|
139
|
+
j := j + 2;
|
|
140
|
+
CONTINUE;
|
|
141
|
+
END IF;
|
|
142
|
+
EXIT WHEN substr(expr, j, 1) = c;
|
|
143
|
+
j := j + 1;
|
|
144
|
+
END LOOP;
|
|
145
|
+
out := out || substr(expr, i, j - i + 1);
|
|
146
|
+
i := j + 1;
|
|
147
|
+
prev := 'value';
|
|
148
|
+
CONTINUE;
|
|
149
|
+
END IF;
|
|
150
|
+
|
|
151
|
+
-- A regex literal stands where an operand does; elsewhere `/` divides.
|
|
152
|
+
IF c = '/' AND prev NOT IN ('value', 'var') THEN
|
|
153
|
+
j := i + 1;
|
|
154
|
+
LOOP
|
|
155
|
+
IF j > n THEN
|
|
156
|
+
RETURN NULL;
|
|
157
|
+
END IF;
|
|
158
|
+
IF substr(expr, j, 1) = '\' THEN
|
|
159
|
+
j := j + 2;
|
|
160
|
+
CONTINUE;
|
|
161
|
+
END IF;
|
|
162
|
+
EXIT WHEN substr(expr, j, 1) = '/';
|
|
163
|
+
j := j + 1;
|
|
164
|
+
END LOOP;
|
|
165
|
+
j := j + 1;
|
|
166
|
+
WHILE j <= n AND substr(expr, j, 1) ~ '[a-z]' LOOP
|
|
167
|
+
j := j + 1;
|
|
168
|
+
END LOOP;
|
|
169
|
+
out := out || substr(expr, i, j - i);
|
|
170
|
+
i := j;
|
|
171
|
+
prev := 'value';
|
|
172
|
+
CONTINUE;
|
|
173
|
+
END IF;
|
|
174
|
+
|
|
175
|
+
IF c = '$' THEN
|
|
176
|
+
j := i + 1;
|
|
177
|
+
IF nx = '$' THEN
|
|
178
|
+
j := j + 1;
|
|
179
|
+
ELSE
|
|
180
|
+
WHILE j <= n AND substr(expr, j, 1) !~ ends_name LOOP
|
|
181
|
+
j := j + 1;
|
|
182
|
+
END LOOP;
|
|
183
|
+
END IF;
|
|
184
|
+
tok := substr(expr, i, j - i);
|
|
185
|
+
out := out || tok;
|
|
186
|
+
i := j;
|
|
187
|
+
prev := CASE WHEN tok IN ('$', '$$') AND context_is_event THEN 'ctx' ELSE 'var' END;
|
|
188
|
+
CONTINUE;
|
|
189
|
+
END IF;
|
|
190
|
+
|
|
191
|
+
IF c ~ '[0-9]' THEN
|
|
192
|
+
j := i;
|
|
193
|
+
WHILE j <= n AND substr(expr, j, 1) ~ '[0-9]' LOOP
|
|
194
|
+
j := j + 1;
|
|
195
|
+
END LOOP;
|
|
196
|
+
IF substr(expr, j, 1) = '.' AND substr(expr, j + 1, 1) ~ '[0-9]' THEN
|
|
197
|
+
j := j + 1;
|
|
198
|
+
WHILE j <= n AND substr(expr, j, 1) ~ '[0-9]' LOOP
|
|
199
|
+
j := j + 1;
|
|
200
|
+
END LOOP;
|
|
201
|
+
END IF;
|
|
202
|
+
IF substr(expr, j, 2) ~ '^[eE][0-9]' OR substr(expr, j, 3) ~ '^[eE][+-][0-9]' THEN
|
|
203
|
+
j := j + 2;
|
|
204
|
+
WHILE j <= n AND substr(expr, j, 1) ~ '[0-9]' LOOP
|
|
205
|
+
j := j + 1;
|
|
206
|
+
END LOOP;
|
|
207
|
+
END IF;
|
|
208
|
+
out := out || substr(expr, i, j - i);
|
|
209
|
+
i := j;
|
|
210
|
+
prev := 'value';
|
|
211
|
+
CONTINUE;
|
|
212
|
+
END IF;
|
|
213
|
+
|
|
214
|
+
IF c = '`' OR c !~ ends_name THEN
|
|
215
|
+
IF c = '`' THEN
|
|
216
|
+
j := strpos(substr(expr, i + 1), '`');
|
|
217
|
+
IF j = 0 THEN
|
|
218
|
+
RETURN NULL;
|
|
219
|
+
END IF;
|
|
220
|
+
word := substr(expr, i + 1, j - 1);
|
|
221
|
+
tok := substr(expr, i, j + 1);
|
|
222
|
+
quoted := true;
|
|
223
|
+
i := i + j + 1;
|
|
224
|
+
ELSE
|
|
225
|
+
j := i;
|
|
226
|
+
WHILE j <= n AND substr(expr, j, 1) !~ ends_name LOOP
|
|
227
|
+
j := j + 1;
|
|
228
|
+
END LOOP;
|
|
229
|
+
word := substr(expr, i, j - i);
|
|
230
|
+
tok := word;
|
|
231
|
+
quoted := false;
|
|
232
|
+
i := j;
|
|
233
|
+
END IF;
|
|
234
|
+
|
|
235
|
+
IF NOT quoted AND word IN ('and', 'or', 'in') THEN
|
|
236
|
+
out := out || tok;
|
|
237
|
+
prev := 'op';
|
|
238
|
+
CONTINUE;
|
|
239
|
+
END IF;
|
|
240
|
+
IF NOT quoted AND word IN ('true', 'false', 'null') THEN
|
|
241
|
+
out := out || tok;
|
|
242
|
+
prev := 'value';
|
|
243
|
+
CONTINUE;
|
|
244
|
+
END IF;
|
|
245
|
+
IF NOT quoted AND word IN ('function', 'λ') THEN
|
|
246
|
+
out := out || tok;
|
|
247
|
+
prev := 'fn';
|
|
248
|
+
CONTINUE;
|
|
249
|
+
END IF;
|
|
250
|
+
|
|
251
|
+
is_field := word IN ('messageId', 'snippet', 'date', 'mailbox');
|
|
252
|
+
IF prev = 'dot' THEN
|
|
253
|
+
-- A later step of a path: another object's field, unless the step before
|
|
254
|
+
-- it is the context or a variable, whose value is unknown here.
|
|
255
|
+
IF is_field AND before_dot IN ('ctx', 'var') THEN
|
|
256
|
+
RETURN NULL;
|
|
257
|
+
END IF;
|
|
258
|
+
out := out || tok;
|
|
259
|
+
prev := 'value';
|
|
260
|
+
CONTINUE;
|
|
261
|
+
END IF;
|
|
262
|
+
|
|
263
|
+
IF NOT is_field THEN
|
|
264
|
+
out := out || tok;
|
|
265
|
+
ELSIF NOT context_is_event THEN
|
|
266
|
+
RETURN NULL;
|
|
267
|
+
ELSIF depth > 0 AND is_object[depth] AND reading_key[depth] THEN
|
|
268
|
+
-- An unquoted key is an expression: renaming it would rename the key.
|
|
269
|
+
RETURN NULL;
|
|
270
|
+
ELSIF word = 'mailbox' THEN
|
|
271
|
+
IF mailbox IS NULL THEN
|
|
272
|
+
RETURN NULL;
|
|
273
|
+
END IF;
|
|
274
|
+
-- The mailbox becomes a string, which has no field, no index and no call.
|
|
275
|
+
j := i;
|
|
276
|
+
WHILE j <= n AND substr(expr, j, 1) ~ '[[:space:]]' LOOP
|
|
277
|
+
j := j + 1;
|
|
278
|
+
END LOOP;
|
|
279
|
+
IF substr(expr, j, 1) IN ('.', '[', '(', '{', '/', '`') THEN
|
|
280
|
+
RETURN NULL;
|
|
281
|
+
END IF;
|
|
282
|
+
out := out || to_json(mailbox)::text;
|
|
283
|
+
ELSE
|
|
284
|
+
tok := CASE word
|
|
285
|
+
WHEN 'messageId' THEN 'id'
|
|
286
|
+
WHEN 'snippet' THEN 'bodyPreview'
|
|
287
|
+
ELSE 'receivedDateTime'
|
|
288
|
+
END;
|
|
289
|
+
out := out || CASE WHEN quoted THEN '`' || tok || '`' ELSE tok END;
|
|
290
|
+
END IF;
|
|
291
|
+
prev := 'value';
|
|
292
|
+
CONTINUE;
|
|
293
|
+
END IF;
|
|
294
|
+
|
|
295
|
+
IF c = '.' THEN
|
|
296
|
+
IF nx = '.' THEN
|
|
297
|
+
out := out || '..';
|
|
298
|
+
i := i + 2;
|
|
299
|
+
prev := 'op';
|
|
300
|
+
CONTINUE;
|
|
301
|
+
END IF;
|
|
302
|
+
before_dot := CASE WHEN prev IN ('ctx', 'var') THEN prev ELSE 'other' END;
|
|
303
|
+
out := out || c;
|
|
304
|
+
i := i + 1;
|
|
305
|
+
prev := 'dot';
|
|
306
|
+
CONTINUE;
|
|
307
|
+
END IF;
|
|
308
|
+
|
|
309
|
+
IF c IN ('(', '[', '{') OR (c = '^' AND nx = '(') THEN
|
|
310
|
+
depth := depth + 1;
|
|
311
|
+
opener[depth] := CASE WHEN c = '^' THEN '(' ELSE c END;
|
|
312
|
+
is_object[depth] := false;
|
|
313
|
+
reading_key[depth] := false;
|
|
314
|
+
questions[depth] := 0;
|
|
315
|
+
is_params[depth] := false;
|
|
316
|
+
IF c = '^' THEN
|
|
317
|
+
-- A sort's terms read each item.
|
|
318
|
+
reads_event[depth] := false;
|
|
319
|
+
out := out || '^(';
|
|
320
|
+
i := i + 2;
|
|
321
|
+
ELSE
|
|
322
|
+
IF c = '(' THEN
|
|
323
|
+
is_params[depth] := prev = 'fn';
|
|
324
|
+
-- A call's arguments and a group read the same context; a lambda's
|
|
325
|
+
-- parameters and a block applied to a path do not.
|
|
326
|
+
reads_event[depth] := context_is_event AND prev NOT IN ('fn', 'dot');
|
|
327
|
+
ELSIF c = '[' THEN
|
|
328
|
+
-- After a value it filters that value; at an operand it builds an array.
|
|
329
|
+
reads_event[depth] := context_is_event AND prev NOT IN ('value', 'var', 'dot');
|
|
330
|
+
ELSE
|
|
331
|
+
is_object[depth] := prev <> 'params';
|
|
332
|
+
reading_key[depth] := prev <> 'params';
|
|
333
|
+
-- An object built at an operand reads the context; after a path it
|
|
334
|
+
-- groups that path's items, and after a lambda's parameters it is the
|
|
335
|
+
-- lambda's body.
|
|
336
|
+
reads_event[depth] := context_is_event AND prev NOT IN ('value', 'var', 'dot', 'params');
|
|
337
|
+
END IF;
|
|
338
|
+
out := out || c;
|
|
339
|
+
i := i + 1;
|
|
340
|
+
END IF;
|
|
341
|
+
context_is_event := reads_event[depth];
|
|
342
|
+
prev := 'open';
|
|
343
|
+
CONTINUE;
|
|
344
|
+
END IF;
|
|
345
|
+
|
|
346
|
+
IF c IN (')', ']', '}') THEN
|
|
347
|
+
IF depth = 0 OR opener[depth] <> (CASE c WHEN ')' THEN '(' WHEN ']' THEN '[' ELSE '{' END) THEN
|
|
348
|
+
RETURN NULL;
|
|
349
|
+
END IF;
|
|
350
|
+
closed_params := is_params[depth];
|
|
351
|
+
depth := depth - 1;
|
|
352
|
+
context_is_event := CASE WHEN depth = 0 THEN true ELSE reads_event[depth] END;
|
|
353
|
+
out := out || c;
|
|
354
|
+
i := i + 1;
|
|
355
|
+
prev := CASE WHEN closed_params THEN 'params' ELSE 'value' END;
|
|
356
|
+
CONTINUE;
|
|
357
|
+
END IF;
|
|
358
|
+
|
|
359
|
+
IF c = ',' THEN
|
|
360
|
+
IF depth > 0 AND is_object[depth] THEN
|
|
361
|
+
reading_key[depth] := true;
|
|
362
|
+
questions[depth] := 0;
|
|
363
|
+
END IF;
|
|
364
|
+
out := out || c;
|
|
365
|
+
i := i + 1;
|
|
366
|
+
prev := 'comma';
|
|
367
|
+
CONTINUE;
|
|
368
|
+
END IF;
|
|
369
|
+
|
|
370
|
+
IF c = ':' THEN
|
|
371
|
+
IF nx = '=' THEN
|
|
372
|
+
out := out || ':=';
|
|
373
|
+
i := i + 2;
|
|
374
|
+
prev := 'op';
|
|
375
|
+
CONTINUE;
|
|
376
|
+
END IF;
|
|
377
|
+
IF depth > 0 AND questions[depth] > 0 THEN
|
|
378
|
+
questions[depth] := questions[depth] - 1;
|
|
379
|
+
prev := 'op';
|
|
380
|
+
ELSIF depth = 0 AND top_questions > 0 THEN
|
|
381
|
+
top_questions := top_questions - 1;
|
|
382
|
+
prev := 'op';
|
|
383
|
+
ELSIF depth > 0 AND is_object[depth] AND reading_key[depth] THEN
|
|
384
|
+
reading_key[depth] := false;
|
|
385
|
+
prev := 'colon';
|
|
386
|
+
ELSE
|
|
387
|
+
RETURN NULL;
|
|
388
|
+
END IF;
|
|
389
|
+
out := out || c;
|
|
390
|
+
i := i + 1;
|
|
391
|
+
CONTINUE;
|
|
392
|
+
END IF;
|
|
393
|
+
|
|
394
|
+
IF c = '?' THEN
|
|
395
|
+
IF nx IN (':', '?') THEN
|
|
396
|
+
out := out || c || nx;
|
|
397
|
+
i := i + 2;
|
|
398
|
+
ELSE
|
|
399
|
+
IF depth > 0 THEN
|
|
400
|
+
questions[depth] := questions[depth] + 1;
|
|
401
|
+
ELSE
|
|
402
|
+
top_questions := top_questions + 1;
|
|
403
|
+
END IF;
|
|
404
|
+
out := out || c;
|
|
405
|
+
i := i + 1;
|
|
406
|
+
END IF;
|
|
407
|
+
prev := 'op';
|
|
408
|
+
CONTINUE;
|
|
409
|
+
END IF;
|
|
410
|
+
|
|
411
|
+
IF c = '*' THEN
|
|
412
|
+
IF nx = '*' THEN
|
|
413
|
+
RETURN NULL;
|
|
414
|
+
END IF;
|
|
415
|
+
IF prev IN ('value', 'var') THEN
|
|
416
|
+
prev := 'op';
|
|
417
|
+
ELSIF prev = 'dot' OR NOT context_is_event THEN
|
|
418
|
+
prev := 'value';
|
|
419
|
+
ELSE
|
|
420
|
+
-- Every field of the event at once.
|
|
421
|
+
RETURN NULL;
|
|
422
|
+
END IF;
|
|
423
|
+
out := out || c;
|
|
424
|
+
i := i + 1;
|
|
425
|
+
CONTINUE;
|
|
426
|
+
END IF;
|
|
427
|
+
|
|
428
|
+
IF (c = '~' AND nx = '>') OR (c = '!' AND nx = '=') OR (c IN ('<', '>') AND nx = '=') THEN
|
|
429
|
+
out := out || c || nx;
|
|
430
|
+
i := i + 2;
|
|
431
|
+
prev := 'op';
|
|
432
|
+
CONTINUE;
|
|
433
|
+
END IF;
|
|
434
|
+
|
|
435
|
+
IF c IN (';', '=', '<', '>', '+', '-', '&', '/') THEN
|
|
436
|
+
out := out || c;
|
|
437
|
+
i := i + 1;
|
|
438
|
+
prev := 'op';
|
|
439
|
+
CONTINUE;
|
|
440
|
+
END IF;
|
|
441
|
+
|
|
442
|
+
-- `|` (transform), `@` and `#` (bindings), `%` (parent or modulo), a lone `~`,
|
|
443
|
+
-- `!` or `^`, and anything else: not modelled.
|
|
444
|
+
RETURN NULL;
|
|
445
|
+
END LOOP;
|
|
446
|
+
|
|
447
|
+
IF depth <> 0 OR prev = 'ctx' THEN
|
|
448
|
+
RETURN NULL;
|
|
449
|
+
END IF;
|
|
450
|
+
RETURN out;
|
|
451
|
+
END;
|
|
452
|
+
$fn$;
|
|
453
|
+
|
|
454
|
+
DO $$
|
|
455
|
+
DECLARE
|
|
456
|
+
link record;
|
|
457
|
+
v_mailbox text;
|
|
458
|
+
v_cursor jsonb;
|
|
459
|
+
v_mapping text;
|
|
460
|
+
v_correlation text;
|
|
461
|
+
BEGIN
|
|
462
|
+
FOR link IN
|
|
463
|
+
SELECT id, scope, poll_cursor, mapping, correlation_key
|
|
464
|
+
FROM stackbone_platform.trigger_subscriptions
|
|
465
|
+
WHERE connector = 'outlook-app-only'
|
|
466
|
+
AND trigger_id = 'message-received'
|
|
467
|
+
ORDER BY created_at, id
|
|
468
|
+
FOR UPDATE
|
|
469
|
+
LOOP
|
|
470
|
+
v_mailbox := nullif(btrim(link.scope ->> 'user-id'), '');
|
|
471
|
+
|
|
472
|
+
v_cursor := link.poll_cursor;
|
|
473
|
+
IF link.poll_cursor IS NOT NULL THEN
|
|
474
|
+
v_cursor := stackbone_platform.pla1932_last_poll(link.poll_cursor);
|
|
475
|
+
IF v_cursor IS NULL THEN
|
|
476
|
+
v_cursor := link.poll_cursor;
|
|
477
|
+
RAISE WARNING 'PLA-1932: trigger link % keeps its cursor %: it is no timestamp newEmail can resume from. Arm the link again, starting from now.',
|
|
478
|
+
link.id, link.poll_cursor;
|
|
479
|
+
END IF;
|
|
480
|
+
END IF;
|
|
481
|
+
|
|
482
|
+
IF link.mapping IS NULL THEN
|
|
483
|
+
v_mapping := '{ "messageId": id, "conversationId": conversationId, "from": from, "subject": subject, "snippet": bodyPreview, "date": receivedDateTime, "isRead": isRead'
|
|
484
|
+
|| CASE WHEN v_mailbox IS NULL THEN '' ELSE ', "mailbox": ' || to_json(v_mailbox)::text END
|
|
485
|
+
|| ' }';
|
|
486
|
+
ELSE
|
|
487
|
+
v_mapping := stackbone_platform.pla1932_new_email_jsonata(link.mapping, v_mailbox);
|
|
488
|
+
IF v_mapping IS NULL THEN
|
|
489
|
+
v_mapping := link.mapping;
|
|
490
|
+
RAISE WARNING 'PLA-1932: trigger link % keeps its mapping as it was: rewrite it over the newEmail event (id, bodyPreview, receivedDateTime, and the mailbox as a string).',
|
|
491
|
+
link.id;
|
|
492
|
+
END IF;
|
|
493
|
+
END IF;
|
|
494
|
+
|
|
495
|
+
v_correlation := link.correlation_key;
|
|
496
|
+
IF link.correlation_key IS NOT NULL THEN
|
|
497
|
+
v_correlation := coalesce(
|
|
498
|
+
stackbone_platform.pla1932_new_email_jsonata(link.correlation_key, v_mailbox),
|
|
499
|
+
link.correlation_key
|
|
500
|
+
);
|
|
501
|
+
END IF;
|
|
502
|
+
|
|
503
|
+
UPDATE stackbone_platform.trigger_subscriptions
|
|
504
|
+
SET connector = 'microsoft-outlook',
|
|
505
|
+
trigger_id = 'newEmail',
|
|
506
|
+
poll_cursor = v_cursor,
|
|
507
|
+
mapping = v_mapping,
|
|
508
|
+
mapping_graph = CASE
|
|
509
|
+
WHEN v_mapping IS DISTINCT FROM link.mapping THEN NULL
|
|
510
|
+
ELSE mapping_graph
|
|
511
|
+
END,
|
|
512
|
+
correlation_key = v_correlation,
|
|
513
|
+
scope = NULL,
|
|
514
|
+
sample_event = NULL,
|
|
515
|
+
poll_revision = poll_revision + 1,
|
|
516
|
+
updated_at = now()
|
|
517
|
+
WHERE id = link.id;
|
|
518
|
+
END LOOP;
|
|
519
|
+
END $$;
|
|
520
|
+
|
|
521
|
+
DROP FUNCTION stackbone_platform.pla1932_last_poll(jsonb);
|
|
522
|
+
DROP FUNCTION stackbone_platform.pla1932_new_email_jsonata(text, text);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
-- 0093_trigger_subscriptions_webhook_token.sql — a webhook link's ingress token (PLA-1856)
|
|
2
|
+
-- A piece's WEBHOOK trigger is delivered by the provider POSTing to the box at
|
|
3
|
+
-- `/api/hooks/<link id>/<token>`. The route is public, so the token is the credential:
|
|
4
|
+
-- 128 random bits, minted when the link arms, cleared when it disarms and minted
|
|
5
|
+
-- again on the next arm, so a URL the provider held before stops answering.
|
|
6
|
+
--
|
|
7
|
+
-- Plaintext, compared in constant time: `onDisable` is handed the same URL `onEnable`
|
|
8
|
+
-- registered, which a digest could not rebuild. Never returned by a list or a read.
|
|
9
|
+
-- Nullable: NULL is "no ingress", which every polling link and every link that is off
|
|
10
|
+
-- reads as.
|
|
11
|
+
|
|
12
|
+
ALTER TABLE stackbone_platform.trigger_subscriptions
|
|
13
|
+
ADD COLUMN IF NOT EXISTS webhook_token text;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
-- 0094_rag_document_versions.sql — RAG document versions (STA3-8)
|
|
2
|
+
-- Every version of a document is its own row, with its own `source` (its storage
|
|
3
|
+
-- key) and its own chunks, so rollback flips a flag and re-embeds nothing. The
|
|
4
|
+
-- versions of one document share a `document_key`; the agent reads only the
|
|
5
|
+
-- `is_active` one.
|
|
6
|
+
--
|
|
7
|
+
-- A document that existed before this is its own version 1, published, keyed by
|
|
8
|
+
-- its `source`.
|
|
9
|
+
|
|
10
|
+
ALTER TABLE stackbone_platform.rag_documents
|
|
11
|
+
ADD COLUMN IF NOT EXISTS document_key text,
|
|
12
|
+
ADD COLUMN IF NOT EXISTS version integer NOT NULL DEFAULT 1,
|
|
13
|
+
ADD COLUMN IF NOT EXISTS is_active boolean NOT NULL DEFAULT true;
|
|
14
|
+
|
|
15
|
+
-- Two overlapping ingests of one source could leave two rows before PLA-2327's
|
|
16
|
+
-- lock. The newest keeps the source as its key; the older ones become documents
|
|
17
|
+
-- of their own, still published, so nothing the agent reads changes and the
|
|
18
|
+
-- unique index below can build. Deleting them would lose data.
|
|
19
|
+
UPDATE stackbone_platform.rag_documents d
|
|
20
|
+
SET document_key = CASE WHEN ranked.rn = 1 THEN d.source ELSE d.source || '#' || d.id END
|
|
21
|
+
FROM (
|
|
22
|
+
SELECT id, row_number() OVER (
|
|
23
|
+
PARTITION BY collection_id, source ORDER BY created_at DESC, id DESC
|
|
24
|
+
) AS rn
|
|
25
|
+
FROM stackbone_platform.rag_documents
|
|
26
|
+
WHERE document_key IS NULL
|
|
27
|
+
) ranked
|
|
28
|
+
WHERE d.id = ranked.id;
|
|
29
|
+
|
|
30
|
+
-- Postgres cannot default one column to another. A workspace on an SDK older
|
|
31
|
+
-- than this migration inserts without the key, so the trigger fills it in
|
|
32
|
+
-- instead of failing every ingest on NOT NULL.
|
|
33
|
+
CREATE OR REPLACE FUNCTION stackbone_platform.rag_documents_default_key()
|
|
34
|
+
RETURNS trigger
|
|
35
|
+
LANGUAGE plpgsql
|
|
36
|
+
AS $$
|
|
37
|
+
BEGIN
|
|
38
|
+
NEW.document_key := COALESCE(NEW.document_key, NEW.source);
|
|
39
|
+
RETURN NEW;
|
|
40
|
+
END;
|
|
41
|
+
$$;
|
|
42
|
+
|
|
43
|
+
DROP TRIGGER IF EXISTS rag_documents_default_key ON stackbone_platform.rag_documents;
|
|
44
|
+
CREATE TRIGGER rag_documents_default_key
|
|
45
|
+
BEFORE INSERT ON stackbone_platform.rag_documents
|
|
46
|
+
FOR EACH ROW EXECUTE FUNCTION stackbone_platform.rag_documents_default_key();
|
|
47
|
+
|
|
48
|
+
ALTER TABLE stackbone_platform.rag_documents
|
|
49
|
+
ALTER COLUMN document_key SET NOT NULL;
|
|
50
|
+
|
|
51
|
+
-- At most one published version per document. Non-deferrable, so publishing is
|
|
52
|
+
-- two statements (unpublish the siblings, then publish the target): one swapping
|
|
53
|
+
-- UPDATE can reach the target before the sibling and fail on the check.
|
|
54
|
+
CREATE UNIQUE INDEX IF NOT EXISTS rag_documents_active_key_uq
|
|
55
|
+
ON stackbone_platform.rag_documents (collection_id, document_key)
|
|
56
|
+
WHERE is_active;
|
|
57
|
+
|
|
58
|
+
-- Covers unpublished versions too: a writer that misses the key lock and hands
|
|
59
|
+
-- out a version number twice fails here instead of leaving two v2s behind.
|
|
60
|
+
CREATE UNIQUE INDEX IF NOT EXISTS rag_documents_key_version_uq
|
|
61
|
+
ON stackbone_platform.rag_documents (collection_id, document_key, version);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
-- 0095_rag_document_version_counters.sql — a version number is never reused (STA3-8 D11)
|
|
2
|
+
-- The last version number each document handed out. Numbering from the rows
|
|
3
|
+
-- left (`max(version) + 1`) gave a deleted draft's number to the next upload,
|
|
4
|
+
-- and the activity log then showed "deleted v3" next to a live, different v3.
|
|
5
|
+
-- A new version takes this counter or the rows' highest version, whichever is
|
|
6
|
+
-- greater, plus one, under the document's key lock. Deleting the whole document
|
|
7
|
+
-- drops its row; deleting one version leaves it alone.
|
|
8
|
+
|
|
9
|
+
CREATE TABLE IF NOT EXISTS stackbone_platform.rag_document_version_counters (
|
|
10
|
+
collection_id uuid NOT NULL REFERENCES stackbone_platform.rag_collections (id) ON DELETE CASCADE,
|
|
11
|
+
document_key text NOT NULL,
|
|
12
|
+
last_version integer NOT NULL,
|
|
13
|
+
PRIMARY KEY (collection_id, document_key)
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
-- A document from before this starts from the highest version it still holds:
|
|
17
|
+
-- a number deleted before 0095 cannot be told apart from one never handed out.
|
|
18
|
+
INSERT INTO stackbone_platform.rag_document_version_counters (collection_id, document_key, last_version)
|
|
19
|
+
SELECT collection_id, document_key, max(version)
|
|
20
|
+
FROM stackbone_platform.rag_documents
|
|
21
|
+
GROUP BY collection_id, document_key
|
|
22
|
+
ON CONFLICT (collection_id, document_key) DO NOTHING;
|