@shware/workflow 7.0.0 → 9.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 +125 -35
- 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/aws/index.cjs +3 -3
- package/dist/aws/index.cjs.map +1 -1
- package/dist/aws/index.mjs +3 -3
- package/dist/aws/index.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 +5 -4
- package/dist/cloudflare/index.d.mts +5 -4
- 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 +84 -35
- package/dist/host/ingest.cjs.map +1 -1
- package/dist/host/ingest.d.cts +29 -16
- package/dist/host/ingest.d.cts.map +1 -1
- package/dist/host/ingest.d.mts +29 -16
- package/dist/host/ingest.d.mts.map +1 -1
- package/dist/host/ingest.mjs +83 -36
- 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/condition-sql.cjs +144 -0
- package/dist/store/condition-sql.cjs.map +1 -0
- package/dist/store/condition-sql.d.cts +38 -0
- package/dist/store/condition-sql.d.cts.map +1 -0
- package/dist/store/condition-sql.d.mts +38 -0
- package/dist/store/condition-sql.d.mts.map +1 -0
- package/dist/store/condition-sql.mjs +143 -0
- package/dist/store/condition-sql.mjs.map +1 -0
- 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 +67 -21
- package/dist/store/index.d.cts.map +1 -1
- package/dist/store/index.d.mts +67 -21
- 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 +248 -54
- package/dist/store/postgres.cjs.map +1 -1
- package/dist/store/postgres.d.cts +25 -7
- package/dist/store/postgres.d.cts.map +1 -1
- package/dist/store/postgres.d.mts +25 -7
- package/dist/store/postgres.d.mts.map +1 -1
- package/dist/store/postgres.mjs +249 -55
- 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.cjs
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
const require_ir = require("../ir.cjs");
|
|
3
3
|
const require_store_erasure = require("./erasure.cjs");
|
|
4
|
+
const require_store_identity = require("./identity.cjs");
|
|
5
|
+
const require_store_condition_sql = require("./condition-sql.cjs");
|
|
4
6
|
const require_store_sql = require("./sql.cjs");
|
|
5
7
|
const require_store_user_lock = require("./user-lock.cjs");
|
|
6
8
|
const require_store_postgres_reporting = require("./postgres-reporting.cjs");
|
|
7
9
|
const require_store_tables = require("./tables.cjs");
|
|
8
10
|
// src/store/postgres.ts
|
|
11
|
+
/** Who a device row belongs to: its user, or before sign-in its visitor. */
|
|
12
|
+
function deviceOwner(row) {
|
|
13
|
+
if (row.user_id !== null) return row.user_id;
|
|
14
|
+
return row.visitor_id === null ? void 0 : require_store_identity.visitorSubject(row.visitor_id);
|
|
15
|
+
}
|
|
9
16
|
/** information_schema's udt_name per column type. */
|
|
10
17
|
const UDT = {
|
|
11
18
|
uuid: "uuid",
|
|
@@ -185,51 +192,228 @@ var PostgresJourneyStore = class {
|
|
|
185
192
|
const row = (await this.sql.query(`SELECT props FROM ${this.t.profile} WHERE user_id = $1`, [userId])).at(0);
|
|
186
193
|
return row === void 0 ? void 0 : asJson(row.props);
|
|
187
194
|
}
|
|
188
|
-
async mergeProfile(userId, props) {
|
|
195
|
+
async mergeProfile(userId, props, at = Date.now()) {
|
|
189
196
|
return this.sql.transaction(async (tx) => {
|
|
190
197
|
await this.lockUser(tx, userId);
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
|
|
198
|
+
const times = asJson((await tx.query(`SELECT props_at FROM ${this.t.profile} WHERE user_id = $1`, [userId]))[0]?.props_at ?? null) ?? {};
|
|
199
|
+
const patch = Object.fromEntries(Object.entries(props).filter(([key]) => (times[key] ?? Number.NEGATIVE_INFINITY) <= at));
|
|
200
|
+
const stamped = {
|
|
201
|
+
...times,
|
|
202
|
+
...Object.fromEntries(Object.keys(patch).map((k) => [k, at]))
|
|
203
|
+
};
|
|
204
|
+
const rows = await tx.query(`INSERT INTO ${this.t.profile} (user_id, props, props_at)
|
|
205
|
+
SELECT $1, jsonb_strip_nulls($2::text::jsonb), $4::text::jsonb ${this.notErased("$1")}
|
|
206
|
+
ON CONFLICT (user_id) DO UPDATE SET
|
|
207
|
+
props = jsonb_strip_nulls(${this.t.profile}.props || $3::text::jsonb),
|
|
208
|
+
props_at = $4::text::jsonb
|
|
194
209
|
RETURNING props`, [
|
|
195
210
|
userId,
|
|
196
|
-
JSON.stringify(
|
|
197
|
-
JSON.stringify(
|
|
211
|
+
JSON.stringify(patch),
|
|
212
|
+
JSON.stringify(patch),
|
|
213
|
+
JSON.stringify(stamped)
|
|
198
214
|
]);
|
|
199
215
|
if (rows.length === 0) throw new require_store_erasure.UserErasedError(userId);
|
|
200
216
|
return asJson(rows[0].props);
|
|
201
217
|
});
|
|
202
218
|
}
|
|
203
|
-
async
|
|
219
|
+
async listSegmentCandidates(page) {
|
|
220
|
+
if (page.segments.length === 0) return [];
|
|
204
221
|
const params = [];
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
params.
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
222
|
+
const bind = (value) => {
|
|
223
|
+
params.push(value);
|
|
224
|
+
return `$${params.length}`;
|
|
225
|
+
};
|
|
226
|
+
const definitions = new Map(page.segments.map((segment) => [segment.id, segment.condition]));
|
|
227
|
+
const branches = page.segments.map(({ id, condition }) => {
|
|
228
|
+
const segment = bind(id);
|
|
229
|
+
const matches = require_store_condition_sql.conditionToSql(condition, {
|
|
230
|
+
tables: this.t,
|
|
231
|
+
user: "u.user_id",
|
|
232
|
+
props: "p.props",
|
|
233
|
+
nowMs: page.now,
|
|
234
|
+
segments: definitions,
|
|
235
|
+
bind
|
|
236
|
+
});
|
|
237
|
+
const key = `segment = ${segment}::text AND user_id = u.user_id`;
|
|
238
|
+
return `SELECT ${segment}::text AS segment
|
|
239
|
+
WHERE (${matches}) IS DISTINCT FROM EXISTS (SELECT 1 FROM ${this.t.segmentMember} WHERE ${key})
|
|
240
|
+
OR EXISTS (SELECT 1 FROM ${this.t.segmentStart} WHERE ${key})`;
|
|
241
|
+
});
|
|
242
|
+
let keyset = "";
|
|
243
|
+
if (page.after !== void 0) keyset = ` AND user_id > ${bind(page.after)}::text`;
|
|
244
|
+
return (await this.sql.query(`WITH u AS MATERIALIZED (
|
|
245
|
+
SELECT user_id FROM (
|
|
246
|
+
SELECT user_id FROM ${this.t.profile}
|
|
247
|
+
UNION SELECT user_id FROM ${this.t.event}
|
|
248
|
+
UNION SELECT user_id FROM ${this.t.segmentMember}
|
|
249
|
+
) known
|
|
250
|
+
-- Segments are about users: a visitor is not one until they sign in
|
|
251
|
+
WHERE user_id NOT LIKE '${require_store_identity.visitorSubject("")}%'${keyset}
|
|
252
|
+
)
|
|
253
|
+
SELECT u.user_id, json_agg(c.segment) AS segments
|
|
254
|
+
FROM u
|
|
255
|
+
LEFT JOIN ${this.t.profile} p ON p.user_id = u.user_id
|
|
256
|
+
CROSS JOIN LATERAL (${branches.join("\n UNION ALL ")}) c
|
|
257
|
+
GROUP BY u.user_id
|
|
258
|
+
ORDER BY u.user_id
|
|
259
|
+
LIMIT ${bind(page.limit)}::int`, params)).map((row) => ({
|
|
260
|
+
userId: row.user_id,
|
|
261
|
+
segments: asJson(row.segments)
|
|
262
|
+
}));
|
|
216
263
|
}
|
|
217
|
-
listDevices(
|
|
218
|
-
|
|
264
|
+
listDevices(subject) {
|
|
265
|
+
const visitorId = require_store_identity.visitorIdOf(subject);
|
|
266
|
+
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]);
|
|
219
267
|
}
|
|
220
268
|
async upsertDevice(device) {
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
269
|
+
const { deviceId = null, userId, visitorId = null, token, platform, at } = device;
|
|
270
|
+
const subject = userId ?? require_store_identity.visitorSubject(visitorId ?? "");
|
|
271
|
+
return this.sql.transaction(async (tx) => {
|
|
272
|
+
await this.lockUser(tx, subject);
|
|
273
|
+
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]);
|
|
274
|
+
for (const row of previous) {
|
|
275
|
+
if (row.token === token) continue;
|
|
276
|
+
const owner = deviceOwner(row);
|
|
277
|
+
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]);
|
|
278
|
+
}
|
|
279
|
+
if ((await tx.query(`INSERT INTO ${this.t.device} (device_id, user_id, visitor_id, token, platform, created_at, updated_at)
|
|
280
|
+
SELECT $1, $2, $3, $4, $5, ${TS("$6")}, ${TS("$6")} ${this.notErased("$7")}
|
|
281
|
+
ON CONFLICT (token) DO UPDATE SET
|
|
282
|
+
device_id = COALESCE(EXCLUDED.device_id, ${this.t.device}.device_id),
|
|
283
|
+
user_id = EXCLUDED.user_id,
|
|
284
|
+
visitor_id = COALESCE(EXCLUDED.visitor_id, ${this.t.device}.visitor_id),
|
|
285
|
+
platform = EXCLUDED.platform,
|
|
286
|
+
updated_at = EXCLUDED.updated_at
|
|
226
287
|
RETURNING token`, [
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
288
|
+
deviceId,
|
|
289
|
+
userId,
|
|
290
|
+
visitorId,
|
|
291
|
+
token,
|
|
292
|
+
platform,
|
|
293
|
+
at,
|
|
294
|
+
subject
|
|
295
|
+
])).length === 0) throw new require_store_erasure.UserErasedError(subject);
|
|
296
|
+
const owners = previous.flatMap((row) => {
|
|
297
|
+
const owner = deviceOwner(row);
|
|
298
|
+
return owner === void 0 || owner === subject ? [] : [owner];
|
|
299
|
+
});
|
|
300
|
+
return [...new Set(owners)];
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
async linkedUser(visitorId) {
|
|
304
|
+
return (await this.sql.query(`SELECT user_id FROM ${this.t.visitor} WHERE visitor_id = $1`, [visitorId]))[0]?.user_id ?? void 0;
|
|
305
|
+
}
|
|
306
|
+
async touchVisitor(visitorId, at) {
|
|
307
|
+
return (await this.sql.query(`INSERT INTO ${this.t.visitor} (visitor_id, user_id, created_at, updated_at)
|
|
308
|
+
VALUES ($1, NULL, ${TS("$2")}, ${TS("$2")})
|
|
309
|
+
ON CONFLICT (visitor_id) DO UPDATE SET updated_at = EXCLUDED.updated_at
|
|
310
|
+
RETURNING user_id`, [visitorId, at]))[0]?.user_id ?? require_store_identity.visitorSubject(visitorId);
|
|
311
|
+
}
|
|
312
|
+
async pruneVisitors(before, limit) {
|
|
313
|
+
const idle = `user_id IS NULL AND updated_at < ${TS("$1")} AND NOT EXISTS (
|
|
314
|
+
SELECT 1 FROM ${this.t.entry} e
|
|
315
|
+
WHERE e.user_id = '${require_store_identity.visitorSubject("")}' || visitor_id AND e.status = 'running')`;
|
|
316
|
+
const candidates = await this.sql.query(`SELECT visitor_id FROM ${this.t.visitor} WHERE ${idle} ORDER BY updated_at LIMIT $2`, [before, limit]);
|
|
317
|
+
let pruned = 0;
|
|
318
|
+
for (const { visitor_id: visitorId } of candidates) {
|
|
319
|
+
const subject = require_store_identity.visitorSubject(visitorId);
|
|
320
|
+
await this.sql.transaction(async (tx) => {
|
|
321
|
+
await this.lockUser(tx, subject);
|
|
322
|
+
if ((await tx.query(`DELETE FROM ${this.t.visitor} WHERE visitor_id = $2 AND ${idle} RETURNING visitor_id`, [before, visitorId])).length === 0) return;
|
|
323
|
+
await this.deleteSubjectRows(tx, subject);
|
|
324
|
+
pruned += 1;
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
return pruned;
|
|
328
|
+
}
|
|
329
|
+
async entryOwner(instanceId) {
|
|
330
|
+
return (await this.sql.query(`SELECT user_id FROM ${this.t.entry} WHERE instance_id = $1`, [instanceId]))[0]?.user_id;
|
|
331
|
+
}
|
|
332
|
+
async linkVisitor(visitorId, userId, at) {
|
|
333
|
+
const subject = require_store_identity.visitorSubject(visitorId);
|
|
334
|
+
if (await this.linkedUser(visitorId) !== void 0) return null;
|
|
335
|
+
return this.sql.transaction(async (tx) => {
|
|
336
|
+
await require_store_user_lock.beginReadCommitted(tx);
|
|
337
|
+
for (const key of [subject, userId].sort()) await require_store_user_lock.lockUser(tx, this.schema, key);
|
|
338
|
+
if ((await tx.query(`INSERT INTO ${this.t.visitor} (visitor_id, user_id, created_at, updated_at)
|
|
339
|
+
SELECT $1, $2, ${TS("$3")}, ${TS("$3")} ${this.notErased("$2")}
|
|
340
|
+
ON CONFLICT (visitor_id) DO UPDATE SET user_id = EXCLUDED.user_id, updated_at = EXCLUDED.updated_at
|
|
341
|
+
WHERE ${this.t.visitor}.user_id IS NULL
|
|
342
|
+
RETURNING visitor_id`, [
|
|
343
|
+
visitorId,
|
|
344
|
+
userId,
|
|
345
|
+
at
|
|
346
|
+
])).length === 0) {
|
|
347
|
+
if ((await tx.query(`SELECT 1 FROM ${this.t.erasure} WHERE user_id = $1`, [userId])).length > 0) throw new require_store_erasure.UserErasedError(userId);
|
|
348
|
+
return null;
|
|
349
|
+
}
|
|
350
|
+
const droppedIds = (await tx.query(`DELETE FROM ${this.t.entry} v USING ${this.t.entry} u
|
|
351
|
+
WHERE v.user_id = $1 AND u.user_id = $2 AND u.workflow = v.workflow
|
|
352
|
+
RETURNING v.instance_id`, [subject, userId])).map((row) => row.instance_id);
|
|
353
|
+
await tx.query(`DELETE FROM ${this.t.subscription} WHERE wake_handle = ANY($1::text[])`, [droppedIds]);
|
|
354
|
+
await tx.query(`DELETE FROM ${this.t.execution} WHERE instance_id = ANY($1::text[])`, [droppedIds]);
|
|
355
|
+
await tx.query(`DELETE FROM ${this.t.event} v USING ${this.t.event} u
|
|
356
|
+
WHERE v.user_id = $1 AND u.user_id = $2 AND u.event_id = v.event_id`, [subject, userId]);
|
|
357
|
+
const events = await tx.query(`SELECT DISTINCT name FROM ${this.t.event} WHERE user_id = $1`, [subject]);
|
|
358
|
+
await tx.query(`INSERT INTO ${this.t.suppression} (user_id, channel, topic, ts)
|
|
359
|
+
SELECT $2, channel, topic, ts FROM ${this.t.suppression} WHERE user_id = $1
|
|
360
|
+
ON CONFLICT (user_id, channel, topic) DO NOTHING`, [subject, userId]);
|
|
361
|
+
await tx.query(`DELETE FROM ${this.t.suppression} WHERE user_id = $1`, [subject]);
|
|
362
|
+
const props = await this.mergeProfileInto(tx, subject, userId);
|
|
363
|
+
for (const table of [
|
|
364
|
+
this.t.event,
|
|
365
|
+
this.t.entry,
|
|
366
|
+
this.t.execution,
|
|
367
|
+
this.t.subscription,
|
|
368
|
+
this.t.instance,
|
|
369
|
+
this.t.message
|
|
370
|
+
]) await tx.query(`UPDATE ${table} SET user_id = $2 WHERE user_id = $1`, [subject, userId]);
|
|
371
|
+
await tx.query(`UPDATE ${this.t.device} SET user_id = $2, updated_at = ${TS("$3")}
|
|
372
|
+
WHERE visitor_id = $1 AND user_id IS NULL`, [
|
|
373
|
+
visitorId,
|
|
374
|
+
userId,
|
|
375
|
+
at
|
|
376
|
+
]);
|
|
377
|
+
return {
|
|
378
|
+
dropped: droppedIds,
|
|
379
|
+
events: events.map((row) => row.name),
|
|
380
|
+
props
|
|
381
|
+
};
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* The visitor's properties under the user's, a key the user has a time for going to the later
|
|
386
|
+
* write, one a row older than the times going to the user if they hold it. The visitor's row
|
|
387
|
+
* goes. Answers the keys that moved.
|
|
388
|
+
*/
|
|
389
|
+
async mergeProfileInto(tx, subject, userId) {
|
|
390
|
+
const rows = await tx.query(`SELECT user_id, props, props_at FROM ${this.t.profile} WHERE user_id = ANY($1::text[])`, [[subject, userId]]);
|
|
391
|
+
const from = rows.find((row) => row.user_id === subject);
|
|
392
|
+
if (from === void 0) return [];
|
|
393
|
+
const to = rows.find((row) => row.user_id === userId);
|
|
394
|
+
const fromTimes = asJson(from.props_at ?? null) ?? {};
|
|
395
|
+
const toTimes = asJson(to?.props_at ?? null) ?? {};
|
|
396
|
+
const toProps = to === void 0 ? {} : asJson(to.props);
|
|
397
|
+
const moved = Object.entries(asJson(from.props)).filter(([key]) => {
|
|
398
|
+
const written = toTimes[key];
|
|
399
|
+
return written === void 0 ? !Object.hasOwn(toProps, key) : (fromTimes[key] ?? 0) > written;
|
|
232
400
|
});
|
|
401
|
+
const props = {
|
|
402
|
+
...toProps,
|
|
403
|
+
...Object.fromEntries(moved)
|
|
404
|
+
};
|
|
405
|
+
const times = {
|
|
406
|
+
...toTimes,
|
|
407
|
+
...Object.fromEntries(moved.map(([key]) => [key, fromTimes[key] ?? 0]))
|
|
408
|
+
};
|
|
409
|
+
await tx.query(`INSERT INTO ${this.t.profile} (user_id, props, props_at) VALUES ($1, $2::text::jsonb, $3::text::jsonb)
|
|
410
|
+
ON CONFLICT (user_id) DO UPDATE SET props = EXCLUDED.props, props_at = EXCLUDED.props_at`, [
|
|
411
|
+
userId,
|
|
412
|
+
JSON.stringify(props),
|
|
413
|
+
JSON.stringify(times)
|
|
414
|
+
]);
|
|
415
|
+
await tx.query(`DELETE FROM ${this.t.profile} WHERE user_id = $1`, [subject]);
|
|
416
|
+
return moved.map(([key]) => key);
|
|
233
417
|
}
|
|
234
418
|
async requestErasure(userId, at) {
|
|
235
419
|
await this.sql.transaction(async (tx) => {
|
|
@@ -270,27 +454,33 @@ var PostgresJourneyStore = class {
|
|
|
270
454
|
async forgetUser(userId, at) {
|
|
271
455
|
await this.sql.transaction(async (tx) => {
|
|
272
456
|
await this.lockUser(tx, userId);
|
|
273
|
-
|
|
274
|
-
this.t.messageOpen,
|
|
275
|
-
this.t.messageClick,
|
|
276
|
-
this.t.messageLink
|
|
277
|
-
]) await tx.query(`DELETE FROM ${child} WHERE message_id IN (SELECT id FROM ${this.t.message} WHERE user_id = $1)`, [userId]);
|
|
278
|
-
for (const table of [
|
|
279
|
-
this.t.message,
|
|
280
|
-
this.t.instance,
|
|
281
|
-
this.t.entry,
|
|
282
|
-
this.t.execution,
|
|
283
|
-
this.t.subscription,
|
|
284
|
-
this.t.segmentStart,
|
|
285
|
-
this.t.segmentMember,
|
|
286
|
-
this.t.suppression,
|
|
287
|
-
this.t.device,
|
|
288
|
-
this.t.event,
|
|
289
|
-
this.t.profile
|
|
290
|
-
]) await tx.query(`DELETE FROM ${table} WHERE user_id = $1`, [userId]);
|
|
457
|
+
await this.deleteSubjectRows(tx, userId);
|
|
291
458
|
await tx.query(`UPDATE ${this.t.erasure} SET erased_at = COALESCE(erased_at, ${TS("$2")}) WHERE user_id = $1`, [userId, at]);
|
|
292
459
|
});
|
|
293
460
|
}
|
|
461
|
+
/** Everything kept about a user or visitor, on a transaction holding their lock. */
|
|
462
|
+
async deleteSubjectRows(tx, subject) {
|
|
463
|
+
for (const child of [
|
|
464
|
+
this.t.messageOpen,
|
|
465
|
+
this.t.messageClick,
|
|
466
|
+
this.t.messageLink
|
|
467
|
+
]) await tx.query(`DELETE FROM ${child} WHERE message_id IN (SELECT id FROM ${this.t.message} WHERE user_id = $1)`, [subject]);
|
|
468
|
+
for (const table of [
|
|
469
|
+
this.t.message,
|
|
470
|
+
this.t.instance,
|
|
471
|
+
this.t.entry,
|
|
472
|
+
this.t.execution,
|
|
473
|
+
this.t.subscription,
|
|
474
|
+
this.t.segmentStart,
|
|
475
|
+
this.t.segmentMember,
|
|
476
|
+
this.t.suppression,
|
|
477
|
+
this.t.device,
|
|
478
|
+
this.t.event,
|
|
479
|
+
this.t.profile
|
|
480
|
+
]) await tx.query(`DELETE FROM ${table} WHERE user_id = $1`, [subject]);
|
|
481
|
+
const visitorId = require_store_identity.visitorIdOf(subject);
|
|
482
|
+
if (visitorId !== void 0) await tx.query(`DELETE FROM ${this.t.device} WHERE visitor_id = $1 AND user_id IS NULL`, [visitorId]);
|
|
483
|
+
}
|
|
294
484
|
async isSuppressed(userId, channel, topic) {
|
|
295
485
|
return (await this.sql.query(`SELECT 1 AS x FROM ${this.t.suppression}
|
|
296
486
|
WHERE user_id = $1 AND channel = $2 AND (topic IS NULL OR topic = $3::text)`, [
|
|
@@ -323,12 +513,14 @@ var PostgresJourneyStore = class {
|
|
|
323
513
|
});
|
|
324
514
|
}
|
|
325
515
|
async removeDevice(token) {
|
|
326
|
-
|
|
516
|
+
const rows = await this.sql.query(`DELETE FROM ${this.t.device} WHERE token = $1 RETURNING user_id, visitor_id`, [token]);
|
|
517
|
+
return rows[0] === void 0 ? void 0 : deviceOwner(rows[0]);
|
|
327
518
|
}
|
|
328
519
|
async unbindDevice(token) {
|
|
329
|
-
|
|
330
|
-
UPDATE ${this.t.device} d SET user_id = NULL FROM old WHERE d.token = old.token
|
|
331
|
-
RETURNING old.user_id`, [token])
|
|
520
|
+
const rows = await this.sql.query(`WITH old AS (SELECT token, user_id, visitor_id FROM ${this.t.device} WHERE token = $1 FOR UPDATE)
|
|
521
|
+
UPDATE ${this.t.device} d SET user_id = NULL, visitor_id = NULL, updated_at = ${TS("$2")} FROM old WHERE d.token = old.token
|
|
522
|
+
RETURNING old.user_id, old.visitor_id`, [token, Date.now()]);
|
|
523
|
+
return rows[0] === void 0 ? void 0 : deviceOwner(rows[0]);
|
|
332
524
|
}
|
|
333
525
|
async listSegments() {
|
|
334
526
|
return (await this.sql.query(`SELECT id, condition FROM ${this.t.segment} ORDER BY id`)).map((row) => ({
|
|
@@ -341,11 +533,12 @@ var PostgresJourneyStore = class {
|
|
|
341
533
|
return row === void 0 ? void 0 : require_ir.ConditionIR.parse(asJson(row.condition));
|
|
342
534
|
}
|
|
343
535
|
async findTriggers(event) {
|
|
344
|
-
return (await this.sql.query(`SELECT workflow, hash, "where" AS where_clause, filter, version FROM ${this.t.trigger} WHERE event = $1`, [event])).map((row) => ({
|
|
536
|
+
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) => ({
|
|
345
537
|
workflow: row.workflow,
|
|
346
538
|
hash: row.hash,
|
|
347
539
|
where: row.where_clause === null ? null : require_ir.ConditionIR.parse(asJson(row.where_clause)),
|
|
348
540
|
filter: row.filter === null ? null : require_ir.ConditionIR.parse(asJson(row.filter)),
|
|
541
|
+
visitors: row.visitors === true,
|
|
349
542
|
version: row.version ?? void 0
|
|
350
543
|
}));
|
|
351
544
|
}
|
|
@@ -412,12 +605,13 @@ var PostgresJourneyStore = class {
|
|
|
412
605
|
JSON.stringify(segment.condition),
|
|
413
606
|
segment.contentHash
|
|
414
607
|
]);
|
|
415
|
-
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)`, [
|
|
608
|
+
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)`, [
|
|
416
609
|
workflow.id,
|
|
417
610
|
workflow.contentHash,
|
|
418
611
|
workflow.trigger.event,
|
|
419
612
|
workflow.trigger.where === void 0 ? null : JSON.stringify(workflow.trigger.where),
|
|
420
613
|
workflow.trigger.filter === void 0 ? null : JSON.stringify(workflow.trigger.filter),
|
|
614
|
+
workflow.trigger.visitors === true,
|
|
421
615
|
version ?? null
|
|
422
616
|
]);
|
|
423
617
|
else if (workflow.trigger.type === "segment") await tx.query(`INSERT INTO ${this.t.segmentTrigger} (workflow, hash, segment, version) VALUES ($1, $2, $3, $4)`, [
|