@hraness/peopleblade 0.1.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.
@@ -0,0 +1,389 @@
1
+ PRAGMA foreign_keys = ON;
2
+
3
+ DROP INDEX person_notes_source_identity;
4
+
5
+ CREATE UNIQUE INDEX person_notes_non_granola_source_identity
6
+ ON person_notes(source, source_id, person_id)
7
+ WHERE source_id IS NOT NULL AND source <> 'granola';
8
+
9
+ CREATE UNIQUE INDEX person_notes_granola_legacy_source_identity
10
+ ON person_notes(source, source_id, person_id)
11
+ WHERE source = 'granola'
12
+ AND source_id IS NOT NULL
13
+ AND json_type(metadata_json, '$.granolaAttendeeIdentity') IS NOT 'object';
14
+
15
+ CREATE INDEX person_notes_granola_source_id
16
+ ON person_notes(source_id, id)
17
+ WHERE source = 'granola' AND source_id IS NOT NULL;
18
+
19
+ CREATE TRIGGER person_notes_positive_id
20
+ AFTER INSERT ON person_notes FOR EACH ROW
21
+ WHEN NEW.id < 1
22
+ BEGIN SELECT RAISE(ABORT, 'person note ids must be positive'); END;
23
+
24
+ CREATE TRIGGER person_notes_no_replace
25
+ BEFORE INSERT ON person_notes FOR EACH ROW
26
+ WHEN EXISTS (
27
+ SELECT 1 FROM person_notes existing
28
+ WHERE (NEW.id IS NOT -1 AND existing.id = NEW.id)
29
+ OR (
30
+ NEW.source_id IS NOT NULL
31
+ AND existing.source = NEW.source
32
+ AND existing.source_id = NEW.source_id
33
+ AND (
34
+ (NEW.source <> 'granola' AND existing.person_id = NEW.person_id)
35
+ OR (
36
+ NEW.source = 'granola'
37
+ AND (
38
+ (
39
+ json_type(NEW.metadata_json, '$.granolaAttendeeIdentity') IS NOT 'object'
40
+ AND existing.person_id = NEW.person_id
41
+ )
42
+ OR (
43
+ json_type(NEW.metadata_json, '$.granolaAttendeeIdentity') = 'object'
44
+ AND json_type(existing.metadata_json, '$.granolaAttendeeIdentity') = 'object'
45
+ AND json_extract(
46
+ NEW.metadata_json, '$.granolaAttendeeIdentity.kind'
47
+ ) IS json_extract(
48
+ existing.metadata_json, '$.granolaAttendeeIdentity.kind'
49
+ )
50
+ AND json_extract(
51
+ NEW.metadata_json, '$.granolaAttendeeIdentity.valueSha256'
52
+ ) IS json_extract(
53
+ existing.metadata_json, '$.granolaAttendeeIdentity.valueSha256'
54
+ )
55
+ )
56
+ OR (
57
+ existing.person_id = NEW.person_id
58
+ AND json_type(
59
+ existing.metadata_json, '$.granolaAttendeeIdentity'
60
+ ) IS NOT 'object'
61
+ )
62
+ )
63
+ )
64
+ )
65
+ )
66
+ )
67
+ BEGIN SELECT RAISE(ABORT, 'person notes cannot be replaced'); END;
68
+
69
+ CREATE TABLE person_note_granola_identity_sources (
70
+ note_id INTEGER PRIMARY KEY CHECK(note_id >= 1)
71
+ REFERENCES person_notes(id) ON DELETE RESTRICT,
72
+ source_id TEXT NOT NULL CHECK(length(source_id) BETWEEN 1 AND 256),
73
+ source_contact_method_id INTEGER NOT NULL CHECK(source_contact_method_id >= 1)
74
+ REFERENCES contact_methods(id) ON DELETE RESTRICT,
75
+ identity_kind TEXT NOT NULL CHECK(identity_kind IN ('email','phone')),
76
+ identity_value_sha256 TEXT NOT NULL CHECK(
77
+ length(identity_value_sha256) = 64
78
+ AND identity_value_sha256 NOT GLOB '*[^0-9a-f]*'
79
+ ),
80
+ created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
81
+ UNIQUE(source_id, identity_kind, identity_value_sha256)
82
+ ) STRICT;
83
+
84
+ CREATE INDEX person_note_granola_identity_sources_method
85
+ ON person_note_granola_identity_sources(source_contact_method_id);
86
+
87
+ CREATE TRIGGER person_note_granola_identity_source_valid
88
+ BEFORE INSERT ON person_note_granola_identity_sources FOR EACH ROW
89
+ BEGIN
90
+ SELECT CASE WHEN
91
+ EXISTS (
92
+ SELECT 1 FROM person_note_granola_identity_sources existing
93
+ WHERE existing.note_id = NEW.note_id
94
+ OR (
95
+ existing.source_id = NEW.source_id
96
+ AND existing.identity_kind = NEW.identity_kind
97
+ AND existing.identity_value_sha256 = NEW.identity_value_sha256
98
+ )
99
+ )
100
+ OR NOT EXISTS (
101
+ SELECT 1
102
+ FROM person_notes note
103
+ JOIN contact_methods method ON method.id = NEW.source_contact_method_id
104
+ LEFT JOIN provider_resources resource ON resource.id = method.provider_resource_id
105
+ LEFT JOIN source_realms realm ON realm.id = resource.source_realm_id
106
+ JOIN person_identity_components method_component
107
+ ON method_component.person_id = method.person_id
108
+ JOIN person_identity_components note_component
109
+ ON note_component.person_id = note.person_id
110
+ WHERE note.id = NEW.note_id
111
+ AND note.source = 'granola'
112
+ AND note.source_id = NEW.source_id
113
+ AND json_type(note.metadata_json, '$.granolaAttendeeIdentity') = 'object'
114
+ AND json_extract(note.metadata_json, '$.granolaAttendeeIdentity.schemaVersion') = 1
115
+ AND json_extract(note.metadata_json, '$.granolaAttendeeIdentity.kind') = NEW.identity_kind
116
+ AND json_extract(
117
+ note.metadata_json, '$.granolaAttendeeIdentity.valueSha256'
118
+ ) = NEW.identity_value_sha256
119
+ AND json_type(
120
+ note.metadata_json, '$.granolaAttendeeIdentity.contactMethodId'
121
+ ) = 'integer'
122
+ AND json_extract(
123
+ note.metadata_json, '$.granolaAttendeeIdentity.contactMethodId'
124
+ ) = NEW.source_contact_method_id
125
+ AND method.kind = NEW.identity_kind
126
+ AND method.confidence = 'exact'
127
+ AND method.active = 1
128
+ AND method.identity_eligible = 1
129
+ AND method_component.canonical_person_id = note_component.canonical_person_id
130
+ AND (
131
+ method.provider_resource_id IS NULL
132
+ OR (resource.active = 1 AND (realm.id IS NULL OR realm.active = 1))
133
+ )
134
+ )
135
+ THEN RAISE(ABORT, 'Granola note identity source is not exact') END;
136
+ END;
137
+
138
+ CREATE TRIGGER person_note_granola_identity_sources_immutable_update
139
+ BEFORE UPDATE ON person_note_granola_identity_sources FOR EACH ROW
140
+ BEGIN SELECT RAISE(ABORT, 'Granola note identity sources are immutable'); END;
141
+
142
+ CREATE TRIGGER person_note_granola_identity_sources_immutable_delete
143
+ BEFORE DELETE ON person_note_granola_identity_sources FOR EACH ROW
144
+ BEGIN SELECT RAISE(ABORT, 'Granola note identity sources are immutable'); END;
145
+
146
+ CREATE TABLE person_note_attribution_revisions (
147
+ id INTEGER PRIMARY KEY AUTOINCREMENT CHECK(id >= 1),
148
+ note_id INTEGER NOT NULL
149
+ REFERENCES person_note_granola_identity_sources(note_id) ON DELETE RESTRICT,
150
+ source_id TEXT NOT NULL CHECK(length(source_id) BETWEEN 1 AND 256),
151
+ from_person_id INTEGER NOT NULL REFERENCES people(id) ON DELETE RESTRICT,
152
+ to_person_id INTEGER NOT NULL REFERENCES people(id) ON DELETE RESTRICT,
153
+ target_contact_method_id INTEGER NOT NULL CHECK(target_contact_method_id >= 1)
154
+ REFERENCES contact_methods(id) ON DELETE RESTRICT,
155
+ identity_kind TEXT NOT NULL CHECK(identity_kind IN ('email','phone')),
156
+ identity_value_sha256 TEXT NOT NULL CHECK(
157
+ length(identity_value_sha256) = 64
158
+ AND identity_value_sha256 NOT GLOB '*[^0-9a-f]*'
159
+ ),
160
+ identity_revision INTEGER NOT NULL CHECK(identity_revision >= 0),
161
+ reason TEXT NOT NULL CHECK(reason = 'exact-granola-source-replay'),
162
+ created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
163
+ CHECK(from_person_id <> to_person_id)
164
+ ) STRICT;
165
+
166
+ CREATE INDEX person_note_attribution_revisions_note
167
+ ON person_note_attribution_revisions(note_id, id);
168
+
169
+ CREATE INDEX person_note_attribution_revisions_target_method
170
+ ON person_note_attribution_revisions(target_contact_method_id);
171
+
172
+ CREATE TRIGGER person_note_attribution_revision_valid
173
+ BEFORE INSERT ON person_note_attribution_revisions FOR EACH ROW
174
+ BEGIN
175
+ SELECT CASE WHEN
176
+ NEW.id IS NOT -1
177
+ OR NEW.from_person_id IS NOT coalesce(
178
+ (SELECT prior.to_person_id
179
+ FROM person_note_attribution_revisions prior
180
+ WHERE prior.note_id = NEW.note_id
181
+ ORDER BY prior.id DESC LIMIT 1),
182
+ (SELECT note.person_id FROM person_notes note WHERE note.id = NEW.note_id)
183
+ )
184
+ OR NOT EXISTS (
185
+ SELECT 1
186
+ FROM person_note_granola_identity_sources source
187
+ JOIN person_notes note ON note.id = source.note_id
188
+ JOIN contact_methods original_method
189
+ ON original_method.id = source.source_contact_method_id
190
+ JOIN contact_methods method ON method.id = NEW.target_contact_method_id
191
+ LEFT JOIN provider_resources resource ON resource.id = method.provider_resource_id
192
+ LEFT JOIN source_realms realm ON realm.id = resource.source_realm_id
193
+ JOIN person_identity_components method_component
194
+ ON method_component.person_id = method.person_id
195
+ JOIN person_identity_components target_component
196
+ ON target_component.person_id = NEW.to_person_id
197
+ WHERE source.note_id = NEW.note_id
198
+ AND source.source_id = NEW.source_id
199
+ AND source.identity_kind = NEW.identity_kind
200
+ AND source.identity_value_sha256 = NEW.identity_value_sha256
201
+ AND note.source = 'granola'
202
+ AND note.source_id = source.source_id
203
+ AND json_type(note.metadata_json, '$.granolaAttendeeIdentity') = 'object'
204
+ AND json_extract(note.metadata_json, '$.granolaAttendeeIdentity.schemaVersion') = 1
205
+ AND json_extract(note.metadata_json, '$.granolaAttendeeIdentity.kind')
206
+ = source.identity_kind
207
+ AND json_extract(
208
+ note.metadata_json, '$.granolaAttendeeIdentity.valueSha256'
209
+ ) = source.identity_value_sha256
210
+ AND json_extract(
211
+ note.metadata_json, '$.granolaAttendeeIdentity.contactMethodId'
212
+ ) = source.source_contact_method_id
213
+ AND original_method.kind = source.identity_kind
214
+ AND method.kind = NEW.identity_kind
215
+ AND method.confidence = 'exact'
216
+ AND method.active = 1
217
+ AND method.identity_eligible = 1
218
+ AND original_method.normalized_value = method.normalized_value
219
+ AND method_component.canonical_person_id = target_component.canonical_person_id
220
+ AND (
221
+ method.provider_resource_id IS NULL
222
+ OR (resource.active = 1 AND (realm.id IS NULL OR realm.active = 1))
223
+ )
224
+ AND (
225
+ NOT EXISTS (
226
+ SELECT 1
227
+ FROM contact_methods rival
228
+ LEFT JOIN provider_resources rival_resource
229
+ ON rival_resource.id = rival.provider_resource_id
230
+ LEFT JOIN source_realms rival_realm
231
+ ON rival_realm.id = rival_resource.source_realm_id
232
+ JOIN person_identity_components rival_component
233
+ ON rival_component.person_id = rival.person_id
234
+ WHERE rival.kind = method.kind
235
+ AND rival.normalized_value = method.normalized_value
236
+ AND rival.confidence = 'exact'
237
+ AND rival.active = 1
238
+ AND rival.identity_eligible = 1
239
+ AND (
240
+ rival.provider_resource_id IS NULL
241
+ OR (
242
+ rival_resource.active = 1
243
+ AND (rival_realm.id IS NULL OR rival_realm.active = 1)
244
+ )
245
+ )
246
+ AND rival_component.canonical_person_id
247
+ <> target_component.canonical_person_id
248
+ )
249
+ OR (
250
+ method.kind = 'email'
251
+ AND method.provider_resource_id IS NULL
252
+ AND json_extract(method.metadata_json, '$.source')
253
+ = 'explicit-user-attestation'
254
+ AND json_extract(method.metadata_json, '$.conflictOverride') = 1
255
+ AND json_type(method.metadata_json, '$.reviewedConflictingMethods') = 'array'
256
+ AND (
257
+ SELECT count(*)
258
+ FROM json_each(
259
+ method.metadata_json, '$.reviewedConflictingMethods'
260
+ ) reviewed
261
+ ) = (
262
+ SELECT count(*)
263
+ FROM contact_methods rival
264
+ LEFT JOIN provider_resources rival_resource
265
+ ON rival_resource.id = rival.provider_resource_id
266
+ LEFT JOIN source_realms rival_realm
267
+ ON rival_realm.id = rival_resource.source_realm_id
268
+ JOIN person_identity_components rival_component
269
+ ON rival_component.person_id = rival.person_id
270
+ WHERE rival.kind = method.kind
271
+ AND rival.normalized_value = method.normalized_value
272
+ AND rival.confidence = 'exact'
273
+ AND rival.active = 1
274
+ AND rival.identity_eligible = 1
275
+ AND (
276
+ rival.provider_resource_id IS NULL
277
+ OR (
278
+ rival_resource.active = 1
279
+ AND (rival_realm.id IS NULL OR rival_realm.active = 1)
280
+ )
281
+ )
282
+ AND rival_component.canonical_person_id
283
+ <> target_component.canonical_person_id
284
+ )
285
+ AND NOT EXISTS (
286
+ SELECT 1
287
+ FROM contact_methods rival
288
+ LEFT JOIN provider_resources rival_resource
289
+ ON rival_resource.id = rival.provider_resource_id
290
+ LEFT JOIN source_realms rival_realm
291
+ ON rival_realm.id = rival_resource.source_realm_id
292
+ JOIN person_identity_components rival_component
293
+ ON rival_component.person_id = rival.person_id
294
+ WHERE rival.kind = method.kind
295
+ AND rival.normalized_value = method.normalized_value
296
+ AND rival.confidence = 'exact'
297
+ AND rival.active = 1
298
+ AND rival.identity_eligible = 1
299
+ AND (
300
+ rival.provider_resource_id IS NULL
301
+ OR (
302
+ rival_resource.active = 1
303
+ AND (rival_realm.id IS NULL OR rival_realm.active = 1)
304
+ )
305
+ )
306
+ AND rival_component.canonical_person_id
307
+ <> target_component.canonical_person_id
308
+ AND NOT EXISTS (
309
+ SELECT 1
310
+ FROM json_each(
311
+ method.metadata_json, '$.reviewedConflictingMethods'
312
+ ) reviewed
313
+ WHERE json_type(reviewed.value) = 'object'
314
+ AND json_extract(reviewed.value, '$.methodId') = rival.id
315
+ AND json_extract(reviewed.value, '$.personId') = rival.person_id
316
+ AND json_extract(reviewed.value, '$.canonicalPersonId')
317
+ = rival_component.canonical_person_id
318
+ AND json_extract(reviewed.value, '$.provider')
319
+ IS rival_resource.provider
320
+ )
321
+ )
322
+ )
323
+ )
324
+ )
325
+ OR NEW.identity_revision IS NOT (
326
+ SELECT identity_revision FROM local_revisions WHERE singleton = 1
327
+ )
328
+ THEN RAISE(ABORT, 'Granola attribution revision is not exact and current') END;
329
+ END;
330
+
331
+ CREATE TRIGGER person_note_attribution_revisions_immutable_update
332
+ BEFORE UPDATE ON person_note_attribution_revisions FOR EACH ROW
333
+ BEGIN SELECT RAISE(ABORT, 'person note attribution revisions are immutable'); END;
334
+
335
+ CREATE TRIGGER person_note_attribution_revisions_immutable_delete
336
+ BEFORE DELETE ON person_note_attribution_revisions FOR EACH ROW
337
+ BEGIN SELECT RAISE(ABORT, 'person note attribution revisions are immutable'); END;
338
+
339
+ CREATE TRIGGER person_note_identity_method_coordinate_immutable_update
340
+ BEFORE UPDATE OF id, person_id, kind, normalized_value
341
+ ON contact_methods FOR EACH ROW
342
+ WHEN (
343
+ NEW.id IS NOT OLD.id
344
+ OR NEW.person_id IS NOT OLD.person_id
345
+ OR NEW.kind IS NOT OLD.kind
346
+ OR NEW.normalized_value IS NOT OLD.normalized_value
347
+ )
348
+ AND (
349
+ EXISTS (
350
+ SELECT 1 FROM person_note_granola_identity_sources source
351
+ WHERE source.source_contact_method_id = OLD.id
352
+ )
353
+ OR EXISTS (
354
+ SELECT 1 FROM person_note_attribution_revisions revision
355
+ WHERE revision.target_contact_method_id = OLD.id
356
+ )
357
+ )
358
+ BEGIN SELECT RAISE(ABORT, 'person note identity method coordinate is immutable'); END;
359
+
360
+ CREATE TRIGGER person_note_identity_method_immutable_delete
361
+ BEFORE DELETE ON contact_methods FOR EACH ROW
362
+ WHEN EXISTS (
363
+ SELECT 1 FROM person_note_granola_identity_sources source
364
+ WHERE source.source_contact_method_id = OLD.id
365
+ )
366
+ OR EXISTS (
367
+ SELECT 1 FROM person_note_attribution_revisions revision
368
+ WHERE revision.target_contact_method_id = OLD.id
369
+ )
370
+ BEGIN SELECT RAISE(ABORT, 'person note identity method is immutable'); END;
371
+
372
+ CREATE VIEW person_notes_current AS
373
+ SELECT
374
+ note.id,
375
+ coalesce(
376
+ (SELECT revision.to_person_id
377
+ FROM person_note_attribution_revisions revision
378
+ WHERE revision.note_id = note.id
379
+ ORDER BY revision.id DESC LIMIT 1),
380
+ note.person_id
381
+ ) AS person_id,
382
+ note.occurred_at,
383
+ note.source,
384
+ note.source_id,
385
+ note.title,
386
+ note.body,
387
+ note.metadata_json,
388
+ note.created_at
389
+ FROM person_notes note;