@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.
- package/LICENSE +21 -0
- package/README.md +79 -0
- package/THIRD_PARTY_NOTICES.md +30 -0
- package/dist/cli.ts +61 -0
- package/dist/migrations/001_core.sql +223 -0
- package/dist/migrations/002_incremental_sources.sql +93 -0
- package/dist/migrations/003_identity_review.sql +67 -0
- package/dist/migrations/004_source_realms_and_observations.sql +91 -0
- package/dist/migrations/005_identity_representatives.sql +87 -0
- package/dist/migrations/006_profile_url_trust.sql +15 -0
- package/dist/migrations/007_person_notes.sql +39 -0
- package/dist/migrations/008_name_identity_trust.sql +33 -0
- package/dist/migrations/009_provider_collection_states.sql +23 -0
- package/dist/migrations/010_source_execution_receipts.sql +51 -0
- package/dist/migrations/011_source_account_convergence.sql +125 -0
- package/dist/migrations/012_granola_note_attribution.sql +389 -0
- package/dist/migrations/013_source_binding_incarnations.sql +339 -0
- package/dist/migrations/014_source_binding_transition_guard_recovery.sql +121 -0
- package/dist/peopleblade.js +20203 -0
- package/package.json +36 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Hraness
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# @hraness/peopleblade
|
|
2
|
+
|
|
3
|
+
PeopleBlade is a local-first, source-aware contact graph for people and agents.
|
|
4
|
+
The authoritative database stays in private SQLite on your machine. Cloud sign-in
|
|
5
|
+
and sync are optional.
|
|
6
|
+
|
|
7
|
+
## Requirements
|
|
8
|
+
|
|
9
|
+
- Bun 1.3.14 or newer
|
|
10
|
+
- macOS or Linux for the currently tested local workflows
|
|
11
|
+
- Provider-specific setup only for live sources you choose to import
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
bun add --global @hraness/peopleblade@0.1.0
|
|
17
|
+
peopleblade --version
|
|
18
|
+
peopleblade init
|
|
19
|
+
peopleblade stats --json
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
The default database is:
|
|
23
|
+
|
|
24
|
+
- macOS: `~/Library/Application Support/PeopleBlade/peopleblade.sqlite3`
|
|
25
|
+
- Linux: `${XDG_DATA_HOME:-~/.local/share}/peopleblade/peopleblade.sqlite3`
|
|
26
|
+
|
|
27
|
+
Use `peopleblade --help` to list source, identity-review, notes, research, backup,
|
|
28
|
+
and optional cloud commands. Every schema upgrade of an existing database creates
|
|
29
|
+
a private backup before applying migrations. `peopleblade backup` creates an
|
|
30
|
+
additional consistent SQLite backup on demand.
|
|
31
|
+
|
|
32
|
+
Optional cloud credentials are revocable. Use `peopleblade cloud devices` to list
|
|
33
|
+
authorized devices, `peopleblade cloud revoke DEVICE_ID` for a lost device, and
|
|
34
|
+
`peopleblade cloud signout` to revoke the current device before removing its local
|
|
35
|
+
token. The signed-in dashboard provides the lost-device recovery path when no CLI
|
|
36
|
+
token remains available.
|
|
37
|
+
|
|
38
|
+
### Optional Wrench-backed sources
|
|
39
|
+
|
|
40
|
+
The package does not install Wrench, browser automation, KB, native model runtimes,
|
|
41
|
+
or provider credentials. Beeper, Google, and WhatsApp live sync commands delegate
|
|
42
|
+
through a separately reviewed `wrench` executable on `PATH` (or the absolute
|
|
43
|
+
`PEOPLEBLADE_WRENCH_EXECUTABLE` path). PeopleBlade bundles only the pinned Wrench
|
|
44
|
+
0.15.0 client validators needed to fence requests and receipts; it does not bundle
|
|
45
|
+
or silently download Wrench's execution runtime.
|
|
46
|
+
|
|
47
|
+
Existing reviewed Wrench 0.15.0 environments remain supported. A fresh public
|
|
48
|
+
Wrench setup is intentionally not prescribed here until Wrench publishes a narrow,
|
|
49
|
+
immutable runtime graph. Archive imports, Apple Contacts, iMessage, notes, identity
|
|
50
|
+
review, backups, and optional PeopleBlade cloud commands do not require Wrench.
|
|
51
|
+
|
|
52
|
+
## Privacy boundary
|
|
53
|
+
|
|
54
|
+
PeopleBlade's provider import paths never retain, search, enrich from, or upload
|
|
55
|
+
message text. The optional cloud projection excludes source records, raw archives,
|
|
56
|
+
local paths, provider credentials, and message bodies. Ambiguous identity links
|
|
57
|
+
remain reviewable instead of being silently merged.
|
|
58
|
+
|
|
59
|
+
This npm artifact contains only the bundled local CLI, its pinned Wrench client
|
|
60
|
+
validators and executable bridge, SQLite migrations, and package documentation.
|
|
61
|
+
It does not contain the Wrench execution runtime, PeopleBlade web server, billing
|
|
62
|
+
implementation, operational tooling, tests, or private product documentation.
|
|
63
|
+
|
|
64
|
+
## Update or remove
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
bun update --global @hraness/peopleblade
|
|
68
|
+
bun remove --global @hraness/peopleblade
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Removing the package does not delete the local SQLite database or its backups.
|
|
72
|
+
See [peopleblade.com](https://peopleblade.com) for supported sources and current
|
|
73
|
+
product boundaries.
|
|
74
|
+
|
|
75
|
+
## License
|
|
76
|
+
|
|
77
|
+
The public PeopleBlade CLI package is available under the MIT License. This
|
|
78
|
+
license applies to the npm package, not to PeopleBlade's private repository or
|
|
79
|
+
hosted service.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Third-party notices
|
|
2
|
+
|
|
3
|
+
The PeopleBlade CLI bundle includes reviewed client-boundary code from
|
|
4
|
+
`@hraness/wrench` 0.15.0 (commit
|
|
5
|
+
`41b16fcbf9ed0e8d8a332aa380b7187642aac2e3`) and message-bundle contract code
|
|
6
|
+
from `@hraness/message-like-me` 0.4.0. Those portions are provided under the
|
|
7
|
+
following license. The PeopleBlade CLI package has its own MIT grant in
|
|
8
|
+
`LICENSE`; this notice preserves the attribution for the bundled portions.
|
|
9
|
+
|
|
10
|
+
MIT License
|
|
11
|
+
|
|
12
|
+
Copyright (c) 2026 Hraness contributors
|
|
13
|
+
|
|
14
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
15
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
16
|
+
in the Software without restriction, including without limitation the rights
|
|
17
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
18
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
19
|
+
furnished to do so, subject to the following conditions:
|
|
20
|
+
|
|
21
|
+
The above copyright notice and this permission notice shall be included in all
|
|
22
|
+
copies or substantial portions of the Software.
|
|
23
|
+
|
|
24
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
25
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
26
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
27
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
28
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
29
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
30
|
+
SOFTWARE.
|
package/dist/cli.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { accessSync, constants, realpathSync } from "node:fs";
|
|
4
|
+
import { isAbsolute, resolve } from "node:path";
|
|
5
|
+
|
|
6
|
+
const configured = process.env.PEOPLEBLADE_WRENCH_EXECUTABLE;
|
|
7
|
+
const executable = configured === undefined
|
|
8
|
+
? Bun.which("wrench")
|
|
9
|
+
: isAbsolute(configured)
|
|
10
|
+
? resolve(configured)
|
|
11
|
+
: null;
|
|
12
|
+
|
|
13
|
+
if (executable === null) {
|
|
14
|
+
console.error(
|
|
15
|
+
configured === undefined
|
|
16
|
+
? "PeopleBlade Wrench-backed sources require a separately reviewed `wrench` executable on PATH."
|
|
17
|
+
: "PEOPLEBLADE_WRENCH_EXECUTABLE must be an absolute executable path.",
|
|
18
|
+
);
|
|
19
|
+
process.exit(127);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let canonicalExecutable: string;
|
|
23
|
+
try {
|
|
24
|
+
accessSync(executable, constants.X_OK);
|
|
25
|
+
canonicalExecutable = realpathSync(executable);
|
|
26
|
+
} catch {
|
|
27
|
+
console.error("The configured PeopleBlade Wrench executable is unavailable or not executable.");
|
|
28
|
+
process.exit(127);
|
|
29
|
+
}
|
|
30
|
+
if (process.argv[1] !== undefined && canonicalExecutable === realpathSync(process.argv[1])) {
|
|
31
|
+
console.error("The PeopleBlade Wrench executable bridge cannot delegate to itself.");
|
|
32
|
+
process.exit(127);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const child = Bun.spawn([canonicalExecutable, ...process.argv.slice(2)], {
|
|
36
|
+
cwd: process.cwd(),
|
|
37
|
+
env: process.env,
|
|
38
|
+
stdin: "inherit",
|
|
39
|
+
stdout: "inherit",
|
|
40
|
+
stderr: "inherit",
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
let terminationTimer: ReturnType<typeof setTimeout> | null = null;
|
|
44
|
+
const forward = (signal: NodeJS.Signals) => {
|
|
45
|
+
try { child.kill(signal); } catch { /* child already settled */ }
|
|
46
|
+
terminationTimer ??= setTimeout(() => {
|
|
47
|
+
try { child.kill("SIGKILL"); } catch { /* child already settled */ }
|
|
48
|
+
}, 5_000);
|
|
49
|
+
terminationTimer.unref();
|
|
50
|
+
};
|
|
51
|
+
const interrupt = () => forward("SIGINT");
|
|
52
|
+
const terminate = () => forward("SIGTERM");
|
|
53
|
+
process.on("SIGINT", interrupt);
|
|
54
|
+
process.on("SIGTERM", terminate);
|
|
55
|
+
try {
|
|
56
|
+
process.exitCode = await child.exited;
|
|
57
|
+
} finally {
|
|
58
|
+
if (terminationTimer !== null) clearTimeout(terminationTimer);
|
|
59
|
+
process.off("SIGINT", interrupt);
|
|
60
|
+
process.off("SIGTERM", terminate);
|
|
61
|
+
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
PRAGMA foreign_keys = ON;
|
|
2
|
+
|
|
3
|
+
CREATE TABLE local_state (
|
|
4
|
+
singleton INTEGER PRIMARY KEY CHECK(singleton = 1),
|
|
5
|
+
instance_id TEXT NOT NULL UNIQUE CHECK(length(instance_id) = 64 AND instance_id NOT GLOB '*[^0-9a-f]*'),
|
|
6
|
+
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
7
|
+
) STRICT;
|
|
8
|
+
INSERT INTO local_state(singleton, instance_id) VALUES (1, lower(hex(randomblob(32))));
|
|
9
|
+
|
|
10
|
+
CREATE TABLE people (
|
|
11
|
+
id INTEGER PRIMARY KEY,
|
|
12
|
+
stable_key TEXT NOT NULL UNIQUE CHECK(length(stable_key) BETWEEN 3 AND 256),
|
|
13
|
+
display_name TEXT,
|
|
14
|
+
given_name TEXT,
|
|
15
|
+
middle_name TEXT,
|
|
16
|
+
family_name TEXT,
|
|
17
|
+
name_prefix TEXT,
|
|
18
|
+
name_suffix TEXT,
|
|
19
|
+
nickname TEXT,
|
|
20
|
+
organization TEXT,
|
|
21
|
+
department TEXT,
|
|
22
|
+
title TEXT,
|
|
23
|
+
birthday TEXT,
|
|
24
|
+
notes TEXT,
|
|
25
|
+
do_not_contact INTEGER NOT NULL DEFAULT 0 CHECK(do_not_contact IN (0,1)),
|
|
26
|
+
metadata_json TEXT NOT NULL DEFAULT '{}' CHECK(json_valid(metadata_json)),
|
|
27
|
+
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
28
|
+
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
29
|
+
) STRICT;
|
|
30
|
+
CREATE INDEX people_name ON people(display_name COLLATE NOCASE, id);
|
|
31
|
+
|
|
32
|
+
CREATE TABLE source_runs (
|
|
33
|
+
id INTEGER PRIMARY KEY,
|
|
34
|
+
provider TEXT NOT NULL CHECK(length(provider) BETWEEN 1 AND 64),
|
|
35
|
+
account_key TEXT NOT NULL COLLATE NOCASE CHECK(length(account_key) BETWEEN 1 AND 256),
|
|
36
|
+
mode TEXT NOT NULL CHECK(length(mode) BETWEEN 1 AND 64),
|
|
37
|
+
source_sha256 TEXT NOT NULL CHECK(length(source_sha256) = 64 AND source_sha256 NOT GLOB '*[^0-9a-f]*'),
|
|
38
|
+
source_locator_sha256 TEXT CHECK(source_locator_sha256 IS NULL OR (length(source_locator_sha256) = 64 AND source_locator_sha256 NOT GLOB '*[^0-9a-f]*')),
|
|
39
|
+
completeness TEXT NOT NULL CHECK(completeness IN ('complete','partial','lower-bound','unknown')),
|
|
40
|
+
result_json TEXT NOT NULL CHECK(json_valid(result_json)),
|
|
41
|
+
started_at TEXT NOT NULL,
|
|
42
|
+
completed_at TEXT,
|
|
43
|
+
UNIQUE(provider, account_key, mode, source_sha256)
|
|
44
|
+
) STRICT;
|
|
45
|
+
|
|
46
|
+
CREATE TABLE contact_methods (
|
|
47
|
+
id INTEGER PRIMARY KEY,
|
|
48
|
+
person_id INTEGER NOT NULL REFERENCES people(id) ON DELETE CASCADE,
|
|
49
|
+
kind TEXT NOT NULL CHECK(kind IN ('email','phone','url','address','date','social','other')),
|
|
50
|
+
value TEXT NOT NULL CHECK(length(value) BETWEEN 1 AND 8192),
|
|
51
|
+
normalized_value TEXT NOT NULL CHECK(length(normalized_value) BETWEEN 1 AND 8192),
|
|
52
|
+
label TEXT NOT NULL DEFAULT '' CHECK(length(label) <= 256),
|
|
53
|
+
is_primary INTEGER NOT NULL DEFAULT 0 CHECK(is_primary IN (0,1)),
|
|
54
|
+
confidence TEXT NOT NULL DEFAULT 'unknown' CHECK(confidence IN ('exact','likely','possible','unknown')),
|
|
55
|
+
first_seen_run_id INTEGER REFERENCES source_runs(id) ON DELETE SET NULL,
|
|
56
|
+
last_seen_run_id INTEGER REFERENCES source_runs(id) ON DELETE SET NULL,
|
|
57
|
+
metadata_json TEXT NOT NULL DEFAULT '{}' CHECK(json_valid(metadata_json)),
|
|
58
|
+
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
59
|
+
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
60
|
+
UNIQUE(person_id, kind, normalized_value, label)
|
|
61
|
+
) STRICT;
|
|
62
|
+
CREATE INDEX contact_methods_lookup ON contact_methods(kind, normalized_value, person_id);
|
|
63
|
+
|
|
64
|
+
CREATE TABLE provider_resources (
|
|
65
|
+
id INTEGER PRIMARY KEY,
|
|
66
|
+
provider TEXT NOT NULL CHECK(length(provider) BETWEEN 1 AND 64),
|
|
67
|
+
account_key TEXT NOT NULL COLLATE NOCASE CHECK(length(account_key) BETWEEN 1 AND 256),
|
|
68
|
+
resource_type TEXT NOT NULL CHECK(length(resource_type) BETWEEN 1 AND 64),
|
|
69
|
+
resource_id TEXT NOT NULL CHECK(length(resource_id) BETWEEN 1 AND 4096),
|
|
70
|
+
person_id INTEGER NOT NULL REFERENCES people(id) ON DELETE RESTRICT,
|
|
71
|
+
username TEXT,
|
|
72
|
+
profile_url TEXT,
|
|
73
|
+
display_name TEXT,
|
|
74
|
+
active INTEGER NOT NULL DEFAULT 1 CHECK(active IN (0,1)),
|
|
75
|
+
first_seen_run_id INTEGER REFERENCES source_runs(id) ON DELETE RESTRICT,
|
|
76
|
+
last_seen_run_id INTEGER REFERENCES source_runs(id) ON DELETE RESTRICT,
|
|
77
|
+
metadata_json TEXT NOT NULL DEFAULT '{}' CHECK(json_valid(metadata_json)),
|
|
78
|
+
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
79
|
+
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
80
|
+
UNIQUE(provider, account_key, resource_type, resource_id)
|
|
81
|
+
) STRICT;
|
|
82
|
+
CREATE INDEX provider_resources_person ON provider_resources(person_id, provider, active);
|
|
83
|
+
|
|
84
|
+
CREATE TABLE source_records (
|
|
85
|
+
run_id INTEGER NOT NULL REFERENCES source_runs(id) ON DELETE RESTRICT,
|
|
86
|
+
ordinal INTEGER NOT NULL CHECK(ordinal > 0),
|
|
87
|
+
record_kind TEXT NOT NULL CHECK(length(record_kind) BETWEEN 1 AND 64),
|
|
88
|
+
record_sha256 TEXT NOT NULL CHECK(length(record_sha256) = 64 AND record_sha256 NOT GLOB '*[^0-9a-f]*'),
|
|
89
|
+
person_id INTEGER REFERENCES people(id) ON DELETE RESTRICT,
|
|
90
|
+
provider_resource_id INTEGER REFERENCES provider_resources(id) ON DELETE RESTRICT,
|
|
91
|
+
resolution TEXT NOT NULL CHECK(resolution IN ('imported','matched','updated','skipped','conflict','error')),
|
|
92
|
+
projection_json TEXT NOT NULL CHECK(json_valid(projection_json)),
|
|
93
|
+
error_text TEXT,
|
|
94
|
+
PRIMARY KEY(run_id, ordinal)
|
|
95
|
+
) STRICT;
|
|
96
|
+
CREATE INDEX source_records_person ON source_records(person_id, run_id);
|
|
97
|
+
|
|
98
|
+
CREATE TABLE interaction_metrics (
|
|
99
|
+
provider TEXT NOT NULL CHECK(length(provider) BETWEEN 1 AND 64),
|
|
100
|
+
account_key TEXT NOT NULL COLLATE NOCASE CHECK(length(account_key) BETWEEN 1 AND 256),
|
|
101
|
+
person_id INTEGER NOT NULL REFERENCES people(id) ON DELETE CASCADE,
|
|
102
|
+
sent_count INTEGER NOT NULL DEFAULT 0 CHECK(sent_count >= 0),
|
|
103
|
+
received_count INTEGER NOT NULL DEFAULT 0 CHECK(received_count >= 0),
|
|
104
|
+
interaction_count INTEGER NOT NULL DEFAULT 0 CHECK(interaction_count >= 0),
|
|
105
|
+
conversation_count INTEGER CHECK(conversation_count IS NULL OR conversation_count >= 0),
|
|
106
|
+
first_interaction_at TEXT,
|
|
107
|
+
last_interaction_at TEXT,
|
|
108
|
+
reciprocal INTEGER NOT NULL DEFAULT 0 CHECK(reciprocal IN (0,1)),
|
|
109
|
+
completeness TEXT NOT NULL CHECK(completeness IN ('complete','partial','lower-bound','unknown')),
|
|
110
|
+
last_seen_run_id INTEGER REFERENCES source_runs(id) ON DELETE SET NULL,
|
|
111
|
+
metadata_json TEXT NOT NULL DEFAULT '{}' CHECK(json_valid(metadata_json)),
|
|
112
|
+
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
113
|
+
PRIMARY KEY(provider, account_key, person_id)
|
|
114
|
+
) STRICT;
|
|
115
|
+
CREATE INDEX interaction_metrics_person ON interaction_metrics(person_id, provider);
|
|
116
|
+
|
|
117
|
+
CREATE TABLE interaction_events (
|
|
118
|
+
id INTEGER PRIMARY KEY,
|
|
119
|
+
provider TEXT NOT NULL CHECK(length(provider) BETWEEN 1 AND 64),
|
|
120
|
+
account_key TEXT NOT NULL COLLATE NOCASE CHECK(length(account_key) BETWEEN 1 AND 256),
|
|
121
|
+
external_id TEXT NOT NULL CHECK(length(external_id) BETWEEN 1 AND 4096),
|
|
122
|
+
person_id INTEGER REFERENCES people(id) ON DELETE RESTRICT,
|
|
123
|
+
occurred_at TEXT,
|
|
124
|
+
direction TEXT CHECK(direction IN ('incoming','outgoing','unknown')),
|
|
125
|
+
conversation_sha256 TEXT CHECK(conversation_sha256 IS NULL OR (length(conversation_sha256) = 64 AND conversation_sha256 NOT GLOB '*[^0-9a-f]*')),
|
|
126
|
+
event_kind TEXT NOT NULL CHECK(length(event_kind) BETWEEN 1 AND 64),
|
|
127
|
+
resolution TEXT NOT NULL CHECK(resolution IN ('matched','skipped','conflict')),
|
|
128
|
+
run_id INTEGER NOT NULL REFERENCES source_runs(id) ON DELETE RESTRICT,
|
|
129
|
+
metadata_json TEXT NOT NULL DEFAULT '{}' CHECK(json_valid(metadata_json)),
|
|
130
|
+
UNIQUE(provider, account_key, external_id)
|
|
131
|
+
) STRICT;
|
|
132
|
+
|
|
133
|
+
CREATE TABLE tags (
|
|
134
|
+
id INTEGER PRIMARY KEY,
|
|
135
|
+
name TEXT NOT NULL COLLATE NOCASE UNIQUE CHECK(length(name) BETWEEN 1 AND 256)
|
|
136
|
+
) STRICT;
|
|
137
|
+
CREATE TABLE person_tags (
|
|
138
|
+
person_id INTEGER NOT NULL REFERENCES people(id) ON DELETE CASCADE,
|
|
139
|
+
tag_id INTEGER NOT NULL REFERENCES tags(id) ON DELETE CASCADE,
|
|
140
|
+
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
141
|
+
PRIMARY KEY(person_id, tag_id)
|
|
142
|
+
) STRICT;
|
|
143
|
+
|
|
144
|
+
CREATE TABLE collections (
|
|
145
|
+
id INTEGER PRIMARY KEY,
|
|
146
|
+
provider TEXT NOT NULL CHECK(length(provider) BETWEEN 1 AND 64),
|
|
147
|
+
account_key TEXT NOT NULL COLLATE NOCASE CHECK(length(account_key) BETWEEN 1 AND 256),
|
|
148
|
+
resource_id TEXT NOT NULL CHECK(length(resource_id) BETWEEN 1 AND 2048),
|
|
149
|
+
name TEXT NOT NULL CHECK(length(name) BETWEEN 1 AND 1024),
|
|
150
|
+
metadata_json TEXT NOT NULL DEFAULT '{}' CHECK(json_valid(metadata_json)),
|
|
151
|
+
UNIQUE(provider, account_key, resource_id)
|
|
152
|
+
) STRICT;
|
|
153
|
+
CREATE TABLE collection_memberships (
|
|
154
|
+
collection_id INTEGER NOT NULL REFERENCES collections(id) ON DELETE CASCADE,
|
|
155
|
+
person_id INTEGER NOT NULL REFERENCES people(id) ON DELETE CASCADE,
|
|
156
|
+
state TEXT NOT NULL CHECK(length(state) BETWEEN 1 AND 64),
|
|
157
|
+
metadata_json TEXT NOT NULL DEFAULT '{}' CHECK(json_valid(metadata_json)),
|
|
158
|
+
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
159
|
+
PRIMARY KEY(collection_id, person_id)
|
|
160
|
+
) STRICT;
|
|
161
|
+
|
|
162
|
+
CREATE TABLE identity_decisions (
|
|
163
|
+
id INTEGER PRIMARY KEY,
|
|
164
|
+
left_person_id INTEGER NOT NULL REFERENCES people(id) ON DELETE RESTRICT,
|
|
165
|
+
right_person_id INTEGER NOT NULL REFERENCES people(id) ON DELETE RESTRICT,
|
|
166
|
+
action TEXT NOT NULL CHECK(action IN ('accept','reject','defer','separate')),
|
|
167
|
+
evidence_sha256 TEXT NOT NULL CHECK(length(evidence_sha256) = 64 AND evidence_sha256 NOT GLOB '*[^0-9a-f]*'),
|
|
168
|
+
evidence_json TEXT NOT NULL CHECK(json_valid(evidence_json)),
|
|
169
|
+
prior_decision_id INTEGER REFERENCES identity_decisions(id) ON DELETE RESTRICT,
|
|
170
|
+
note TEXT,
|
|
171
|
+
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
172
|
+
CHECK(left_person_id < right_person_id)
|
|
173
|
+
) STRICT;
|
|
174
|
+
CREATE UNIQUE INDEX identity_one_successor ON identity_decisions(prior_decision_id) WHERE prior_decision_id IS NOT NULL;
|
|
175
|
+
|
|
176
|
+
CREATE TABLE public_enrichments (
|
|
177
|
+
id INTEGER PRIMARY KEY,
|
|
178
|
+
person_id INTEGER NOT NULL REFERENCES people(id) ON DELETE RESTRICT,
|
|
179
|
+
provider TEXT NOT NULL CHECK(provider IN ('exa-luna','hunter','agent-public-web','human-public-web')),
|
|
180
|
+
input_sha256 TEXT NOT NULL CHECK(length(input_sha256) = 64 AND input_sha256 NOT GLOB '*[^0-9a-f]*'),
|
|
181
|
+
evidence_sha256 TEXT NOT NULL CHECK(length(evidence_sha256) = 64 AND evidence_sha256 NOT GLOB '*[^0-9a-f]*'),
|
|
182
|
+
evidence_json TEXT NOT NULL CHECK(json_valid(evidence_json)),
|
|
183
|
+
output_json TEXT NOT NULL CHECK(json_valid(output_json)),
|
|
184
|
+
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
185
|
+
UNIQUE(person_id, provider, input_sha256, evidence_sha256)
|
|
186
|
+
) STRICT;
|
|
187
|
+
|
|
188
|
+
CREATE TABLE legacy_migrations (
|
|
189
|
+
id INTEGER PRIMARY KEY,
|
|
190
|
+
source_format TEXT NOT NULL CHECK(source_format = 'rolodex-v1'),
|
|
191
|
+
source_sha256 TEXT NOT NULL UNIQUE CHECK(length(source_sha256) = 64 AND source_sha256 NOT GLOB '*[^0-9a-f]*'),
|
|
192
|
+
source_locator_sha256 TEXT NOT NULL CHECK(length(source_locator_sha256) = 64 AND source_locator_sha256 NOT GLOB '*[^0-9a-f]*'),
|
|
193
|
+
result_json TEXT NOT NULL CHECK(json_valid(result_json)),
|
|
194
|
+
migrated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
195
|
+
) STRICT;
|
|
196
|
+
|
|
197
|
+
CREATE TABLE cloud_sync_state (
|
|
198
|
+
singleton INTEGER PRIMARY KEY CHECK(singleton = 1),
|
|
199
|
+
base_url TEXT NOT NULL,
|
|
200
|
+
device_id TEXT NOT NULL,
|
|
201
|
+
last_snapshot_id TEXT,
|
|
202
|
+
last_synced_at TEXT
|
|
203
|
+
) STRICT;
|
|
204
|
+
|
|
205
|
+
CREATE TRIGGER people_updated_at AFTER UPDATE ON people FOR EACH ROW
|
|
206
|
+
BEGIN UPDATE people SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id; END;
|
|
207
|
+
CREATE TRIGGER contact_methods_updated_at AFTER UPDATE ON contact_methods FOR EACH ROW
|
|
208
|
+
BEGIN UPDATE contact_methods SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id; END;
|
|
209
|
+
CREATE TRIGGER provider_resources_updated_at AFTER UPDATE ON provider_resources FOR EACH ROW
|
|
210
|
+
BEGIN UPDATE provider_resources SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id; END;
|
|
211
|
+
|
|
212
|
+
CREATE TRIGGER source_runs_immutable_update BEFORE UPDATE ON source_runs FOR EACH ROW
|
|
213
|
+
BEGIN SELECT RAISE(ABORT, 'source runs are immutable'); END;
|
|
214
|
+
CREATE TRIGGER source_runs_immutable_delete BEFORE DELETE ON source_runs FOR EACH ROW
|
|
215
|
+
BEGIN SELECT RAISE(ABORT, 'source runs are immutable'); END;
|
|
216
|
+
CREATE TRIGGER source_records_immutable_update BEFORE UPDATE ON source_records FOR EACH ROW
|
|
217
|
+
BEGIN SELECT RAISE(ABORT, 'source records are immutable'); END;
|
|
218
|
+
CREATE TRIGGER source_records_immutable_delete BEFORE DELETE ON source_records FOR EACH ROW
|
|
219
|
+
BEGIN SELECT RAISE(ABORT, 'source records are immutable'); END;
|
|
220
|
+
CREATE TRIGGER public_enrichments_immutable_update BEFORE UPDATE ON public_enrichments FOR EACH ROW
|
|
221
|
+
BEGIN SELECT RAISE(ABORT, 'public enrichments are immutable'); END;
|
|
222
|
+
CREATE TRIGGER public_enrichments_immutable_delete BEFORE DELETE ON public_enrichments FOR EACH ROW
|
|
223
|
+
BEGIN SELECT RAISE(ABORT, 'public enrichments are immutable'); END;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
PRAGMA foreign_keys = ON;
|
|
2
|
+
|
|
3
|
+
CREATE TABLE local_revisions (
|
|
4
|
+
singleton INTEGER PRIMARY KEY CHECK(singleton = 1),
|
|
5
|
+
identity_revision INTEGER NOT NULL DEFAULT 0 CHECK(identity_revision >= 0)
|
|
6
|
+
) STRICT;
|
|
7
|
+
INSERT INTO local_revisions(singleton, identity_revision) VALUES (1, 0);
|
|
8
|
+
|
|
9
|
+
CREATE TABLE provider_accounts (
|
|
10
|
+
provider TEXT NOT NULL CHECK(length(provider) BETWEEN 1 AND 64),
|
|
11
|
+
auth_id TEXT NOT NULL CHECK(length(auth_id) BETWEEN 1 AND 128),
|
|
12
|
+
account_key TEXT NOT NULL COLLATE NOCASE CHECK(length(account_key) BETWEEN 1 AND 256),
|
|
13
|
+
auth_sha256 TEXT NOT NULL CHECK(length(auth_sha256) = 64 AND auth_sha256 NOT GLOB '*[^0-9a-f]*'),
|
|
14
|
+
first_seen_at TEXT NOT NULL,
|
|
15
|
+
last_seen_at TEXT NOT NULL,
|
|
16
|
+
PRIMARY KEY(provider, auth_id),
|
|
17
|
+
UNIQUE(provider, account_key)
|
|
18
|
+
) STRICT;
|
|
19
|
+
|
|
20
|
+
CREATE TRIGGER provider_accounts_identity_immutable BEFORE UPDATE OF provider,auth_id,account_key,auth_sha256 ON provider_accounts FOR EACH ROW
|
|
21
|
+
BEGIN SELECT RAISE(ABORT, 'provider account binding is immutable'); END;
|
|
22
|
+
|
|
23
|
+
CREATE TABLE archive_accounts (
|
|
24
|
+
provider TEXT NOT NULL CHECK(length(provider) BETWEEN 1 AND 64),
|
|
25
|
+
account_key TEXT NOT NULL COLLATE NOCASE CHECK(length(account_key) BETWEEN 1 AND 256),
|
|
26
|
+
owner_key TEXT NOT NULL COLLATE NOCASE CHECK(length(owner_key) BETWEEN 1 AND 256),
|
|
27
|
+
current_run_id INTEGER NOT NULL REFERENCES source_runs(id) ON DELETE RESTRICT,
|
|
28
|
+
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
29
|
+
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
30
|
+
PRIMARY KEY(provider, account_key),
|
|
31
|
+
UNIQUE(provider, owner_key)
|
|
32
|
+
) STRICT;
|
|
33
|
+
CREATE TRIGGER archive_accounts_identity_immutable BEFORE UPDATE OF provider,account_key,owner_key ON archive_accounts FOR EACH ROW
|
|
34
|
+
BEGIN SELECT RAISE(ABORT, 'archive account binding is immutable'); END;
|
|
35
|
+
CREATE TRIGGER archive_accounts_run_insert BEFORE INSERT ON archive_accounts FOR EACH ROW
|
|
36
|
+
BEGIN
|
|
37
|
+
SELECT CASE WHEN NOT EXISTS (
|
|
38
|
+
SELECT 1 FROM source_runs run WHERE run.id=NEW.current_run_id AND run.provider=NEW.provider AND run.account_key=NEW.account_key
|
|
39
|
+
) THEN RAISE(ABORT, 'archive account run realm mismatch') END;
|
|
40
|
+
END;
|
|
41
|
+
CREATE TRIGGER archive_accounts_run_update BEFORE UPDATE OF current_run_id ON archive_accounts FOR EACH ROW
|
|
42
|
+
BEGIN
|
|
43
|
+
SELECT CASE WHEN NEW.current_run_id <= OLD.current_run_id THEN RAISE(ABORT, 'archive account run must advance') END;
|
|
44
|
+
SELECT CASE WHEN NOT EXISTS (
|
|
45
|
+
SELECT 1 FROM source_runs run WHERE run.id=NEW.current_run_id AND run.provider=NEW.provider AND run.account_key=NEW.account_key
|
|
46
|
+
) THEN RAISE(ABORT, 'archive account run realm mismatch') END;
|
|
47
|
+
END;
|
|
48
|
+
|
|
49
|
+
CREATE TRIGGER contact_methods_identity_insert AFTER INSERT ON contact_methods FOR EACH ROW
|
|
50
|
+
BEGIN UPDATE local_revisions SET identity_revision = identity_revision + 1 WHERE singleton = 1; END;
|
|
51
|
+
CREATE TRIGGER contact_methods_identity_update AFTER UPDATE ON contact_methods FOR EACH ROW
|
|
52
|
+
BEGIN UPDATE local_revisions SET identity_revision = identity_revision + 1 WHERE singleton = 1; END;
|
|
53
|
+
CREATE TRIGGER contact_methods_identity_delete AFTER DELETE ON contact_methods FOR EACH ROW
|
|
54
|
+
BEGIN UPDATE local_revisions SET identity_revision = identity_revision + 1 WHERE singleton = 1; END;
|
|
55
|
+
|
|
56
|
+
CREATE TABLE provider_checkpoints (
|
|
57
|
+
provider TEXT NOT NULL CHECK(length(provider) BETWEEN 1 AND 64),
|
|
58
|
+
account_key TEXT NOT NULL COLLATE NOCASE CHECK(length(account_key) BETWEEN 1 AND 256),
|
|
59
|
+
stream_key TEXT NOT NULL CHECK(length(stream_key) BETWEEN 1 AND 128),
|
|
60
|
+
source_locator_sha256 TEXT NOT NULL CHECK(length(source_locator_sha256) = 64 AND source_locator_sha256 NOT GLOB '*[^0-9a-f]*'),
|
|
61
|
+
schema_sha256 TEXT NOT NULL CHECK(length(schema_sha256) = 64 AND schema_sha256 NOT GLOB '*[^0-9a-f]*'),
|
|
62
|
+
generation_sha256 TEXT CHECK(generation_sha256 IS NULL OR (length(generation_sha256) = 64 AND generation_sha256 NOT GLOB '*[^0-9a-f]*')),
|
|
63
|
+
cursor TEXT NOT NULL CHECK(length(cursor) BETWEEN 1 AND 256),
|
|
64
|
+
cursor_anchor_sha256 TEXT CHECK(cursor_anchor_sha256 IS NULL OR (length(cursor_anchor_sha256) = 64 AND cursor_anchor_sha256 NOT GLOB '*[^0-9a-f]*')),
|
|
65
|
+
last_run_id INTEGER NOT NULL REFERENCES source_runs(id) ON DELETE RESTRICT,
|
|
66
|
+
metadata_json TEXT NOT NULL DEFAULT '{}' CHECK(json_valid(metadata_json)),
|
|
67
|
+
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
68
|
+
PRIMARY KEY(provider, account_key, stream_key)
|
|
69
|
+
) STRICT;
|
|
70
|
+
|
|
71
|
+
CREATE TRIGGER provider_checkpoint_run_insert BEFORE INSERT ON provider_checkpoints FOR EACH ROW
|
|
72
|
+
BEGIN
|
|
73
|
+
SELECT CASE WHEN NOT EXISTS (
|
|
74
|
+
SELECT 1 FROM source_runs run
|
|
75
|
+
WHERE run.id = NEW.last_run_id AND run.provider = NEW.provider AND run.account_key = NEW.account_key
|
|
76
|
+
) THEN RAISE(ABORT, 'checkpoint run realm mismatch') END;
|
|
77
|
+
END;
|
|
78
|
+
CREATE TRIGGER provider_checkpoint_run_update BEFORE UPDATE ON provider_checkpoints FOR EACH ROW
|
|
79
|
+
BEGIN
|
|
80
|
+
SELECT CASE WHEN NEW.provider <> OLD.provider OR NEW.account_key <> OLD.account_key OR NEW.stream_key <> OLD.stream_key
|
|
81
|
+
THEN RAISE(ABORT, 'checkpoint identity is immutable') END;
|
|
82
|
+
SELECT CASE WHEN NEW.last_run_id <= OLD.last_run_id
|
|
83
|
+
THEN RAISE(ABORT, 'checkpoint run must advance') END;
|
|
84
|
+
SELECT CASE WHEN NOT EXISTS (
|
|
85
|
+
SELECT 1 FROM source_runs run
|
|
86
|
+
WHERE run.id = NEW.last_run_id AND run.provider = NEW.provider AND run.account_key = NEW.account_key
|
|
87
|
+
) THEN RAISE(ABORT, 'checkpoint run realm mismatch') END;
|
|
88
|
+
END;
|
|
89
|
+
|
|
90
|
+
CREATE TRIGGER interaction_events_immutable_update BEFORE UPDATE ON interaction_events FOR EACH ROW
|
|
91
|
+
BEGIN SELECT RAISE(ABORT, 'interaction events are immutable'); END;
|
|
92
|
+
CREATE TRIGGER interaction_events_immutable_delete BEFORE DELETE ON interaction_events FOR EACH ROW
|
|
93
|
+
BEGIN SELECT RAISE(ABORT, 'interaction events are immutable'); END;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
PRAGMA foreign_keys = ON;
|
|
2
|
+
|
|
3
|
+
CREATE VIEW identity_current_decisions AS
|
|
4
|
+
SELECT decision.*
|
|
5
|
+
FROM identity_decisions decision
|
|
6
|
+
WHERE NOT EXISTS (
|
|
7
|
+
SELECT 1 FROM identity_decisions successor WHERE successor.prior_decision_id = decision.id
|
|
8
|
+
);
|
|
9
|
+
|
|
10
|
+
CREATE VIEW person_identity_components AS
|
|
11
|
+
WITH RECURSIVE
|
|
12
|
+
accepted(left_person_id, right_person_id) AS (
|
|
13
|
+
SELECT left_person_id, right_person_id
|
|
14
|
+
FROM identity_current_decisions
|
|
15
|
+
WHERE action = 'accept'
|
|
16
|
+
),
|
|
17
|
+
reach(person_id, member_id) AS (
|
|
18
|
+
SELECT id, id FROM people
|
|
19
|
+
UNION
|
|
20
|
+
SELECT reach.person_id,
|
|
21
|
+
CASE WHEN accepted.left_person_id = reach.member_id
|
|
22
|
+
THEN accepted.right_person_id ELSE accepted.left_person_id END
|
|
23
|
+
FROM reach
|
|
24
|
+
JOIN accepted
|
|
25
|
+
ON accepted.left_person_id = reach.member_id OR accepted.right_person_id = reach.member_id
|
|
26
|
+
)
|
|
27
|
+
SELECT person_id, min(member_id) AS canonical_person_id
|
|
28
|
+
FROM reach
|
|
29
|
+
GROUP BY person_id;
|
|
30
|
+
|
|
31
|
+
CREATE TRIGGER identity_decisions_immutable_update
|
|
32
|
+
BEFORE UPDATE ON identity_decisions FOR EACH ROW
|
|
33
|
+
BEGIN SELECT RAISE(ABORT, 'identity decisions are immutable'); END;
|
|
34
|
+
|
|
35
|
+
CREATE TRIGGER identity_decisions_immutable_delete
|
|
36
|
+
BEFORE DELETE ON identity_decisions FOR EACH ROW
|
|
37
|
+
BEGIN SELECT RAISE(ABORT, 'identity decisions are immutable'); END;
|
|
38
|
+
|
|
39
|
+
CREATE TRIGGER identity_decisions_predecessor
|
|
40
|
+
BEFORE INSERT ON identity_decisions FOR EACH ROW
|
|
41
|
+
WHEN NEW.prior_decision_id IS NOT NULL
|
|
42
|
+
BEGIN
|
|
43
|
+
SELECT CASE WHEN NOT EXISTS (
|
|
44
|
+
SELECT 1 FROM identity_current_decisions prior
|
|
45
|
+
WHERE prior.id = NEW.prior_decision_id
|
|
46
|
+
AND prior.left_person_id = NEW.left_person_id
|
|
47
|
+
AND prior.right_person_id = NEW.right_person_id
|
|
48
|
+
) THEN RAISE(ABORT, 'identity predecessor must be the current decision for the same pair') END;
|
|
49
|
+
END;
|
|
50
|
+
|
|
51
|
+
CREATE TRIGGER identity_decisions_revision
|
|
52
|
+
AFTER INSERT ON identity_decisions FOR EACH ROW
|
|
53
|
+
BEGIN UPDATE local_revisions SET identity_revision = identity_revision + 1 WHERE singleton = 1; END;
|
|
54
|
+
|
|
55
|
+
CREATE TRIGGER provider_resources_identity_insert
|
|
56
|
+
AFTER INSERT ON provider_resources FOR EACH ROW
|
|
57
|
+
BEGIN UPDATE local_revisions SET identity_revision = identity_revision + 1 WHERE singleton = 1; END;
|
|
58
|
+
CREATE TRIGGER provider_resources_identity_update
|
|
59
|
+
AFTER UPDATE OF provider,account_key,resource_type,resource_id,person_id,username,profile_url,active ON provider_resources FOR EACH ROW
|
|
60
|
+
BEGIN UPDATE local_revisions SET identity_revision = identity_revision + 1 WHERE singleton = 1; END;
|
|
61
|
+
CREATE TRIGGER provider_resources_identity_delete
|
|
62
|
+
AFTER DELETE ON provider_resources FOR EACH ROW
|
|
63
|
+
BEGIN UPDATE local_revisions SET identity_revision = identity_revision + 1 WHERE singleton = 1; END;
|
|
64
|
+
|
|
65
|
+
CREATE TRIGGER people_identity_update
|
|
66
|
+
AFTER UPDATE OF display_name,given_name,middle_name,family_name,name_prefix,name_suffix,nickname,organization,department,title ON people FOR EACH ROW
|
|
67
|
+
BEGIN UPDATE local_revisions SET identity_revision = identity_revision + 1 WHERE singleton = 1; END;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
PRAGMA foreign_keys = ON;
|
|
2
|
+
|
|
3
|
+
CREATE TABLE source_bindings (
|
|
4
|
+
id INTEGER PRIMARY KEY,
|
|
5
|
+
authority TEXT NOT NULL CHECK(length(authority) BETWEEN 1 AND 64),
|
|
6
|
+
auth_id TEXT NOT NULL CHECK(length(auth_id) BETWEEN 1 AND 128),
|
|
7
|
+
subject_sha256 TEXT NOT NULL CHECK(length(subject_sha256) = 64 AND subject_sha256 NOT GLOB '*[^0-9a-f]*'),
|
|
8
|
+
auth_sha256 TEXT NOT NULL CHECK(length(auth_sha256) = 64 AND auth_sha256 NOT GLOB '*[^0-9a-f]*'),
|
|
9
|
+
first_seen_at TEXT NOT NULL,
|
|
10
|
+
last_seen_at TEXT NOT NULL,
|
|
11
|
+
complete_realm_inventory_observed_at TEXT,
|
|
12
|
+
complete_realm_inventory_sha256 TEXT,
|
|
13
|
+
metadata_json TEXT NOT NULL DEFAULT '{}' CHECK(json_valid(metadata_json)),
|
|
14
|
+
CHECK(
|
|
15
|
+
(complete_realm_inventory_observed_at IS NULL AND complete_realm_inventory_sha256 IS NULL)
|
|
16
|
+
OR (
|
|
17
|
+
complete_realm_inventory_observed_at IS NOT NULL
|
|
18
|
+
AND length(complete_realm_inventory_sha256) = 64
|
|
19
|
+
AND complete_realm_inventory_sha256 NOT GLOB '*[^0-9a-f]*'
|
|
20
|
+
)
|
|
21
|
+
),
|
|
22
|
+
UNIQUE(authority, auth_id),
|
|
23
|
+
UNIQUE(authority, subject_sha256)
|
|
24
|
+
) STRICT;
|
|
25
|
+
|
|
26
|
+
CREATE TRIGGER source_bindings_identity_immutable
|
|
27
|
+
BEFORE UPDATE OF authority,auth_id,subject_sha256,auth_sha256 ON source_bindings FOR EACH ROW
|
|
28
|
+
BEGIN SELECT RAISE(ABORT, 'source binding identity is immutable'); END;
|
|
29
|
+
|
|
30
|
+
CREATE TABLE source_realms (
|
|
31
|
+
id INTEGER PRIMARY KEY,
|
|
32
|
+
source_binding_id INTEGER NOT NULL REFERENCES source_bindings(id) ON DELETE RESTRICT,
|
|
33
|
+
authority TEXT NOT NULL CHECK(length(authority) BETWEEN 1 AND 64),
|
|
34
|
+
account_key TEXT NOT NULL COLLATE NOCASE CHECK(length(account_key) BETWEEN 1 AND 256),
|
|
35
|
+
service TEXT NOT NULL CHECK(length(service) BETWEEN 1 AND 64),
|
|
36
|
+
identity_namespace TEXT NOT NULL CHECK(length(identity_namespace) BETWEEN 1 AND 128),
|
|
37
|
+
external_id_sha256 TEXT NOT NULL CHECK(length(external_id_sha256) = 64 AND external_id_sha256 NOT GLOB '*[^0-9a-f]*'),
|
|
38
|
+
active INTEGER NOT NULL DEFAULT 1 CHECK(active IN (0,1)),
|
|
39
|
+
first_seen_at TEXT NOT NULL,
|
|
40
|
+
last_seen_at TEXT NOT NULL,
|
|
41
|
+
state_observed_at TEXT NOT NULL,
|
|
42
|
+
materialized_source_sha256 TEXT CHECK(
|
|
43
|
+
materialized_source_sha256 IS NULL OR (
|
|
44
|
+
length(materialized_source_sha256) = 64
|
|
45
|
+
AND materialized_source_sha256 NOT GLOB '*[^0-9a-f]*'
|
|
46
|
+
)
|
|
47
|
+
),
|
|
48
|
+
metadata_json TEXT NOT NULL DEFAULT '{}' CHECK(json_valid(metadata_json)),
|
|
49
|
+
UNIQUE(authority, account_key),
|
|
50
|
+
UNIQUE(source_binding_id, external_id_sha256)
|
|
51
|
+
) STRICT;
|
|
52
|
+
CREATE INDEX source_realms_binding ON source_realms(source_binding_id, active, service);
|
|
53
|
+
|
|
54
|
+
CREATE TRIGGER source_realms_identity_immutable
|
|
55
|
+
BEFORE UPDATE OF source_binding_id,authority,account_key,identity_namespace,external_id_sha256
|
|
56
|
+
ON source_realms FOR EACH ROW
|
|
57
|
+
BEGIN SELECT RAISE(ABORT, 'source realm identity is immutable'); END;
|
|
58
|
+
|
|
59
|
+
ALTER TABLE provider_resources ADD COLUMN source_realm_id INTEGER REFERENCES source_realms(id) ON DELETE RESTRICT;
|
|
60
|
+
CREATE INDEX provider_resources_realm ON provider_resources(source_realm_id, active, person_id);
|
|
61
|
+
|
|
62
|
+
ALTER TABLE contact_methods ADD COLUMN provider_resource_id INTEGER REFERENCES provider_resources(id) ON DELETE RESTRICT;
|
|
63
|
+
ALTER TABLE contact_methods ADD COLUMN active INTEGER NOT NULL DEFAULT 1 CHECK(active IN (0,1));
|
|
64
|
+
ALTER TABLE contact_methods ADD COLUMN identity_eligible INTEGER NOT NULL DEFAULT 1 CHECK(identity_eligible IN (0,1));
|
|
65
|
+
CREATE INDEX contact_methods_resource ON contact_methods(provider_resource_id, active, kind);
|
|
66
|
+
|
|
67
|
+
CREATE TABLE person_field_observations (
|
|
68
|
+
id INTEGER PRIMARY KEY,
|
|
69
|
+
provider_resource_id INTEGER NOT NULL REFERENCES provider_resources(id) ON DELETE RESTRICT,
|
|
70
|
+
field TEXT NOT NULL CHECK(field IN (
|
|
71
|
+
'display_name','given_name','middle_name','family_name','name_prefix','name_suffix',
|
|
72
|
+
'nickname','organization','department','title','birthday'
|
|
73
|
+
)),
|
|
74
|
+
value TEXT NOT NULL CHECK(length(value) BETWEEN 1 AND 8192),
|
|
75
|
+
basis TEXT NOT NULL CHECK(length(basis) BETWEEN 1 AND 128),
|
|
76
|
+
priority INTEGER NOT NULL CHECK(priority BETWEEN 0 AND 1000),
|
|
77
|
+
active INTEGER NOT NULL DEFAULT 1 CHECK(active IN (0,1)),
|
|
78
|
+
first_seen_run_id INTEGER NOT NULL REFERENCES source_runs(id) ON DELETE RESTRICT,
|
|
79
|
+
last_seen_run_id INTEGER NOT NULL REFERENCES source_runs(id) ON DELETE RESTRICT,
|
|
80
|
+
metadata_json TEXT NOT NULL DEFAULT '{}' CHECK(json_valid(metadata_json)),
|
|
81
|
+
UNIQUE(provider_resource_id, field, value, basis)
|
|
82
|
+
) STRICT;
|
|
83
|
+
CREATE INDEX person_field_observations_current
|
|
84
|
+
ON person_field_observations(provider_resource_id, field, active, priority DESC, id);
|
|
85
|
+
|
|
86
|
+
CREATE TRIGGER person_field_observations_identity_revision_insert
|
|
87
|
+
AFTER INSERT ON person_field_observations FOR EACH ROW
|
|
88
|
+
BEGIN UPDATE local_revisions SET identity_revision = identity_revision + 1 WHERE singleton = 1; END;
|
|
89
|
+
CREATE TRIGGER person_field_observations_identity_revision_update
|
|
90
|
+
AFTER UPDATE OF value,basis,priority,active ON person_field_observations FOR EACH ROW
|
|
91
|
+
BEGIN UPDATE local_revisions SET identity_revision = identity_revision + 1 WHERE singleton = 1; END;
|