kryptheon-night 0.1.2 → 0.1.4

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/sqlengine.js ADDED
@@ -0,0 +1,200 @@
1
+ // Reaching the SQL engine from Node.
2
+ //
3
+ // engine.sql holds the work. This is only the door: put the functions
4
+ // somewhere, call them, take them away again.
5
+ //
6
+ // Two doors lead to the same engine, and this is the one the command line
7
+ // uses. The other is the installer, where the same functions stay put and
8
+ // pg_cron calls them nightly - so anything that happens here must be a thing
9
+ // the scheduler could do too. No cleverness in Node that the database cannot
10
+ // do on its own, or the two doors stop leading to the same place.
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+
15
+ // The token engine.sql carries wherever its own schema name belongs. A token
16
+ // rather than the word "kryptheon" so that replacing it cannot quietly hit a
17
+ // comment or a string that happened to say the same thing.
18
+ const PLACEHOLDER = '__KN__';
19
+
20
+ /** The engine, with its functions addressed to one schema. */
21
+ function engineFor(target) {
22
+ const source = fs.readFileSync(path.join(__dirname, 'engine.sql'), 'utf8');
23
+ if (!source.includes(PLACEHOLDER)) {
24
+ throw new Error('engine.sql has no ' + PLACEHOLDER + ' in it, so it cannot be addressed anywhere');
25
+ }
26
+ return source.split(PLACEHOLDER).join(quote(target));
27
+ }
28
+
29
+ function quote(name) {
30
+ return '"' + String(name).split('"').join('""') + '"';
31
+ }
32
+
33
+ /**
34
+ * Puts the engine somewhere it can be called from.
35
+ *
36
+ * `CREATE OR REPLACE` throughout, so installing twice is not an error and an
37
+ * upgrade is the same operation as an install.
38
+ */
39
+ async function install(client, target) {
40
+ await client.query(engineFor(target));
41
+ return target;
42
+ }
43
+
44
+ /** Takes it away again, functions and all. */
45
+ async function uninstall(client, target) {
46
+ await client.query('DROP SCHEMA IF EXISTS ' + quote(target) + ' CASCADE');
47
+ }
48
+
49
+ /** What the engine says the shape of an app is. */
50
+ async function readSchema(client, target, source) {
51
+ const { rows } = await client.query('SELECT ' + quote(target) + '.read_schema($1) AS shape', [source]);
52
+ return rows[0].shape;
53
+ }
54
+
55
+ /**
56
+ * What the engine would run to rebuild one schema as another.
57
+ *
58
+ * Handed back without running any of it, so the guard can look at every
59
+ * statement before one of them touches the database - and so a check can read
60
+ * what would have happened without anything happening.
61
+ */
62
+ async function copyStatements(client, target, plan, into) {
63
+ const { rows } = await client.query(
64
+ 'SELECT ' + quote(target) + '.copy_statements($1::jsonb, $2) AS statements',
65
+ [JSON.stringify(plan), into],
66
+ );
67
+ return rows[0].statements;
68
+ }
69
+
70
+ /** Builds the copy, and hands back every statement it ran. */
71
+ async function writeSchema(client, target, plan, into) {
72
+ const { rows } = await client.query(
73
+ 'SELECT ' + quote(target) + '.write_schema($1::jsonb, $2) AS statements',
74
+ [JSON.stringify(plan), into],
75
+ );
76
+ return rows[0].statements;
77
+ }
78
+
79
+ /*
80
+ * The attacks.
81
+ *
82
+ * Thin on purpose: each one hands the engine the same arguments its Node twin
83
+ * takes and gives back the same shape, so a caller can be pointed at either
84
+ * without knowing which it has. `twin.check.js` has been calling these through
85
+ * raw SQL since they were written; they are functions here so that `scan.js`
86
+ * can reach them too, which is what makes the two doors one engine rather than
87
+ * one on paper.
88
+ */
89
+
90
+ /** Puts two fake people in the copy, and says what it could not seed. */
91
+ async function seed(client, target, into, tables) {
92
+ const { rows } = await client.query(
93
+ 'SELECT ' + quote(target) + '.seed($1, $2::jsonb) AS answer',
94
+ [into, JSON.stringify(tables)],
95
+ );
96
+ return rows[0].answer;
97
+ }
98
+
99
+ /** Can the wrong person read this? */
100
+ async function impersonate(client, target, into, tables) {
101
+ const { rows } = await client.query(
102
+ 'SELECT ' + quote(target) + '.impersonate($1, $2::jsonb) AS answer',
103
+ [into, JSON.stringify(tables)],
104
+ );
105
+ return rows[0].answer;
106
+ }
107
+
108
+ /** Can a stranger change it? Every write rolled back inside the engine. */
109
+ async function tamper(client, target, into, tables, seeded, policies) {
110
+ const { rows } = await client.query(
111
+ 'SELECT ' + quote(target) + '.tamper($1, $2::jsonb, $3::jsonb, $4::jsonb) AS answer',
112
+ [into, JSON.stringify(tables), JSON.stringify(seeded), JSON.stringify(policies || [])],
113
+ );
114
+ return rows[0].answer;
115
+ }
116
+
117
+ /** Can a half-finished row survive? */
118
+ async function orphan(client, target, into, tables, seeded) {
119
+ const { rows } = await client.query(
120
+ 'SELECT ' + quote(target) + '.orphan($1, $2::jsonb, $3::jsonb) AS answer',
121
+ [into, JSON.stringify(tables), JSON.stringify(seeded)],
122
+ );
123
+ return rows[0].answer;
124
+ }
125
+
126
+ /**
127
+ * Can the same thing exist twice?
128
+ *
129
+ * Inside the database this one cannot actually race anything - two requests at
130
+ * the same instant need two connections, and no credential ever moves - so it
131
+ * comes back with every column it would have tried in `notTried`, with the
132
+ * reason. That is deliberate and is the honest answer: reporting nothing at
133
+ * all would read as safety.
134
+ */
135
+ async function collide(client, target, into, tables, indexes) {
136
+ const { rows } = await client.query(
137
+ 'SELECT ' + quote(target) + '.collide($1, $2::jsonb, $3::jsonb) AS answer',
138
+ [into, JSON.stringify(tables), JSON.stringify(indexes)],
139
+ );
140
+ return rows[0].answer;
141
+ }
142
+
143
+ /**
144
+ * The same six operations `scan.js` asks of an engine, done in SQL.
145
+ *
146
+ * Handed an engine schema that is already installed, because installing and
147
+ * removing it is the caller's business - `withEngine` below - and a scan
148
+ * should not have to know that this engine needs putting somewhere first.
149
+ *
150
+ * `collide` is not among them. The scan races two real requests down two real
151
+ * connections, which is something no engine living inside the database can do;
152
+ * that is measured and written up in STATE.md, and the report names what it
153
+ * could not race rather than leaving it out.
154
+ */
155
+ function adapterFor(target) {
156
+ return {
157
+ name: 'sql',
158
+ readSchema: (client, source) => readSchema(client, target, source),
159
+ writeSchema: (client, plan, into) => writeSchema(client, target, plan, into),
160
+ seed: (client, into, tables) => seed(client, target, into, tables),
161
+ impersonate: (client, into, tables) => impersonate(client, target, into, tables),
162
+ tamper: (client, into, tables, sown, policies) =>
163
+ tamper(client, target, into, tables, sown, policies),
164
+ orphan: (client, into, tables, sown) => orphan(client, target, into, tables, sown),
165
+ };
166
+ }
167
+
168
+ /**
169
+ * Installs the engine, does something with it, and takes it away.
170
+ *
171
+ * The schema is named for the moment it was made, the same as the copy is, so
172
+ * that one abandoned by a dropped connection can be recognised and swept later
173
+ * rather than sitting there for ever.
174
+ */
175
+ async function withEngine(client, work) {
176
+ const target = 'kn_engine_' + Date.now().toString(36);
177
+ await install(client, target);
178
+ try {
179
+ return await work(target);
180
+ } finally {
181
+ await uninstall(client, target).catch(() => {});
182
+ }
183
+ }
184
+
185
+ module.exports = {
186
+ PLACEHOLDER: PLACEHOLDER,
187
+ engineFor: engineFor,
188
+ install: install,
189
+ uninstall: uninstall,
190
+ readSchema: readSchema,
191
+ copyStatements: copyStatements,
192
+ writeSchema: writeSchema,
193
+ seed: seed,
194
+ impersonate: impersonate,
195
+ tamper: tamper,
196
+ orphan: orphan,
197
+ collide: collide,
198
+ adapterFor: adapterFor,
199
+ withEngine: withEngine,
200
+ };