@shware/workflow 7.0.0 → 8.0.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/README.md +121 -34
- package/dist/aws/engine-step.cjs +4 -1
- package/dist/aws/engine-step.cjs.map +1 -1
- package/dist/aws/engine-step.d.cts.map +1 -1
- package/dist/aws/engine-step.d.mts.map +1 -1
- package/dist/aws/engine-step.mjs +4 -1
- package/dist/aws/engine-step.mjs.map +1 -1
- package/dist/client.cjs +66 -41
- package/dist/client.cjs.map +1 -1
- package/dist/client.d.cts +72 -22
- package/dist/client.d.cts.map +1 -1
- package/dist/client.d.mts +72 -22
- package/dist/client.d.mts.map +1 -1
- package/dist/client.mjs +66 -42
- package/dist/client.mjs.map +1 -1
- package/dist/cloudflare/index.d.cts +4 -3
- package/dist/cloudflare/index.d.mts +4 -3
- package/dist/cloudflare/runner.cjs +3 -1
- package/dist/cloudflare/runner.cjs.map +1 -1
- package/dist/cloudflare/runner.d.cts.map +1 -1
- package/dist/cloudflare/runner.d.mts.map +1 -1
- package/dist/cloudflare/runner.mjs +3 -1
- package/dist/cloudflare/runner.mjs.map +1 -1
- package/dist/cloudflare/worker.cjs +12 -1
- package/dist/cloudflare/worker.cjs.map +1 -1
- package/dist/cloudflare/worker.d.cts +9 -1
- package/dist/cloudflare/worker.d.cts.map +1 -1
- package/dist/cloudflare/worker.d.mts +9 -1
- package/dist/cloudflare/worker.d.mts.map +1 -1
- package/dist/cloudflare/worker.mjs +12 -1
- package/dist/cloudflare/worker.mjs.map +1 -1
- package/dist/contract.cjs.map +1 -1
- package/dist/contract.d.cts +4 -4
- package/dist/contract.d.mts +4 -4
- package/dist/contract.mjs.map +1 -1
- package/dist/dsl/trigger.cjs +2 -1
- package/dist/dsl/trigger.cjs.map +1 -1
- package/dist/dsl/trigger.d.cts +6 -0
- package/dist/dsl/trigger.d.cts.map +1 -1
- package/dist/dsl/trigger.d.mts +6 -0
- package/dist/dsl/trigger.d.mts.map +1 -1
- package/dist/dsl/trigger.mjs +2 -1
- package/dist/dsl/trigger.mjs.map +1 -1
- package/dist/engine/condition.cjs +3 -3
- package/dist/engine/condition.cjs.map +1 -1
- package/dist/engine/condition.d.cts +3 -3
- package/dist/engine/condition.d.mts +3 -3
- package/dist/engine/condition.mjs +3 -3
- package/dist/engine/condition.mjs.map +1 -1
- package/dist/engine/interpreter.cjs +9 -5
- package/dist/engine/interpreter.cjs.map +1 -1
- package/dist/engine/interpreter.d.cts.map +1 -1
- package/dist/engine/interpreter.d.mts.map +1 -1
- package/dist/engine/interpreter.mjs +9 -5
- package/dist/engine/interpreter.mjs.map +1 -1
- package/dist/engine/ports.d.cts +2 -0
- package/dist/engine/ports.d.cts.map +1 -1
- package/dist/engine/ports.d.mts +2 -0
- package/dist/engine/ports.d.mts.map +1 -1
- package/dist/host/batch.cjs +172 -0
- package/dist/host/batch.cjs.map +1 -0
- package/dist/host/batch.d.cts +88 -0
- package/dist/host/batch.d.cts.map +1 -0
- package/dist/host/batch.d.mts +88 -0
- package/dist/host/batch.d.mts.map +1 -0
- package/dist/host/batch.mjs +163 -0
- package/dist/host/batch.mjs.map +1 -0
- package/dist/host/execute.cjs +10 -6
- package/dist/host/execute.cjs.map +1 -1
- package/dist/host/execute.mjs +10 -6
- package/dist/host/execute.mjs.map +1 -1
- package/dist/host/http.cjs +30 -89
- package/dist/host/http.cjs.map +1 -1
- package/dist/host/http.d.cts +1 -1
- package/dist/host/http.d.cts.map +1 -1
- package/dist/host/http.d.mts +1 -1
- package/dist/host/http.d.mts.map +1 -1
- package/dist/host/http.mjs +24 -83
- package/dist/host/http.mjs.map +1 -1
- package/dist/host/ingest.cjs +56 -13
- package/dist/host/ingest.cjs.map +1 -1
- package/dist/host/ingest.d.cts +17 -6
- package/dist/host/ingest.d.cts.map +1 -1
- package/dist/host/ingest.d.mts +17 -6
- package/dist/host/ingest.d.mts.map +1 -1
- package/dist/host/ingest.mjs +55 -14
- package/dist/host/ingest.mjs.map +1 -1
- package/dist/host/journey-host.cjs +6 -0
- package/dist/host/journey-host.cjs.map +1 -1
- package/dist/host/journey-host.d.cts +5 -2
- package/dist/host/journey-host.d.cts.map +1 -1
- package/dist/host/journey-host.d.mts +5 -2
- package/dist/host/journey-host.d.mts.map +1 -1
- package/dist/host/journey-host.mjs +6 -0
- package/dist/host/journey-host.mjs.map +1 -1
- package/dist/host/platform.cjs.map +1 -1
- package/dist/host/platform.d.cts +2 -0
- package/dist/host/platform.d.cts.map +1 -1
- package/dist/host/platform.d.mts +2 -0
- package/dist/host/platform.d.mts.map +1 -1
- package/dist/host/platform.mjs.map +1 -1
- package/dist/ir.cjs +2 -1
- package/dist/ir.cjs.map +1 -1
- package/dist/ir.d.cts +3 -0
- package/dist/ir.d.cts.map +1 -1
- package/dist/ir.d.mts +3 -0
- package/dist/ir.d.mts.map +1 -1
- package/dist/ir.mjs +2 -1
- package/dist/ir.mjs.map +1 -1
- package/dist/store/facts.cjs +13 -9
- package/dist/store/facts.cjs.map +1 -1
- package/dist/store/facts.d.cts +6 -2
- package/dist/store/facts.d.cts.map +1 -1
- package/dist/store/facts.d.mts +6 -2
- package/dist/store/facts.d.mts.map +1 -1
- package/dist/store/facts.mjs +13 -9
- package/dist/store/facts.mjs.map +1 -1
- package/dist/store/identity.cjs +34 -0
- package/dist/store/identity.cjs.map +1 -0
- package/dist/store/identity.d.cts +13 -0
- package/dist/store/identity.d.cts.map +1 -0
- package/dist/store/identity.d.mts +13 -0
- package/dist/store/identity.d.mts.map +1 -0
- package/dist/store/identity.mjs +30 -0
- package/dist/store/identity.mjs.map +1 -0
- package/dist/store/index.cjs +5 -0
- package/dist/store/index.cjs.map +1 -1
- package/dist/store/index.d.cts +49 -15
- package/dist/store/index.d.cts.map +1 -1
- package/dist/store/index.d.mts +49 -15
- package/dist/store/index.d.mts.map +1 -1
- package/dist/store/index.mjs +2 -1
- package/dist/store/index.mjs.map +1 -1
- package/dist/store/postgres.cjs +206 -44
- package/dist/store/postgres.cjs.map +1 -1
- package/dist/store/postgres.d.cts +17 -4
- package/dist/store/postgres.d.cts.map +1 -1
- package/dist/store/postgres.d.mts +17 -4
- package/dist/store/postgres.d.mts.map +1 -1
- package/dist/store/postgres.mjs +207 -45
- package/dist/store/postgres.mjs.map +1 -1
- package/dist/store/tables.cjs +24 -6
- package/dist/store/tables.cjs.map +1 -1
- package/dist/store/tables.d.cts +1 -0
- package/dist/store/tables.d.cts.map +1 -1
- package/dist/store/tables.d.mts +1 -0
- package/dist/store/tables.d.mts.map +1 -1
- package/dist/store/tables.mjs +24 -6
- package/dist/store/tables.mjs.map +1 -1
- package/dist/webpush.cjs.map +1 -1
- package/dist/webpush.d.cts +1 -1
- package/dist/webpush.d.mts +1 -1
- package/dist/webpush.mjs.map +1 -1
- package/package.json +1 -1
package/dist/store/postgres.mjs
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import { ConditionIR, WorkflowIR } from "../ir.mjs";
|
|
2
2
|
import { UserErasedError } from "./erasure.mjs";
|
|
3
|
+
import { visitorIdOf, visitorSubject } from "./identity.mjs";
|
|
3
4
|
import { uuidv7 } from "./sql.mjs";
|
|
4
|
-
import { lockUserTransaction } from "./user-lock.mjs";
|
|
5
|
+
import { beginReadCommitted, lockUser, lockUserTransaction } from "./user-lock.mjs";
|
|
5
6
|
import { PostgresReporting } from "./postgres-reporting.mjs";
|
|
6
7
|
import { DEFAULT_SCHEMA, TABLES, columnName, indexName, uniqueConstraintName } from "./tables.mjs";
|
|
7
8
|
// src/store/postgres.ts
|
|
9
|
+
/** Who a device row belongs to: its user, or before sign-in its visitor. */
|
|
10
|
+
function deviceOwner(row) {
|
|
11
|
+
if (row.user_id !== null) return row.user_id;
|
|
12
|
+
return row.visitor_id === null ? void 0 : visitorSubject(row.visitor_id);
|
|
13
|
+
}
|
|
8
14
|
/** information_schema's udt_name per column type. */
|
|
9
15
|
const UDT = {
|
|
10
16
|
uuid: "uuid",
|
|
@@ -184,16 +190,25 @@ var PostgresJourneyStore = class {
|
|
|
184
190
|
const row = (await this.sql.query(`SELECT props FROM ${this.t.profile} WHERE user_id = $1`, [userId])).at(0);
|
|
185
191
|
return row === void 0 ? void 0 : asJson(row.props);
|
|
186
192
|
}
|
|
187
|
-
async mergeProfile(userId, props) {
|
|
193
|
+
async mergeProfile(userId, props, at = Date.now()) {
|
|
188
194
|
return this.sql.transaction(async (tx) => {
|
|
189
195
|
await this.lockUser(tx, userId);
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
196
|
+
const times = asJson((await tx.query(`SELECT props_at FROM ${this.t.profile} WHERE user_id = $1`, [userId]))[0]?.props_at ?? null) ?? {};
|
|
197
|
+
const patch = Object.fromEntries(Object.entries(props).filter(([key]) => (times[key] ?? Number.NEGATIVE_INFINITY) <= at));
|
|
198
|
+
const stamped = {
|
|
199
|
+
...times,
|
|
200
|
+
...Object.fromEntries(Object.keys(patch).map((k) => [k, at]))
|
|
201
|
+
};
|
|
202
|
+
const rows = await tx.query(`INSERT INTO ${this.t.profile} (user_id, props, props_at)
|
|
203
|
+
SELECT $1, jsonb_strip_nulls($2::text::jsonb), $4::text::jsonb ${this.notErased("$1")}
|
|
204
|
+
ON CONFLICT (user_id) DO UPDATE SET
|
|
205
|
+
props = jsonb_strip_nulls(${this.t.profile}.props || $3::text::jsonb),
|
|
206
|
+
props_at = $4::text::jsonb
|
|
193
207
|
RETURNING props`, [
|
|
194
208
|
userId,
|
|
195
|
-
JSON.stringify(
|
|
196
|
-
JSON.stringify(
|
|
209
|
+
JSON.stringify(patch),
|
|
210
|
+
JSON.stringify(patch),
|
|
211
|
+
JSON.stringify(stamped)
|
|
197
212
|
]);
|
|
198
213
|
if (rows.length === 0) throw new UserErasedError(userId);
|
|
199
214
|
return asJson(rows[0].props);
|
|
@@ -201,10 +216,10 @@ var PostgresJourneyStore = class {
|
|
|
201
216
|
}
|
|
202
217
|
async listUserIds(page) {
|
|
203
218
|
const params = [];
|
|
204
|
-
let where = ""
|
|
219
|
+
let where = ` WHERE user_id NOT LIKE '${visitorSubject("")}%'`;
|
|
205
220
|
if (page?.after !== void 0) {
|
|
206
221
|
params.push(page.after);
|
|
207
|
-
where
|
|
222
|
+
where += ` AND user_id > $${params.length}`;
|
|
208
223
|
}
|
|
209
224
|
let limit = "";
|
|
210
225
|
if (page?.limit !== void 0) {
|
|
@@ -213,23 +228,160 @@ var PostgresJourneyStore = class {
|
|
|
213
228
|
}
|
|
214
229
|
return (await this.sql.query(`SELECT user_id FROM (SELECT user_id FROM ${this.t.profile} UNION SELECT user_id FROM ${this.t.event}) u${where} ORDER BY user_id${limit}`, params)).map((row) => row.user_id);
|
|
215
230
|
}
|
|
216
|
-
listDevices(
|
|
217
|
-
|
|
231
|
+
listDevices(subject) {
|
|
232
|
+
const visitorId = visitorIdOf(subject);
|
|
233
|
+
return this.sql.query(visitorId === void 0 ? `SELECT token, platform FROM ${this.t.device} WHERE user_id = $1 ORDER BY updated_at DESC` : `SELECT token, platform FROM ${this.t.device} WHERE visitor_id = $1 AND user_id IS NULL ORDER BY updated_at DESC`, [visitorId ?? subject]);
|
|
218
234
|
}
|
|
219
235
|
async upsertDevice(device) {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
236
|
+
const { deviceId = null, userId, visitorId = null, token, platform, at } = device;
|
|
237
|
+
const subject = userId ?? visitorSubject(visitorId ?? "");
|
|
238
|
+
return this.sql.transaction(async (tx) => {
|
|
239
|
+
await this.lockUser(tx, subject);
|
|
240
|
+
const previous = await tx.query(`SELECT token, user_id, visitor_id FROM ${this.t.device} WHERE token = $1 OR device_id = $2 FOR UPDATE`, [token, deviceId]);
|
|
241
|
+
for (const row of previous) {
|
|
242
|
+
if (row.token === token) continue;
|
|
243
|
+
const owner = deviceOwner(row);
|
|
244
|
+
await tx.query(owner === void 0 || owner === subject ? `DELETE FROM ${this.t.device} WHERE token = $1` : `UPDATE ${this.t.device} SET device_id = NULL WHERE token = $1`, [row.token]);
|
|
245
|
+
}
|
|
246
|
+
if ((await tx.query(`INSERT INTO ${this.t.device} (device_id, user_id, visitor_id, token, platform, created_at, updated_at)
|
|
247
|
+
SELECT $1, $2, $3, $4, $5, ${TS("$6")}, ${TS("$6")} ${this.notErased("$7")}
|
|
248
|
+
ON CONFLICT (token) DO UPDATE SET
|
|
249
|
+
device_id = COALESCE(EXCLUDED.device_id, ${this.t.device}.device_id),
|
|
250
|
+
user_id = EXCLUDED.user_id,
|
|
251
|
+
visitor_id = COALESCE(EXCLUDED.visitor_id, ${this.t.device}.visitor_id),
|
|
252
|
+
platform = EXCLUDED.platform,
|
|
253
|
+
updated_at = EXCLUDED.updated_at
|
|
225
254
|
RETURNING token`, [
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
255
|
+
deviceId,
|
|
256
|
+
userId,
|
|
257
|
+
visitorId,
|
|
258
|
+
token,
|
|
259
|
+
platform,
|
|
260
|
+
at,
|
|
261
|
+
subject
|
|
262
|
+
])).length === 0) throw new UserErasedError(subject);
|
|
263
|
+
const owners = previous.flatMap((row) => {
|
|
264
|
+
const owner = deviceOwner(row);
|
|
265
|
+
return owner === void 0 || owner === subject ? [] : [owner];
|
|
266
|
+
});
|
|
267
|
+
return [...new Set(owners)];
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
async linkedUser(visitorId) {
|
|
271
|
+
return (await this.sql.query(`SELECT user_id FROM ${this.t.visitor} WHERE visitor_id = $1`, [visitorId]))[0]?.user_id ?? void 0;
|
|
272
|
+
}
|
|
273
|
+
async touchVisitor(visitorId, at) {
|
|
274
|
+
return (await this.sql.query(`INSERT INTO ${this.t.visitor} (visitor_id, user_id, created_at, updated_at)
|
|
275
|
+
VALUES ($1, NULL, ${TS("$2")}, ${TS("$2")})
|
|
276
|
+
ON CONFLICT (visitor_id) DO UPDATE SET updated_at = EXCLUDED.updated_at
|
|
277
|
+
RETURNING user_id`, [visitorId, at]))[0]?.user_id ?? visitorSubject(visitorId);
|
|
278
|
+
}
|
|
279
|
+
async pruneVisitors(before, limit) {
|
|
280
|
+
const idle = `user_id IS NULL AND updated_at < ${TS("$1")} AND NOT EXISTS (
|
|
281
|
+
SELECT 1 FROM ${this.t.entry} e
|
|
282
|
+
WHERE e.user_id = '${visitorSubject("")}' || visitor_id AND e.status = 'running')`;
|
|
283
|
+
const candidates = await this.sql.query(`SELECT visitor_id FROM ${this.t.visitor} WHERE ${idle} ORDER BY updated_at LIMIT $2`, [before, limit]);
|
|
284
|
+
let pruned = 0;
|
|
285
|
+
for (const { visitor_id: visitorId } of candidates) {
|
|
286
|
+
const subject = visitorSubject(visitorId);
|
|
287
|
+
await this.sql.transaction(async (tx) => {
|
|
288
|
+
await this.lockUser(tx, subject);
|
|
289
|
+
if ((await tx.query(`DELETE FROM ${this.t.visitor} WHERE visitor_id = $2 AND ${idle} RETURNING visitor_id`, [before, visitorId])).length === 0) return;
|
|
290
|
+
await this.deleteSubjectRows(tx, subject);
|
|
291
|
+
pruned += 1;
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
return pruned;
|
|
295
|
+
}
|
|
296
|
+
async entryOwner(instanceId) {
|
|
297
|
+
return (await this.sql.query(`SELECT user_id FROM ${this.t.entry} WHERE instance_id = $1`, [instanceId]))[0]?.user_id;
|
|
298
|
+
}
|
|
299
|
+
async linkVisitor(visitorId, userId, at) {
|
|
300
|
+
const subject = visitorSubject(visitorId);
|
|
301
|
+
if (await this.linkedUser(visitorId) !== void 0) return null;
|
|
302
|
+
return this.sql.transaction(async (tx) => {
|
|
303
|
+
await beginReadCommitted(tx);
|
|
304
|
+
for (const key of [subject, userId].sort()) await lockUser(tx, this.schema, key);
|
|
305
|
+
if ((await tx.query(`INSERT INTO ${this.t.visitor} (visitor_id, user_id, created_at, updated_at)
|
|
306
|
+
SELECT $1, $2, ${TS("$3")}, ${TS("$3")} ${this.notErased("$2")}
|
|
307
|
+
ON CONFLICT (visitor_id) DO UPDATE SET user_id = EXCLUDED.user_id, updated_at = EXCLUDED.updated_at
|
|
308
|
+
WHERE ${this.t.visitor}.user_id IS NULL
|
|
309
|
+
RETURNING visitor_id`, [
|
|
310
|
+
visitorId,
|
|
311
|
+
userId,
|
|
312
|
+
at
|
|
313
|
+
])).length === 0) {
|
|
314
|
+
if ((await tx.query(`SELECT 1 FROM ${this.t.erasure} WHERE user_id = $1`, [userId])).length > 0) throw new UserErasedError(userId);
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
const droppedIds = (await tx.query(`DELETE FROM ${this.t.entry} v USING ${this.t.entry} u
|
|
318
|
+
WHERE v.user_id = $1 AND u.user_id = $2 AND u.workflow = v.workflow
|
|
319
|
+
RETURNING v.instance_id`, [subject, userId])).map((row) => row.instance_id);
|
|
320
|
+
await tx.query(`DELETE FROM ${this.t.subscription} WHERE wake_handle = ANY($1::text[])`, [droppedIds]);
|
|
321
|
+
await tx.query(`DELETE FROM ${this.t.execution} WHERE instance_id = ANY($1::text[])`, [droppedIds]);
|
|
322
|
+
await tx.query(`DELETE FROM ${this.t.event} v USING ${this.t.event} u
|
|
323
|
+
WHERE v.user_id = $1 AND u.user_id = $2 AND u.event_id = v.event_id`, [subject, userId]);
|
|
324
|
+
const events = await tx.query(`SELECT DISTINCT name FROM ${this.t.event} WHERE user_id = $1`, [subject]);
|
|
325
|
+
await tx.query(`INSERT INTO ${this.t.suppression} (user_id, channel, topic, ts)
|
|
326
|
+
SELECT $2, channel, topic, ts FROM ${this.t.suppression} WHERE user_id = $1
|
|
327
|
+
ON CONFLICT (user_id, channel, topic) DO NOTHING`, [subject, userId]);
|
|
328
|
+
await tx.query(`DELETE FROM ${this.t.suppression} WHERE user_id = $1`, [subject]);
|
|
329
|
+
const props = await this.mergeProfileInto(tx, subject, userId);
|
|
330
|
+
for (const table of [
|
|
331
|
+
this.t.event,
|
|
332
|
+
this.t.entry,
|
|
333
|
+
this.t.execution,
|
|
334
|
+
this.t.subscription,
|
|
335
|
+
this.t.instance,
|
|
336
|
+
this.t.message
|
|
337
|
+
]) await tx.query(`UPDATE ${table} SET user_id = $2 WHERE user_id = $1`, [subject, userId]);
|
|
338
|
+
await tx.query(`UPDATE ${this.t.device} SET user_id = $2, updated_at = ${TS("$3")}
|
|
339
|
+
WHERE visitor_id = $1 AND user_id IS NULL`, [
|
|
340
|
+
visitorId,
|
|
341
|
+
userId,
|
|
342
|
+
at
|
|
343
|
+
]);
|
|
344
|
+
return {
|
|
345
|
+
dropped: droppedIds,
|
|
346
|
+
events: events.map((row) => row.name),
|
|
347
|
+
props
|
|
348
|
+
};
|
|
231
349
|
});
|
|
232
350
|
}
|
|
351
|
+
/**
|
|
352
|
+
* The visitor's properties under the user's, a key the user has a time for going to the later
|
|
353
|
+
* write, one a row older than the times going to the user if they hold it. The visitor's row
|
|
354
|
+
* goes. Answers the keys that moved.
|
|
355
|
+
*/
|
|
356
|
+
async mergeProfileInto(tx, subject, userId) {
|
|
357
|
+
const rows = await tx.query(`SELECT user_id, props, props_at FROM ${this.t.profile} WHERE user_id = ANY($1::text[])`, [[subject, userId]]);
|
|
358
|
+
const from = rows.find((row) => row.user_id === subject);
|
|
359
|
+
if (from === void 0) return [];
|
|
360
|
+
const to = rows.find((row) => row.user_id === userId);
|
|
361
|
+
const fromTimes = asJson(from.props_at ?? null) ?? {};
|
|
362
|
+
const toTimes = asJson(to?.props_at ?? null) ?? {};
|
|
363
|
+
const toProps = to === void 0 ? {} : asJson(to.props);
|
|
364
|
+
const moved = Object.entries(asJson(from.props)).filter(([key]) => {
|
|
365
|
+
const written = toTimes[key];
|
|
366
|
+
return written === void 0 ? !Object.hasOwn(toProps, key) : (fromTimes[key] ?? 0) > written;
|
|
367
|
+
});
|
|
368
|
+
const props = {
|
|
369
|
+
...toProps,
|
|
370
|
+
...Object.fromEntries(moved)
|
|
371
|
+
};
|
|
372
|
+
const times = {
|
|
373
|
+
...toTimes,
|
|
374
|
+
...Object.fromEntries(moved.map(([key]) => [key, fromTimes[key] ?? 0]))
|
|
375
|
+
};
|
|
376
|
+
await tx.query(`INSERT INTO ${this.t.profile} (user_id, props, props_at) VALUES ($1, $2::text::jsonb, $3::text::jsonb)
|
|
377
|
+
ON CONFLICT (user_id) DO UPDATE SET props = EXCLUDED.props, props_at = EXCLUDED.props_at`, [
|
|
378
|
+
userId,
|
|
379
|
+
JSON.stringify(props),
|
|
380
|
+
JSON.stringify(times)
|
|
381
|
+
]);
|
|
382
|
+
await tx.query(`DELETE FROM ${this.t.profile} WHERE user_id = $1`, [subject]);
|
|
383
|
+
return moved.map(([key]) => key);
|
|
384
|
+
}
|
|
233
385
|
async requestErasure(userId, at) {
|
|
234
386
|
await this.sql.transaction(async (tx) => {
|
|
235
387
|
await this.lockUser(tx, userId);
|
|
@@ -269,27 +421,33 @@ var PostgresJourneyStore = class {
|
|
|
269
421
|
async forgetUser(userId, at) {
|
|
270
422
|
await this.sql.transaction(async (tx) => {
|
|
271
423
|
await this.lockUser(tx, userId);
|
|
272
|
-
|
|
273
|
-
this.t.messageOpen,
|
|
274
|
-
this.t.messageClick,
|
|
275
|
-
this.t.messageLink
|
|
276
|
-
]) await tx.query(`DELETE FROM ${child} WHERE message_id IN (SELECT id FROM ${this.t.message} WHERE user_id = $1)`, [userId]);
|
|
277
|
-
for (const table of [
|
|
278
|
-
this.t.message,
|
|
279
|
-
this.t.instance,
|
|
280
|
-
this.t.entry,
|
|
281
|
-
this.t.execution,
|
|
282
|
-
this.t.subscription,
|
|
283
|
-
this.t.segmentStart,
|
|
284
|
-
this.t.segmentMember,
|
|
285
|
-
this.t.suppression,
|
|
286
|
-
this.t.device,
|
|
287
|
-
this.t.event,
|
|
288
|
-
this.t.profile
|
|
289
|
-
]) await tx.query(`DELETE FROM ${table} WHERE user_id = $1`, [userId]);
|
|
424
|
+
await this.deleteSubjectRows(tx, userId);
|
|
290
425
|
await tx.query(`UPDATE ${this.t.erasure} SET erased_at = COALESCE(erased_at, ${TS("$2")}) WHERE user_id = $1`, [userId, at]);
|
|
291
426
|
});
|
|
292
427
|
}
|
|
428
|
+
/** Everything kept about a user or visitor, on a transaction holding their lock. */
|
|
429
|
+
async deleteSubjectRows(tx, subject) {
|
|
430
|
+
for (const child of [
|
|
431
|
+
this.t.messageOpen,
|
|
432
|
+
this.t.messageClick,
|
|
433
|
+
this.t.messageLink
|
|
434
|
+
]) await tx.query(`DELETE FROM ${child} WHERE message_id IN (SELECT id FROM ${this.t.message} WHERE user_id = $1)`, [subject]);
|
|
435
|
+
for (const table of [
|
|
436
|
+
this.t.message,
|
|
437
|
+
this.t.instance,
|
|
438
|
+
this.t.entry,
|
|
439
|
+
this.t.execution,
|
|
440
|
+
this.t.subscription,
|
|
441
|
+
this.t.segmentStart,
|
|
442
|
+
this.t.segmentMember,
|
|
443
|
+
this.t.suppression,
|
|
444
|
+
this.t.device,
|
|
445
|
+
this.t.event,
|
|
446
|
+
this.t.profile
|
|
447
|
+
]) await tx.query(`DELETE FROM ${table} WHERE user_id = $1`, [subject]);
|
|
448
|
+
const visitorId = visitorIdOf(subject);
|
|
449
|
+
if (visitorId !== void 0) await tx.query(`DELETE FROM ${this.t.device} WHERE visitor_id = $1 AND user_id IS NULL`, [visitorId]);
|
|
450
|
+
}
|
|
293
451
|
async isSuppressed(userId, channel, topic) {
|
|
294
452
|
return (await this.sql.query(`SELECT 1 AS x FROM ${this.t.suppression}
|
|
295
453
|
WHERE user_id = $1 AND channel = $2 AND (topic IS NULL OR topic = $3::text)`, [
|
|
@@ -322,12 +480,14 @@ var PostgresJourneyStore = class {
|
|
|
322
480
|
});
|
|
323
481
|
}
|
|
324
482
|
async removeDevice(token) {
|
|
325
|
-
|
|
483
|
+
const rows = await this.sql.query(`DELETE FROM ${this.t.device} WHERE token = $1 RETURNING user_id, visitor_id`, [token]);
|
|
484
|
+
return rows[0] === void 0 ? void 0 : deviceOwner(rows[0]);
|
|
326
485
|
}
|
|
327
486
|
async unbindDevice(token) {
|
|
328
|
-
|
|
329
|
-
UPDATE ${this.t.device} d SET user_id = NULL FROM old WHERE d.token = old.token
|
|
330
|
-
RETURNING old.user_id`, [token])
|
|
487
|
+
const rows = await this.sql.query(`WITH old AS (SELECT token, user_id, visitor_id FROM ${this.t.device} WHERE token = $1 FOR UPDATE)
|
|
488
|
+
UPDATE ${this.t.device} d SET user_id = NULL, visitor_id = NULL, updated_at = ${TS("$2")} FROM old WHERE d.token = old.token
|
|
489
|
+
RETURNING old.user_id, old.visitor_id`, [token, Date.now()]);
|
|
490
|
+
return rows[0] === void 0 ? void 0 : deviceOwner(rows[0]);
|
|
331
491
|
}
|
|
332
492
|
async listSegments() {
|
|
333
493
|
return (await this.sql.query(`SELECT id, condition FROM ${this.t.segment} ORDER BY id`)).map((row) => ({
|
|
@@ -340,11 +500,12 @@ var PostgresJourneyStore = class {
|
|
|
340
500
|
return row === void 0 ? void 0 : ConditionIR.parse(asJson(row.condition));
|
|
341
501
|
}
|
|
342
502
|
async findTriggers(event) {
|
|
343
|
-
return (await this.sql.query(`SELECT workflow, hash, "where" AS where_clause, filter, version FROM ${this.t.trigger} WHERE event = $1`, [event])).map((row) => ({
|
|
503
|
+
return (await this.sql.query(`SELECT workflow, hash, "where" AS where_clause, filter, visitors, version FROM ${this.t.trigger} WHERE event = $1`, [event])).map((row) => ({
|
|
344
504
|
workflow: row.workflow,
|
|
345
505
|
hash: row.hash,
|
|
346
506
|
where: row.where_clause === null ? null : ConditionIR.parse(asJson(row.where_clause)),
|
|
347
507
|
filter: row.filter === null ? null : ConditionIR.parse(asJson(row.filter)),
|
|
508
|
+
visitors: row.visitors === true,
|
|
348
509
|
version: row.version ?? void 0
|
|
349
510
|
}));
|
|
350
511
|
}
|
|
@@ -411,12 +572,13 @@ var PostgresJourneyStore = class {
|
|
|
411
572
|
JSON.stringify(segment.condition),
|
|
412
573
|
segment.contentHash
|
|
413
574
|
]);
|
|
414
|
-
for (const workflow of bundle.workflows) if (workflow.trigger.type === "event") await tx.query(`INSERT INTO ${this.t.trigger} (workflow, hash, event, "where", filter, version) VALUES ($1, $2, $3, $4::text::jsonb, $5::text::jsonb, $6)`, [
|
|
575
|
+
for (const workflow of bundle.workflows) if (workflow.trigger.type === "event") await tx.query(`INSERT INTO ${this.t.trigger} (workflow, hash, event, "where", filter, visitors, version) VALUES ($1, $2, $3, $4::text::jsonb, $5::text::jsonb, $6, $7)`, [
|
|
415
576
|
workflow.id,
|
|
416
577
|
workflow.contentHash,
|
|
417
578
|
workflow.trigger.event,
|
|
418
579
|
workflow.trigger.where === void 0 ? null : JSON.stringify(workflow.trigger.where),
|
|
419
580
|
workflow.trigger.filter === void 0 ? null : JSON.stringify(workflow.trigger.filter),
|
|
581
|
+
workflow.trigger.visitors === true,
|
|
420
582
|
version ?? null
|
|
421
583
|
]);
|
|
422
584
|
else if (workflow.trigger.type === "segment") await tx.query(`INSERT INTO ${this.t.segmentTrigger} (workflow, hash, segment, version) VALUES ($1, $2, $3, $4)`, [
|