@vercel/board 0.0.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/README.md +180 -0
  2. package/dist/a2-C41hD8SF.mjs +3045 -0
  3. package/dist/a2-D24cvwpN.d.mts +3359 -0
  4. package/dist/access-CJU2-vaa.mjs +226 -0
  5. package/dist/access-CV96SYgr.d.mts +32 -0
  6. package/dist/access.d.mts +30 -0
  7. package/dist/access.mjs +4 -0
  8. package/dist/actors-DjMeZpnU.mjs +224 -0
  9. package/dist/attempts-Bxp9KgPf.mjs +919 -0
  10. package/dist/auth-COzc09sn.mjs +276 -0
  11. package/dist/auth-CU56Q2Xh.d.mts +204 -0
  12. package/dist/backend-a2/blob-vercel.d.mts +8 -0
  13. package/dist/backend-a2/blob-vercel.mjs +81 -0
  14. package/dist/backend-a2/contract.d.mts +18 -0
  15. package/dist/backend-a2/contract.mjs +5 -0
  16. package/dist/backend-a2/scheduler-local.d.mts +11 -0
  17. package/dist/backend-a2/scheduler-local.mjs +2 -0
  18. package/dist/backend-a2/scheduler-qstash.d.mts +2 -0
  19. package/dist/backend-a2/scheduler-qstash.mjs +2 -0
  20. package/dist/backend-a2/scheduler-vercel.d.mts +2 -0
  21. package/dist/backend-a2/scheduler-vercel.mjs +2 -0
  22. package/dist/backend-a2/store-memory.d.mts +11 -0
  23. package/dist/backend-a2/store-memory.mjs +19 -0
  24. package/dist/backend-a2/store-postgres.d.mts +38 -0
  25. package/dist/backend-a2/store-postgres.mjs +110 -0
  26. package/dist/backend-a2/store-redis-http.d.mts +17 -0
  27. package/dist/backend-a2/store-redis-http.mjs +32 -0
  28. package/dist/backend-a2/store-redis.d.mts +23 -0
  29. package/dist/backend-a2/store-redis.mjs +39 -0
  30. package/dist/backend-a2/store-sqlite.d.mts +12 -0
  31. package/dist/backend-a2/store-sqlite.mjs +31 -0
  32. package/dist/backend-a2.d.mts +4 -0
  33. package/dist/backend-a2.mjs +2 -0
  34. package/dist/backend-http-0roEmjl0.mjs +613 -0
  35. package/dist/backend-http-BVRno-ca.mjs +469 -0
  36. package/dist/backend-http-Bid7RIpC.d.mts +630 -0
  37. package/dist/backend-http.d.mts +10 -0
  38. package/dist/backend-http.mjs +2 -0
  39. package/dist/backend-http.node.d.mts +10 -0
  40. package/dist/backend-http.node.mjs +2 -0
  41. package/dist/blob-C0J3wbGv.d.mts +39 -0
  42. package/dist/bots-CGkO76qH.mjs +67 -0
  43. package/dist/chat-provider-XCnQlxq6.d.mts +140 -0
  44. package/dist/chat-provider-jFSeyNmB.mjs +389 -0
  45. package/dist/cli.d.mts +1 -0
  46. package/dist/cli.mjs +2252 -0
  47. package/dist/client-DGDPaN7U.mjs +380 -0
  48. package/dist/conformance-DRR23YNy.mjs +512 -0
  49. package/dist/conformance.d.mts +30 -0
  50. package/dist/conformance.mjs +2 -0
  51. package/dist/contract-CuJmlrhw.mjs +566 -0
  52. package/dist/emoji-CTLpvH-A.mjs +445 -0
  53. package/dist/engine-Bir5s8ts.mjs +1449 -0
  54. package/dist/errors-DX3hpwWt.mjs +64 -0
  55. package/dist/exports-DLiE7fLY.d.mts +4 -0
  56. package/dist/exports-Dvc1o52I.mjs +1285 -0
  57. package/dist/files-SniSmeEr.mjs +47 -0
  58. package/dist/index.d.mts +13 -0
  59. package/dist/index.default-Bkg77Pxn.mjs +12 -0
  60. package/dist/index.mjs +19 -0
  61. package/dist/index.node.d.mts +14 -0
  62. package/dist/index.node.mjs +5 -0
  63. package/dist/inspect/app.js +416 -0
  64. package/dist/inspect/engine.js +5 -0
  65. package/dist/local-Cr_dSjvs.mjs +44 -0
  66. package/dist/own-OwgPWdmh.mjs +15 -0
  67. package/dist/project-CFiArraX.mjs +124 -0
  68. package/dist/provider-BGjz-mDa.d.mts +2473 -0
  69. package/dist/provider-CuewcDNZ.mjs +205 -0
  70. package/dist/redis-tables-B35vv4CZ.mjs +842 -0
  71. package/dist/reducers-B_vUGQRp.d.mts +1172 -0
  72. package/dist/reducers-mUPFuFJV.mjs +293 -0
  73. package/dist/schemas-Be08nyfW.mjs +74 -0
  74. package/dist/schemas-CWxs3ECJ.d.mts +50 -0
  75. package/dist/serve-BsOOc-6_.mjs +170 -0
  76. package/dist/server-jwPY7_wi.mjs +17 -0
  77. package/dist/serving-BNbzc96L.mjs +2 -0
  78. package/dist/sql-DbiEnqM0.mjs +571 -0
  79. package/dist/sqlite-db-Cbj03vmp.mjs +70 -0
  80. package/dist/sync-github.d.mts +473 -0
  81. package/dist/sync-github.mjs +603 -0
  82. package/dist/sync-kapso.d.mts +175 -0
  83. package/dist/sync-kapso.mjs +86 -0
  84. package/dist/sync-slack.d.mts +364 -0
  85. package/dist/sync-slack.mjs +728 -0
  86. package/dist/sync.chat.d.mts +2 -0
  87. package/dist/sync.chat.mjs +2 -0
  88. package/dist/sync.d.mts +3 -0
  89. package/dist/sync.mjs +4 -0
  90. package/dist/targets-yM5JBUgr.mjs +94 -0
  91. package/dist/tokens-DKLn5P9B.mjs +256 -0
  92. package/dist/types-BHCjTZdB.d.mts +104 -0
  93. package/dist/users-Dls7_ANV.mjs +42 -0
  94. package/dist/watch-spec-W0c7ypHJ.mjs +787 -0
  95. package/dist/watchers-hash-CLjsa6od.mjs +225 -0
  96. package/docs/01-board/01-mounting.md +506 -0
  97. package/docs/01-board/02-database.md +258 -0
  98. package/docs/01-board/03-identity.md +264 -0
  99. package/docs/01-board/04-messages.md +192 -0
  100. package/docs/01-board/05-reading.md +177 -0
  101. package/docs/01-board/06-watchers.md +249 -0
  102. package/docs/01-board/07-schedules.md +133 -0
  103. package/docs/01-board/08-middleware.md +139 -0
  104. package/docs/01-board/09-access.md +501 -0
  105. package/docs/01-quickstart.md +196 -0
  106. package/docs/02-sync/01-overview.md +290 -0
  107. package/docs/02-sync/02-callbacks.md +265 -0
  108. package/docs/02-sync/03-slack.md +248 -0
  109. package/docs/02-sync/04-github.md +201 -0
  110. package/docs/02-sync/05-whatsapp.md +148 -0
  111. package/docs/02-sync/06-providers.md +228 -0
  112. package/docs/03-guides/01-coding-agents.md +226 -0
  113. package/docs/03-guides/02-inspector.md +104 -0
  114. package/docs/03-guides/03-ci.md +251 -0
  115. package/docs/03-guides/04-triage.md +233 -0
  116. package/docs/03-guides/05-agent-factory.md +142 -0
  117. package/docs/04-reference/01-cli.md +606 -0
  118. package/docs/04-reference/02-board.md +579 -0
  119. package/docs/04-reference/03-http.md +489 -0
  120. package/docs/04-reference/04-log.md +250 -0
  121. package/docs/04-reference/05-conformance.md +97 -0
  122. package/docs/index.md +186 -0
  123. package/package.json +168 -1
  124. package/sql/postgres/board.sql +145 -0
  125. package/sql/postgres/notifications.sql +24 -0
  126. package/sql/postgres/schema.sql +451 -0
@@ -0,0 +1,451 @@
1
+ -- The tables for board/backend-a2/store-postgres: A2's, which keep the board's log, then
2
+ -- board's, which it reads from. Apply this file before the first deploy, and
3
+ -- again when a board release says the tables changed. Every statement creates
4
+ -- only what is missing and leaves existing data alone, so it's safe to re-run.
5
+ -- The breakpoint after each statement splits it for migration tools that send
6
+ -- one statement per query; to every other tool it's only a comment.
7
+
8
+ -- A2's tables for experimental-a2/store-postgres. drizzle-schema.ts, next to
9
+ -- this file, shows the same tables as Drizzle definitions, as an example.
10
+ --
11
+ -- Apply this file before the first deploy, and again when a release's
12
+ -- changelog says the tables changed. Every statement creates only what is
13
+ -- missing and leaves existing data alone, so the file is safe to re-run on any
14
+ -- A2 database, whichever release created it. A2 checks nothing up front: a
15
+ -- query that finds a table or column missing throws STORE_NOT_CONFIGURED.
16
+ --
17
+ -- On a database from a release that ran handlers, it drops the work those
18
+ -- handlers hadn't finished, and the columns they dispatched by, so drain
19
+ -- earlier deployments and apply it while none is running.
20
+ --
21
+ -- Each index sits behind a catalog check: `create index if not exists` takes
22
+ -- a share lock on its table, and requires owning it, even when the index
23
+ -- exists. The check makes a re-run on a live database wait for no writer.
24
+ --
25
+ -- The file works as a migration as it ships: copy it into your migration
26
+ -- tool's folder. The breakpoint comment after each statement splits it for
27
+ -- tools that send one statement per query, like Drizzle on PGlite; to every
28
+ -- other tool, psql included, it's only a comment.
29
+
30
+ create table if not exists a2_events (
31
+ session_id text not null,
32
+ idx bigint not null,
33
+ event_type text not null,
34
+ payload jsonb not null,
35
+ event_id text not null,
36
+ created_at bigint not null,
37
+ cause jsonb,
38
+ primary key (session_id, idx)
39
+ );
40
+ --> statement-breakpoint
41
+
42
+ -- One row per run of a cursor: an event cursor's one event, a lane's run of
43
+ -- queued events, or a log cursor's position. A run is keyed by the head it
44
+ -- started at (`epoch`), so a clear starts new runs instead of rewriting old
45
+ -- ones.
46
+ create table if not exists a2_cursors (
47
+ session_id text not null,
48
+ consumer text not null,
49
+ cursor_key text not null,
50
+ epoch bigint not null,
51
+ lane text,
52
+ kind text not null,
53
+ position bigint not null,
54
+ head bigint,
55
+ types jsonb,
56
+ attempt_count integer not null default 0,
57
+ first_claimed_at bigint,
58
+ last_claimed_at bigint,
59
+ claim_holder text,
60
+ claim_expires_at bigint,
61
+ failure_count integer not null default 0,
62
+ last_failed_at bigint,
63
+ last_failed_attempt integer,
64
+ last_error text,
65
+ failed_at bigint,
66
+ completed_idx bigint,
67
+ completed_attempt integer,
68
+ returned_event_ids jsonb,
69
+ primary key (session_id, cursor_key, consumer, epoch)
70
+ );
71
+ --> statement-breakpoint
72
+
73
+ -- A lane cursor's queue: the events appended for the lane, in log order.
74
+ create table if not exists a2_lane_events (
75
+ session_id text not null,
76
+ consumer text not null,
77
+ lane text not null,
78
+ idx bigint not null,
79
+ done boolean not null default false,
80
+ completed_attempt integer,
81
+ returned_event_ids jsonb,
82
+ primary key (session_id, consumer, lane, idx)
83
+ );
84
+ --> statement-breakpoint
85
+
86
+ create table if not exists a2_snapshots (
87
+ session_id text not null,
88
+ reducer_name text not null,
89
+ up_to_index bigint not null,
90
+ state jsonb not null,
91
+ updated_at bigint not null,
92
+ primary key (session_id, reducer_name)
93
+ );
94
+ --> statement-breakpoint
95
+
96
+ create table if not exists a2_snapshot_history (
97
+ session_id text not null,
98
+ reducer_name text not null,
99
+ up_to_index bigint not null,
100
+ state jsonb not null,
101
+ updated_at bigint not null,
102
+ primary key (session_id, reducer_name, up_to_index)
103
+ );
104
+ --> statement-breakpoint
105
+
106
+ -- Earlier releases pinned snapshots per event; this one pins them per
107
+ -- cursor head. Pins only hold history for work in flight, so old ones go.
108
+ do $$ begin
109
+ if to_regclass('a2_snapshot_pins') is not null and not exists (
110
+ select 1 from pg_attribute
111
+ where attrelid = to_regclass('a2_snapshot_pins')
112
+ and attname = 'cursor_key' and not attisdropped
113
+ ) then
114
+ drop table a2_snapshot_pins;
115
+ end if;
116
+ end $$;
117
+ --> statement-breakpoint
118
+
119
+ create table if not exists a2_snapshot_pins (
120
+ session_id text not null,
121
+ event_index bigint not null,
122
+ consumer text not null,
123
+ cursor_key text not null,
124
+ reducer_name text not null,
125
+ up_to_index bigint not null,
126
+ primary key (session_id, event_index, consumer, cursor_key, reducer_name, up_to_index)
127
+ );
128
+ --> statement-breakpoint
129
+
130
+ create table if not exists a2_clears (
131
+ session_id text primary key,
132
+ clear_index bigint not null
133
+ );
134
+ --> statement-breakpoint
135
+
136
+ create table if not exists a2_presence (
137
+ ns text not null,
138
+ participant text not null,
139
+ field text not null,
140
+ value jsonb not null,
141
+ seen bigint not null,
142
+ at bigint not null,
143
+ expires_at bigint not null,
144
+ primary key (ns, participant, field)
145
+ );
146
+ --> statement-breakpoint
147
+
148
+ -- From an earlier release: work its handlers hadn't finished is dropped,
149
+ -- since this release runs no handlers to finish it. Every event is kept.
150
+ -- Causes gain the dispatcher that appended them, the handlers table
151
+ -- (`a2.handlers`), and its lane. Then the old dispatch columns go, and
152
+ -- their indexes with them. The block locks the events first, so an earlier
153
+ -- release still writing waits and then fails, rather than committing between
154
+ -- its statements, and it commits whole, or not at all.
155
+ do $$ begin
156
+ if exists (
157
+ select 1 from pg_attribute
158
+ where attrelid = to_regclass('a2_events')
159
+ and attname = 'processed_at' and not attisdropped
160
+ ) then
161
+ lock table a2_events in access exclusive mode;
162
+
163
+ update a2_events child
164
+ set cause = jsonb_build_object(
165
+ 'consumer', 'a2.handlers',
166
+ 'lane', (select parent.lane from a2_events parent
167
+ where parent.session_id = child.session_id
168
+ and parent.idx = (child.cause ->> 'index')::bigint)
169
+ ) || child.cause
170
+ where child.cause is not null and not child.cause ? 'consumer';
171
+
172
+ alter table a2_events
173
+ drop column lane,
174
+ drop column lane_ready,
175
+ drop column processed_at,
176
+ drop column processed_by_attempt,
177
+ drop column returned_event_ids,
178
+ drop column first_claimed_at,
179
+ drop column last_claimed_at,
180
+ drop column attempt_count,
181
+ drop column failure_count,
182
+ drop column last_failed_at,
183
+ drop column last_failed_attempt,
184
+ drop column last_error,
185
+ drop column failed_at,
186
+ drop column claim_holder,
187
+ drop column claim_expires_at;
188
+ end if;
189
+ end $$;
190
+ --> statement-breakpoint
191
+
192
+ -- Earlier releases recorded a schema version here and checked it before
193
+ -- their first query. This one checks nothing up front.
194
+ drop table if exists a2_schema;
195
+ --> statement-breakpoint
196
+
197
+ -- Runs an operation's statements in one round trip: A2 sends each write that
198
+ -- takes a session's lock as one call, the lock and then the statements that
199
+ -- need it, so it holds the lock only while the server runs them. Each
200
+ -- statement takes its snapshot when it starts, so those after the lock see
201
+ -- what its last holder committed; a single statement can't, since its
202
+ -- snapshot predates the lock. Each statement is a select of jsonb rows: a row
203
+ -- with an `abort` key fails the call and rolls it back, and one with a `stop`
204
+ -- key ends it. It needs read committed, the default: under a stricter level
205
+ -- every statement would share the first one's snapshot, so it refuses to run.
206
+ -- Created only if missing, as the indexes are: a later release that changes it
207
+ -- adds a function with a new name.
208
+ do $$ declare grantee text; begin
209
+ if to_regprocedure('a2_batch(text[], jsonb)') is null then
210
+ create function a2_batch(statements text[], params jsonb)
211
+ returns setof jsonb
212
+ language plpgsql
213
+ volatile
214
+ as $batch$
215
+ declare
216
+ statement text;
217
+ result jsonb;
218
+ begin
219
+ if current_setting('transaction_isolation') <> 'read committed' then
220
+ raise exception using errcode = 'A2002',
221
+ message = 'a2_batch needs the read committed isolation level';
222
+ end if;
223
+ foreach statement in array statements loop
224
+ for result in execute statement using params loop
225
+ if result ? 'abort' then
226
+ raise exception using errcode = 'A2001',
227
+ message = 'a2_batch aborted', detail = result::text;
228
+ end if;
229
+ return next result;
230
+ if result ? 'stop' then
231
+ return;
232
+ end if;
233
+ end loop;
234
+ end loop;
235
+ end
236
+ $batch$;
237
+ -- Default privileges can grant a new function to roles besides PUBLIC, as
238
+ -- platforms that serve functions over an API grant theirs: it starts with
239
+ -- none but its owner's. Grants made after it exist are left alone.
240
+ for grantee in
241
+ select acl.grantee::regrole::text
242
+ from pg_proc proc, aclexplode(proc.proacl) acl
243
+ where proc.oid = to_regprocedure('a2_batch(text[], jsonb)')
244
+ and acl.grantee not in (0, proc.proowner)
245
+ loop
246
+ execute format('revoke execute on function a2_batch(text[], jsonb) from %s', grantee);
247
+ end loop;
248
+ end if;
249
+ -- It runs any statement it's handed, as its caller: only its owner, the role
250
+ -- that applies this file, may call it, and not every role, as Postgres lets
251
+ -- by default. Platforms that serve functions over an API would serve it too.
252
+ if pg_has_role(
253
+ current_user,
254
+ (select proowner from pg_proc
255
+ where oid = to_regprocedure('a2_batch(text[], jsonb)')),
256
+ 'usage'
257
+ ) then
258
+ revoke execute on function a2_batch(text[], jsonb) from public;
259
+ end if;
260
+ end $$;
261
+ --> statement-breakpoint
262
+
263
+ do $$ begin
264
+ if to_regclass('a2_events_types') is null then
265
+ create index if not exists a2_events_types
266
+ on a2_events (session_id, event_type, idx);
267
+ end if;
268
+ if to_regclass('a2_cursors_heads') is null then
269
+ create index if not exists a2_cursors_heads
270
+ on a2_cursors (session_id, head) where head is not null;
271
+ end if;
272
+ if to_regclass('a2_cursors_lanes') is null then
273
+ create index if not exists a2_cursors_lanes
274
+ on a2_cursors (session_id) where kind = 'lane';
275
+ end if;
276
+ if to_regclass('a2_snapshot_pins_checkpoint') is null then
277
+ create index if not exists a2_snapshot_pins_checkpoint
278
+ on a2_snapshot_pins (session_id, reducer_name, up_to_index);
279
+ end if;
280
+ end $$;
281
+ --> statement-breakpoint
282
+
283
+ -- Event ids are unique per session, through the index on (session_id,
284
+ -- event_id). It replaces a2_events_event_id, on event_id alone. The swap
285
+ -- locks the table before it builds, so it waits for running queries instead
286
+ -- of deadlocking with a writer when the whole file runs as one transaction.
287
+ -- The old index goes only once the new one is valid, so a session's ids
288
+ -- always have a unique index.
289
+ do $$ begin
290
+ if to_regclass('a2_events_session_event_id') is null then
291
+ if to_regclass('a2_events_event_id') is not null then
292
+ lock table a2_events in access exclusive mode;
293
+ end if;
294
+ create unique index if not exists a2_events_session_event_id
295
+ on a2_events (session_id, event_id);
296
+ end if;
297
+ if to_regclass('a2_events_event_id') is not null and exists (
298
+ select 1 from pg_index
299
+ where indexrelid = to_regclass('a2_events_session_event_id')
300
+ and indisvalid and indisunique
301
+ ) then
302
+ drop index a2_events_event_id;
303
+ end if;
304
+ end $$;
305
+ --> statement-breakpoint
306
+
307
+ -- board's tables, which live next to A2's. schema.sql holds both: apply that.
308
+
309
+ create table if not exists board_positions (
310
+ consumer text primary key,
311
+ log_index bigint not null
312
+ );
313
+ --> statement-breakpoint
314
+ create table if not exists board_messages (
315
+ id bigint primary key,
316
+ event_id text not null unique,
317
+ channel text not null,
318
+ re bigint,
319
+ by_actor text not null,
320
+ by_principal text not null,
321
+ by_profile text,
322
+ from_key text,
323
+ no_sync boolean not null,
324
+ text_lower text not null,
325
+ deleted boolean not null,
326
+ created_at bigint not null,
327
+ replies integer not null default 0,
328
+ at bigint,
329
+ data text not null
330
+ );
331
+ --> statement-breakpoint
332
+ create index if not exists board_messages_channel on board_messages (channel, id);
333
+ --> statement-breakpoint
334
+ create index if not exists board_messages_re on board_messages (re, id);
335
+ --> statement-breakpoint
336
+ create index if not exists board_messages_by_actor on board_messages (by_actor, id);
337
+ --> statement-breakpoint
338
+ create index if not exists board_messages_by_principal on board_messages (by_principal, id);
339
+ --> statement-breakpoint
340
+ create index if not exists board_threads_at on board_messages (at) where re is null;
341
+ --> statement-breakpoint
342
+ create index if not exists board_threads_channel on board_messages (channel, at) where re is null;
343
+ --> statement-breakpoint
344
+ create table if not exists board_versions (
345
+ log_index bigint primary key,
346
+ message_id bigint not null,
347
+ data text not null
348
+ );
349
+ --> statement-breakpoint
350
+ create index if not exists board_versions_message on board_versions (message_id, log_index);
351
+ --> statement-breakpoint
352
+ create table if not exists board_terms (
353
+ message_id bigint not null,
354
+ term text not null,
355
+ value text not null,
356
+ primary key (message_id, term, value)
357
+ );
358
+ --> statement-breakpoint
359
+ create index if not exists board_terms_term on board_terms (term, value, message_id);
360
+ --> statement-breakpoint
361
+ create table if not exists board_sync_threads (
362
+ thread text primary key,
363
+ data text not null
364
+ );
365
+ --> statement-breakpoint
366
+ create table if not exists board_external_threads (
367
+ external text primary key,
368
+ thread text not null
369
+ );
370
+ --> statement-breakpoint
371
+ create table if not exists board_copies (
372
+ message_id bigint not null,
373
+ target text not null,
374
+ data text not null,
375
+ primary key (message_id, target)
376
+ );
377
+ --> statement-breakpoint
378
+ create table if not exists board_external_messages (
379
+ key text primary key,
380
+ message_id bigint not null
381
+ );
382
+ --> statement-breakpoint
383
+ create index if not exists board_messages_recent on board_messages (by_principal, created_at) where re is null;
384
+ --> statement-breakpoint
385
+ drop function if exists board_apply(text, bigint, boolean, jsonb, jsonb, jsonb);
386
+ --> statement-breakpoint
387
+ create or replace function board_apply(
388
+ apply_consumer text,
389
+ apply_index bigint,
390
+ apply_next boolean,
391
+ apply_message jsonb,
392
+ apply_terms jsonb,
393
+ apply_summary jsonb,
394
+ apply_sync jsonb
395
+ ) returns boolean language plpgsql as $$
396
+ begin
397
+ if apply_next then
398
+ update board_positions set log_index = apply_index
399
+ where consumer = apply_consumer and log_index = apply_index - 1;
400
+ else
401
+ insert into board_positions (consumer, log_index) values (apply_consumer, apply_index)
402
+ on conflict (consumer) do update set log_index = excluded.log_index
403
+ where board_positions.log_index < excluded.log_index;
404
+ end if;
405
+ if not found then
406
+ return false;
407
+ end if;
408
+ if apply_message is not null then
409
+ if (apply_message->>'created')::boolean then
410
+ insert into board_messages
411
+ (id, event_id, channel, re, by_actor, by_principal, by_profile, from_key, no_sync, text_lower, deleted, created_at, data)
412
+ values (
413
+ (apply_message->>'id')::bigint, apply_message->>'event_id', apply_message->>'channel', (apply_message->>'re')::bigint,
414
+ apply_message->>'by_actor', apply_message->>'by_principal', apply_message->>'by_profile', apply_message->>'from_key',
415
+ (apply_message->>'no_sync')::boolean, apply_message->>'text_lower', (apply_message->>'deleted')::boolean,
416
+ (apply_message->>'created_at')::bigint, apply_message->>'data');
417
+ else
418
+ update board_messages
419
+ set text_lower = apply_message->>'text_lower', deleted = (apply_message->>'deleted')::boolean, data = apply_message->>'data'
420
+ where id = (apply_message->>'id')::bigint;
421
+ end if;
422
+ insert into board_versions (log_index, message_id, data)
423
+ values (apply_index, (apply_message->>'id')::bigint, apply_message->>'data');
424
+ if apply_terms is not null then
425
+ delete from board_terms where message_id = (apply_message->>'id')::bigint;
426
+ insert into board_terms (message_id, term, value)
427
+ select (apply_message->>'id')::bigint, term->>0, term->>1 from jsonb_array_elements(apply_terms) as term;
428
+ end if;
429
+ end if;
430
+ if apply_summary is not null then
431
+ update board_messages set replies = (apply_summary->>'replies')::int, at = (apply_summary->>'at')::bigint
432
+ where id = (apply_summary->>'root')::bigint;
433
+ end if;
434
+ if apply_sync is not null then
435
+ if jsonb_typeof(apply_sync->'thread') = 'object' then
436
+ insert into board_sync_threads (thread, data) values (apply_sync->'thread'->>'key', apply_sync->'thread'->>'data')
437
+ on conflict (thread) do update set data = excluded.data;
438
+ end if;
439
+ insert into board_external_threads (external, thread)
440
+ select entry->>0, entry->>1 from jsonb_array_elements(apply_sync->'external_threads') as entry
441
+ on conflict (external) do update set thread = excluded.thread;
442
+ insert into board_copies (message_id, target, data)
443
+ select (entry->>0)::bigint, entry->>1, entry->>2 from jsonb_array_elements(apply_sync->'copies') as entry
444
+ on conflict (message_id, target) do update set data = excluded.data;
445
+ insert into board_external_messages (key, message_id)
446
+ select entry->>0, (entry->>1)::bigint from jsonb_array_elements(apply_sync->'external_messages') as entry
447
+ on conflict (key) do update set message_id = excluded.message_id;
448
+ end if;
449
+ return true;
450
+ end
451
+ $$;