@shware/workflow 6.1.1 → 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 +132 -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 -2
- package/dist/aws/index.cjs.map +1 -1
- package/dist/aws/index.mjs +3 -2
- 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 +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 +13 -2
- 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 +13 -2
- 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/email-sender.cjs +8 -1
- package/dist/email-sender.cjs.map +1 -1
- package/dist/email-sender.d.cts +2 -2
- package/dist/email-sender.d.cts.map +1 -1
- package/dist/email-sender.d.mts +2 -2
- package/dist/email-sender.d.mts.map +1 -1
- package/dist/email-sender.mjs +9 -2
- package/dist/email-sender.mjs.map +1 -1
- package/dist/email.cjs +5 -0
- package/dist/email.cjs.map +1 -1
- package/dist/email.d.cts +2 -0
- package/dist/email.d.cts.map +1 -1
- package/dist/email.d.mts +2 -0
- package/dist/email.d.mts.map +1 -1
- package/dist/email.mjs +5 -1
- package/dist/email.mjs.map +1 -1
- package/dist/engine/condition.cjs +7 -5
- package/dist/engine/condition.cjs.map +1 -1
- package/dist/engine/condition.d.cts +3 -3
- package/dist/engine/condition.d.cts.map +1 -1
- package/dist/engine/condition.d.mts +3 -3
- package/dist/engine/condition.d.mts.map +1 -1
- package/dist/engine/condition.mjs +7 -5
- package/dist/engine/condition.mjs.map +1 -1
- package/dist/engine/interpreter.cjs +11 -6
- 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 +11 -6
- package/dist/engine/interpreter.mjs.map +1 -1
- package/dist/engine/ports.d.cts +21 -3
- package/dist/engine/ports.d.cts.map +1 -1
- package/dist/engine/ports.d.mts +21 -3
- 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 +31 -90
- package/dist/host/http.cjs.map +1 -1
- package/dist/host/http.d.cts +4 -2
- package/dist/host/http.d.cts.map +1 -1
- package/dist/host/http.d.mts +4 -2
- package/dist/host/http.d.mts.map +1 -1
- package/dist/host/http.mjs +25 -84
- package/dist/host/http.mjs.map +1 -1
- package/dist/host/ingest.cjs +164 -25
- package/dist/host/ingest.cjs.map +1 -1
- package/dist/host/ingest.d.cts +38 -8
- package/dist/host/ingest.d.cts.map +1 -1
- package/dist/host/ingest.d.mts +38 -8
- package/dist/host/ingest.d.mts.map +1 -1
- package/dist/host/ingest.mjs +161 -26
- package/dist/host/ingest.mjs.map +1 -1
- package/dist/host/journey-host.cjs +8 -1
- package/dist/host/journey-host.cjs.map +1 -1
- package/dist/host/journey-host.d.cts +10 -4
- package/dist/host/journey-host.d.cts.map +1 -1
- package/dist/host/journey-host.d.mts +10 -4
- package/dist/host/journey-host.d.mts.map +1 -1
- package/dist/host/journey-host.mjs +8 -1
- 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/index.cjs +1 -0
- package/dist/index.d.cts +2 -2
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +2 -2
- 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/reporting/observer.cjs +1 -0
- package/dist/reporting/observer.cjs.map +1 -1
- package/dist/reporting/observer.mjs +1 -0
- package/dist/reporting/observer.mjs.map +1 -1
- package/dist/store/facts.cjs +29 -9
- package/dist/store/facts.cjs.map +1 -1
- package/dist/store/facts.d.cts +7 -2
- package/dist/store/facts.d.cts.map +1 -1
- package/dist/store/facts.d.mts +7 -2
- package/dist/store/facts.d.mts.map +1 -1
- package/dist/store/facts.mjs +29 -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 +119 -25
- package/dist/store/index.d.cts.map +1 -1
- package/dist/store/index.d.mts +119 -25
- 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-reporting.cjs +4 -0
- package/dist/store/postgres-reporting.cjs.map +1 -1
- package/dist/store/postgres-reporting.d.cts +1 -0
- package/dist/store/postgres-reporting.d.cts.map +1 -1
- package/dist/store/postgres-reporting.d.mts +1 -0
- package/dist/store/postgres-reporting.d.mts.map +1 -1
- package/dist/store/postgres-reporting.mjs +4 -0
- package/dist/store/postgres-reporting.mjs.map +1 -1
- package/dist/store/postgres.cjs +271 -55
- package/dist/store/postgres.cjs.map +1 -1
- package/dist/store/postgres.d.cts +29 -7
- package/dist/store/postgres.d.cts.map +1 -1
- package/dist/store/postgres.d.mts +29 -7
- package/dist/store/postgres.d.mts.map +1 -1
- package/dist/store/postgres.mjs +272 -56
- package/dist/store/postgres.mjs.map +1 -1
- package/dist/store/tables.cjs +28 -9
- 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 +28 -9
- 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,17 @@
|
|
|
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");
|
|
4
5
|
const require_store_sql = require("./sql.cjs");
|
|
5
6
|
const require_store_user_lock = require("./user-lock.cjs");
|
|
6
7
|
const require_store_postgres_reporting = require("./postgres-reporting.cjs");
|
|
7
8
|
const require_store_tables = require("./tables.cjs");
|
|
8
9
|
// src/store/postgres.ts
|
|
10
|
+
/** Who a device row belongs to: its user, or before sign-in its visitor. */
|
|
11
|
+
function deviceOwner(row) {
|
|
12
|
+
if (row.user_id !== null) return row.user_id;
|
|
13
|
+
return row.visitor_id === null ? void 0 : require_store_identity.visitorSubject(row.visitor_id);
|
|
14
|
+
}
|
|
9
15
|
/** information_schema's udt_name per column type. */
|
|
10
16
|
const UDT = {
|
|
11
17
|
uuid: "uuid",
|
|
@@ -155,29 +161,55 @@ var PostgresJourneyStore = class {
|
|
|
155
161
|
params
|
|
156
162
|
};
|
|
157
163
|
}
|
|
158
|
-
async countEvents(userId, event, window) {
|
|
164
|
+
async countEvents(userId, event, window, atLeast) {
|
|
159
165
|
const { where, params } = this.eventWindow(userId, event, window);
|
|
160
|
-
const
|
|
166
|
+
const capped = atLeast !== void 0 && Number.isInteger(atLeast) && atLeast > 0;
|
|
167
|
+
if (capped) params.push(atLeast);
|
|
168
|
+
const rows = await this.sql.query(capped ? `SELECT count(*)::int AS c FROM (SELECT 1 FROM ${this.t.event} WHERE ${where} LIMIT $${params.length}) capped` : `SELECT count(*)::int AS c FROM ${this.t.event} WHERE ${where}`, params);
|
|
161
169
|
return Number(rows.at(0)?.c ?? 0);
|
|
162
170
|
}
|
|
163
|
-
async
|
|
171
|
+
async pageEventPayloads(userId, event, window, page) {
|
|
164
172
|
const { where, params } = this.eventWindow(userId, event, window);
|
|
165
|
-
|
|
173
|
+
let keyset = "";
|
|
174
|
+
if (page.after !== void 0) {
|
|
175
|
+
params.push(page.after.ts, page.after.id);
|
|
176
|
+
keyset = ` AND (ts, id) > ($${params.length - 1}::timestamptz, $${params.length}::uuid)`;
|
|
177
|
+
}
|
|
178
|
+
params.push(page.limit);
|
|
179
|
+
const rows = await this.sql.query(`SELECT payload, ts::text AS ts, id::text AS id FROM ${this.t.event}
|
|
180
|
+
WHERE ${where}${keyset} ORDER BY ts, id LIMIT $${params.length}`, params);
|
|
181
|
+
const last = rows.at(-1);
|
|
182
|
+
return {
|
|
183
|
+
payloads: rows.map((row) => asJson(row.payload)),
|
|
184
|
+
...last !== void 0 && rows.length === page.limit ? { next: {
|
|
185
|
+
ts: last.ts,
|
|
186
|
+
id: last.id
|
|
187
|
+
} } : {}
|
|
188
|
+
};
|
|
166
189
|
}
|
|
167
190
|
async getProfile(userId) {
|
|
168
191
|
const row = (await this.sql.query(`SELECT props FROM ${this.t.profile} WHERE user_id = $1`, [userId])).at(0);
|
|
169
192
|
return row === void 0 ? void 0 : asJson(row.props);
|
|
170
193
|
}
|
|
171
|
-
async mergeProfile(userId, props) {
|
|
194
|
+
async mergeProfile(userId, props, at = Date.now()) {
|
|
172
195
|
return this.sql.transaction(async (tx) => {
|
|
173
196
|
await this.lockUser(tx, userId);
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
|
|
197
|
+
const times = asJson((await tx.query(`SELECT props_at FROM ${this.t.profile} WHERE user_id = $1`, [userId]))[0]?.props_at ?? null) ?? {};
|
|
198
|
+
const patch = Object.fromEntries(Object.entries(props).filter(([key]) => (times[key] ?? Number.NEGATIVE_INFINITY) <= at));
|
|
199
|
+
const stamped = {
|
|
200
|
+
...times,
|
|
201
|
+
...Object.fromEntries(Object.keys(patch).map((k) => [k, at]))
|
|
202
|
+
};
|
|
203
|
+
const rows = await tx.query(`INSERT INTO ${this.t.profile} (user_id, props, props_at)
|
|
204
|
+
SELECT $1, jsonb_strip_nulls($2::text::jsonb), $4::text::jsonb ${this.notErased("$1")}
|
|
205
|
+
ON CONFLICT (user_id) DO UPDATE SET
|
|
206
|
+
props = jsonb_strip_nulls(${this.t.profile}.props || $3::text::jsonb),
|
|
207
|
+
props_at = $4::text::jsonb
|
|
177
208
|
RETURNING props`, [
|
|
178
209
|
userId,
|
|
179
|
-
JSON.stringify(
|
|
180
|
-
JSON.stringify(
|
|
210
|
+
JSON.stringify(patch),
|
|
211
|
+
JSON.stringify(patch),
|
|
212
|
+
JSON.stringify(stamped)
|
|
181
213
|
]);
|
|
182
214
|
if (rows.length === 0) throw new require_store_erasure.UserErasedError(userId);
|
|
183
215
|
return asJson(rows[0].props);
|
|
@@ -185,10 +217,10 @@ var PostgresJourneyStore = class {
|
|
|
185
217
|
}
|
|
186
218
|
async listUserIds(page) {
|
|
187
219
|
const params = [];
|
|
188
|
-
let where = ""
|
|
220
|
+
let where = ` WHERE user_id NOT LIKE '${require_store_identity.visitorSubject("")}%'`;
|
|
189
221
|
if (page?.after !== void 0) {
|
|
190
222
|
params.push(page.after);
|
|
191
|
-
where
|
|
223
|
+
where += ` AND user_id > $${params.length}`;
|
|
192
224
|
}
|
|
193
225
|
let limit = "";
|
|
194
226
|
if (page?.limit !== void 0) {
|
|
@@ -197,23 +229,160 @@ var PostgresJourneyStore = class {
|
|
|
197
229
|
}
|
|
198
230
|
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);
|
|
199
231
|
}
|
|
200
|
-
listDevices(
|
|
201
|
-
|
|
232
|
+
listDevices(subject) {
|
|
233
|
+
const visitorId = require_store_identity.visitorIdOf(subject);
|
|
234
|
+
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]);
|
|
202
235
|
}
|
|
203
236
|
async upsertDevice(device) {
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
237
|
+
const { deviceId = null, userId, visitorId = null, token, platform, at } = device;
|
|
238
|
+
const subject = userId ?? require_store_identity.visitorSubject(visitorId ?? "");
|
|
239
|
+
return this.sql.transaction(async (tx) => {
|
|
240
|
+
await this.lockUser(tx, subject);
|
|
241
|
+
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]);
|
|
242
|
+
for (const row of previous) {
|
|
243
|
+
if (row.token === token) continue;
|
|
244
|
+
const owner = deviceOwner(row);
|
|
245
|
+
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]);
|
|
246
|
+
}
|
|
247
|
+
if ((await tx.query(`INSERT INTO ${this.t.device} (device_id, user_id, visitor_id, token, platform, created_at, updated_at)
|
|
248
|
+
SELECT $1, $2, $3, $4, $5, ${TS("$6")}, ${TS("$6")} ${this.notErased("$7")}
|
|
249
|
+
ON CONFLICT (token) DO UPDATE SET
|
|
250
|
+
device_id = COALESCE(EXCLUDED.device_id, ${this.t.device}.device_id),
|
|
251
|
+
user_id = EXCLUDED.user_id,
|
|
252
|
+
visitor_id = COALESCE(EXCLUDED.visitor_id, ${this.t.device}.visitor_id),
|
|
253
|
+
platform = EXCLUDED.platform,
|
|
254
|
+
updated_at = EXCLUDED.updated_at
|
|
209
255
|
RETURNING token`, [
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
256
|
+
deviceId,
|
|
257
|
+
userId,
|
|
258
|
+
visitorId,
|
|
259
|
+
token,
|
|
260
|
+
platform,
|
|
261
|
+
at,
|
|
262
|
+
subject
|
|
263
|
+
])).length === 0) throw new require_store_erasure.UserErasedError(subject);
|
|
264
|
+
const owners = previous.flatMap((row) => {
|
|
265
|
+
const owner = deviceOwner(row);
|
|
266
|
+
return owner === void 0 || owner === subject ? [] : [owner];
|
|
267
|
+
});
|
|
268
|
+
return [...new Set(owners)];
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
async linkedUser(visitorId) {
|
|
272
|
+
return (await this.sql.query(`SELECT user_id FROM ${this.t.visitor} WHERE visitor_id = $1`, [visitorId]))[0]?.user_id ?? void 0;
|
|
273
|
+
}
|
|
274
|
+
async touchVisitor(visitorId, at) {
|
|
275
|
+
return (await this.sql.query(`INSERT INTO ${this.t.visitor} (visitor_id, user_id, created_at, updated_at)
|
|
276
|
+
VALUES ($1, NULL, ${TS("$2")}, ${TS("$2")})
|
|
277
|
+
ON CONFLICT (visitor_id) DO UPDATE SET updated_at = EXCLUDED.updated_at
|
|
278
|
+
RETURNING user_id`, [visitorId, at]))[0]?.user_id ?? require_store_identity.visitorSubject(visitorId);
|
|
279
|
+
}
|
|
280
|
+
async pruneVisitors(before, limit) {
|
|
281
|
+
const idle = `user_id IS NULL AND updated_at < ${TS("$1")} AND NOT EXISTS (
|
|
282
|
+
SELECT 1 FROM ${this.t.entry} e
|
|
283
|
+
WHERE e.user_id = '${require_store_identity.visitorSubject("")}' || visitor_id AND e.status = 'running')`;
|
|
284
|
+
const candidates = await this.sql.query(`SELECT visitor_id FROM ${this.t.visitor} WHERE ${idle} ORDER BY updated_at LIMIT $2`, [before, limit]);
|
|
285
|
+
let pruned = 0;
|
|
286
|
+
for (const { visitor_id: visitorId } of candidates) {
|
|
287
|
+
const subject = require_store_identity.visitorSubject(visitorId);
|
|
288
|
+
await this.sql.transaction(async (tx) => {
|
|
289
|
+
await this.lockUser(tx, subject);
|
|
290
|
+
if ((await tx.query(`DELETE FROM ${this.t.visitor} WHERE visitor_id = $2 AND ${idle} RETURNING visitor_id`, [before, visitorId])).length === 0) return;
|
|
291
|
+
await this.deleteSubjectRows(tx, subject);
|
|
292
|
+
pruned += 1;
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
return pruned;
|
|
296
|
+
}
|
|
297
|
+
async entryOwner(instanceId) {
|
|
298
|
+
return (await this.sql.query(`SELECT user_id FROM ${this.t.entry} WHERE instance_id = $1`, [instanceId]))[0]?.user_id;
|
|
299
|
+
}
|
|
300
|
+
async linkVisitor(visitorId, userId, at) {
|
|
301
|
+
const subject = require_store_identity.visitorSubject(visitorId);
|
|
302
|
+
if (await this.linkedUser(visitorId) !== void 0) return null;
|
|
303
|
+
return this.sql.transaction(async (tx) => {
|
|
304
|
+
await require_store_user_lock.beginReadCommitted(tx);
|
|
305
|
+
for (const key of [subject, userId].sort()) await require_store_user_lock.lockUser(tx, this.schema, key);
|
|
306
|
+
if ((await tx.query(`INSERT INTO ${this.t.visitor} (visitor_id, user_id, created_at, updated_at)
|
|
307
|
+
SELECT $1, $2, ${TS("$3")}, ${TS("$3")} ${this.notErased("$2")}
|
|
308
|
+
ON CONFLICT (visitor_id) DO UPDATE SET user_id = EXCLUDED.user_id, updated_at = EXCLUDED.updated_at
|
|
309
|
+
WHERE ${this.t.visitor}.user_id IS NULL
|
|
310
|
+
RETURNING visitor_id`, [
|
|
311
|
+
visitorId,
|
|
312
|
+
userId,
|
|
313
|
+
at
|
|
314
|
+
])).length === 0) {
|
|
315
|
+
if ((await tx.query(`SELECT 1 FROM ${this.t.erasure} WHERE user_id = $1`, [userId])).length > 0) throw new require_store_erasure.UserErasedError(userId);
|
|
316
|
+
return null;
|
|
317
|
+
}
|
|
318
|
+
const droppedIds = (await tx.query(`DELETE FROM ${this.t.entry} v USING ${this.t.entry} u
|
|
319
|
+
WHERE v.user_id = $1 AND u.user_id = $2 AND u.workflow = v.workflow
|
|
320
|
+
RETURNING v.instance_id`, [subject, userId])).map((row) => row.instance_id);
|
|
321
|
+
await tx.query(`DELETE FROM ${this.t.subscription} WHERE wake_handle = ANY($1::text[])`, [droppedIds]);
|
|
322
|
+
await tx.query(`DELETE FROM ${this.t.execution} WHERE instance_id = ANY($1::text[])`, [droppedIds]);
|
|
323
|
+
await tx.query(`DELETE FROM ${this.t.event} v USING ${this.t.event} u
|
|
324
|
+
WHERE v.user_id = $1 AND u.user_id = $2 AND u.event_id = v.event_id`, [subject, userId]);
|
|
325
|
+
const events = await tx.query(`SELECT DISTINCT name FROM ${this.t.event} WHERE user_id = $1`, [subject]);
|
|
326
|
+
await tx.query(`INSERT INTO ${this.t.suppression} (user_id, channel, topic, ts)
|
|
327
|
+
SELECT $2, channel, topic, ts FROM ${this.t.suppression} WHERE user_id = $1
|
|
328
|
+
ON CONFLICT (user_id, channel, topic) DO NOTHING`, [subject, userId]);
|
|
329
|
+
await tx.query(`DELETE FROM ${this.t.suppression} WHERE user_id = $1`, [subject]);
|
|
330
|
+
const props = await this.mergeProfileInto(tx, subject, userId);
|
|
331
|
+
for (const table of [
|
|
332
|
+
this.t.event,
|
|
333
|
+
this.t.entry,
|
|
334
|
+
this.t.execution,
|
|
335
|
+
this.t.subscription,
|
|
336
|
+
this.t.instance,
|
|
337
|
+
this.t.message
|
|
338
|
+
]) await tx.query(`UPDATE ${table} SET user_id = $2 WHERE user_id = $1`, [subject, userId]);
|
|
339
|
+
await tx.query(`UPDATE ${this.t.device} SET user_id = $2, updated_at = ${TS("$3")}
|
|
340
|
+
WHERE visitor_id = $1 AND user_id IS NULL`, [
|
|
341
|
+
visitorId,
|
|
342
|
+
userId,
|
|
343
|
+
at
|
|
344
|
+
]);
|
|
345
|
+
return {
|
|
346
|
+
dropped: droppedIds,
|
|
347
|
+
events: events.map((row) => row.name),
|
|
348
|
+
props
|
|
349
|
+
};
|
|
215
350
|
});
|
|
216
351
|
}
|
|
352
|
+
/**
|
|
353
|
+
* The visitor's properties under the user's, a key the user has a time for going to the later
|
|
354
|
+
* write, one a row older than the times going to the user if they hold it. The visitor's row
|
|
355
|
+
* goes. Answers the keys that moved.
|
|
356
|
+
*/
|
|
357
|
+
async mergeProfileInto(tx, subject, userId) {
|
|
358
|
+
const rows = await tx.query(`SELECT user_id, props, props_at FROM ${this.t.profile} WHERE user_id = ANY($1::text[])`, [[subject, userId]]);
|
|
359
|
+
const from = rows.find((row) => row.user_id === subject);
|
|
360
|
+
if (from === void 0) return [];
|
|
361
|
+
const to = rows.find((row) => row.user_id === userId);
|
|
362
|
+
const fromTimes = asJson(from.props_at ?? null) ?? {};
|
|
363
|
+
const toTimes = asJson(to?.props_at ?? null) ?? {};
|
|
364
|
+
const toProps = to === void 0 ? {} : asJson(to.props);
|
|
365
|
+
const moved = Object.entries(asJson(from.props)).filter(([key]) => {
|
|
366
|
+
const written = toTimes[key];
|
|
367
|
+
return written === void 0 ? !Object.hasOwn(toProps, key) : (fromTimes[key] ?? 0) > written;
|
|
368
|
+
});
|
|
369
|
+
const props = {
|
|
370
|
+
...toProps,
|
|
371
|
+
...Object.fromEntries(moved)
|
|
372
|
+
};
|
|
373
|
+
const times = {
|
|
374
|
+
...toTimes,
|
|
375
|
+
...Object.fromEntries(moved.map(([key]) => [key, fromTimes[key] ?? 0]))
|
|
376
|
+
};
|
|
377
|
+
await tx.query(`INSERT INTO ${this.t.profile} (user_id, props, props_at) VALUES ($1, $2::text::jsonb, $3::text::jsonb)
|
|
378
|
+
ON CONFLICT (user_id) DO UPDATE SET props = EXCLUDED.props, props_at = EXCLUDED.props_at`, [
|
|
379
|
+
userId,
|
|
380
|
+
JSON.stringify(props),
|
|
381
|
+
JSON.stringify(times)
|
|
382
|
+
]);
|
|
383
|
+
await tx.query(`DELETE FROM ${this.t.profile} WHERE user_id = $1`, [subject]);
|
|
384
|
+
return moved.map(([key]) => key);
|
|
385
|
+
}
|
|
217
386
|
async requestErasure(userId, at) {
|
|
218
387
|
await this.sql.transaction(async (tx) => {
|
|
219
388
|
await this.lockUser(tx, userId);
|
|
@@ -253,27 +422,33 @@ var PostgresJourneyStore = class {
|
|
|
253
422
|
async forgetUser(userId, at) {
|
|
254
423
|
await this.sql.transaction(async (tx) => {
|
|
255
424
|
await this.lockUser(tx, userId);
|
|
256
|
-
|
|
257
|
-
this.t.messageOpen,
|
|
258
|
-
this.t.messageClick,
|
|
259
|
-
this.t.messageLink
|
|
260
|
-
]) await tx.query(`DELETE FROM ${child} WHERE message_id IN (SELECT id FROM ${this.t.message} WHERE user_id = $1)`, [userId]);
|
|
261
|
-
for (const table of [
|
|
262
|
-
this.t.message,
|
|
263
|
-
this.t.instance,
|
|
264
|
-
this.t.entry,
|
|
265
|
-
this.t.execution,
|
|
266
|
-
this.t.subscription,
|
|
267
|
-
this.t.segmentStart,
|
|
268
|
-
this.t.segmentMember,
|
|
269
|
-
this.t.suppression,
|
|
270
|
-
this.t.device,
|
|
271
|
-
this.t.event,
|
|
272
|
-
this.t.profile
|
|
273
|
-
]) await tx.query(`DELETE FROM ${table} WHERE user_id = $1`, [userId]);
|
|
425
|
+
await this.deleteSubjectRows(tx, userId);
|
|
274
426
|
await tx.query(`UPDATE ${this.t.erasure} SET erased_at = COALESCE(erased_at, ${TS("$2")}) WHERE user_id = $1`, [userId, at]);
|
|
275
427
|
});
|
|
276
428
|
}
|
|
429
|
+
/** Everything kept about a user or visitor, on a transaction holding their lock. */
|
|
430
|
+
async deleteSubjectRows(tx, subject) {
|
|
431
|
+
for (const child of [
|
|
432
|
+
this.t.messageOpen,
|
|
433
|
+
this.t.messageClick,
|
|
434
|
+
this.t.messageLink
|
|
435
|
+
]) await tx.query(`DELETE FROM ${child} WHERE message_id IN (SELECT id FROM ${this.t.message} WHERE user_id = $1)`, [subject]);
|
|
436
|
+
for (const table of [
|
|
437
|
+
this.t.message,
|
|
438
|
+
this.t.instance,
|
|
439
|
+
this.t.entry,
|
|
440
|
+
this.t.execution,
|
|
441
|
+
this.t.subscription,
|
|
442
|
+
this.t.segmentStart,
|
|
443
|
+
this.t.segmentMember,
|
|
444
|
+
this.t.suppression,
|
|
445
|
+
this.t.device,
|
|
446
|
+
this.t.event,
|
|
447
|
+
this.t.profile
|
|
448
|
+
]) await tx.query(`DELETE FROM ${table} WHERE user_id = $1`, [subject]);
|
|
449
|
+
const visitorId = require_store_identity.visitorIdOf(subject);
|
|
450
|
+
if (visitorId !== void 0) await tx.query(`DELETE FROM ${this.t.device} WHERE visitor_id = $1 AND user_id IS NULL`, [visitorId]);
|
|
451
|
+
}
|
|
277
452
|
async isSuppressed(userId, channel, topic) {
|
|
278
453
|
return (await this.sql.query(`SELECT 1 AS x FROM ${this.t.suppression}
|
|
279
454
|
WHERE user_id = $1 AND channel = $2 AND (topic IS NULL OR topic = $3::text)`, [
|
|
@@ -306,12 +481,14 @@ var PostgresJourneyStore = class {
|
|
|
306
481
|
});
|
|
307
482
|
}
|
|
308
483
|
async removeDevice(token) {
|
|
309
|
-
|
|
484
|
+
const rows = await this.sql.query(`DELETE FROM ${this.t.device} WHERE token = $1 RETURNING user_id, visitor_id`, [token]);
|
|
485
|
+
return rows[0] === void 0 ? void 0 : deviceOwner(rows[0]);
|
|
310
486
|
}
|
|
311
487
|
async unbindDevice(token) {
|
|
312
|
-
|
|
313
|
-
UPDATE ${this.t.device} d SET user_id = NULL FROM old WHERE d.token = old.token
|
|
314
|
-
RETURNING old.user_id`, [token])
|
|
488
|
+
const rows = await this.sql.query(`WITH old AS (SELECT token, user_id, visitor_id FROM ${this.t.device} WHERE token = $1 FOR UPDATE)
|
|
489
|
+
UPDATE ${this.t.device} d SET user_id = NULL, visitor_id = NULL, updated_at = ${TS("$2")} FROM old WHERE d.token = old.token
|
|
490
|
+
RETURNING old.user_id, old.visitor_id`, [token, Date.now()]);
|
|
491
|
+
return rows[0] === void 0 ? void 0 : deviceOwner(rows[0]);
|
|
315
492
|
}
|
|
316
493
|
async listSegments() {
|
|
317
494
|
return (await this.sql.query(`SELECT id, condition FROM ${this.t.segment} ORDER BY id`)).map((row) => ({
|
|
@@ -324,11 +501,12 @@ var PostgresJourneyStore = class {
|
|
|
324
501
|
return row === void 0 ? void 0 : require_ir.ConditionIR.parse(asJson(row.condition));
|
|
325
502
|
}
|
|
326
503
|
async findTriggers(event) {
|
|
327
|
-
return (await this.sql.query(`SELECT workflow, hash, "where" AS where_clause, filter, version FROM ${this.t.trigger} WHERE event = $1`, [event])).map((row) => ({
|
|
504
|
+
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) => ({
|
|
328
505
|
workflow: row.workflow,
|
|
329
506
|
hash: row.hash,
|
|
330
507
|
where: row.where_clause === null ? null : require_ir.ConditionIR.parse(asJson(row.where_clause)),
|
|
331
508
|
filter: row.filter === null ? null : require_ir.ConditionIR.parse(asJson(row.filter)),
|
|
509
|
+
visitors: row.visitors === true,
|
|
332
510
|
version: row.version ?? void 0
|
|
333
511
|
}));
|
|
334
512
|
}
|
|
@@ -348,8 +526,18 @@ var PostgresJourneyStore = class {
|
|
|
348
526
|
const row = (await this.sql.query(`SELECT ir FROM ${this.t.ir} WHERE content_hash = $1`, [contentHash])).at(0);
|
|
349
527
|
return row === void 0 ? void 0 : require_ir.WorkflowIR.parse(asJson(row.ir));
|
|
350
528
|
}
|
|
351
|
-
replaceRoutes(bundle) {
|
|
352
|
-
return this.sql.transaction((tx) =>
|
|
529
|
+
replaceRoutes(bundle, version) {
|
|
530
|
+
return this.sql.transaction(async (tx) => {
|
|
531
|
+
if (version !== void 0) await tx.query(`INSERT INTO ${this.t.deployment} (scope, version_id, version_created_at, applied_at)
|
|
532
|
+
VALUES ('routes', $1, ${TS("$2")}, ${TS("$3")})
|
|
533
|
+
ON CONFLICT (scope) DO UPDATE SET version_id = EXCLUDED.version_id,
|
|
534
|
+
version_created_at = EXCLUDED.version_created_at, applied_at = EXCLUDED.applied_at`, [
|
|
535
|
+
version.id,
|
|
536
|
+
version.createdAt,
|
|
537
|
+
Date.now()
|
|
538
|
+
]);
|
|
539
|
+
await this.swapRoutes(tx, bundle, version?.id);
|
|
540
|
+
});
|
|
353
541
|
}
|
|
354
542
|
async liveBundleVersion() {
|
|
355
543
|
const row = (await this.sql.transaction((tx) => tx.query(`SELECT version_id, (extract(epoch FROM version_created_at) * 1000)::double precision AS created_at
|
|
@@ -385,12 +573,13 @@ var PostgresJourneyStore = class {
|
|
|
385
573
|
JSON.stringify(segment.condition),
|
|
386
574
|
segment.contentHash
|
|
387
575
|
]);
|
|
388
|
-
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)`, [
|
|
576
|
+
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)`, [
|
|
389
577
|
workflow.id,
|
|
390
578
|
workflow.contentHash,
|
|
391
579
|
workflow.trigger.event,
|
|
392
580
|
workflow.trigger.where === void 0 ? null : JSON.stringify(workflow.trigger.where),
|
|
393
581
|
workflow.trigger.filter === void 0 ? null : JSON.stringify(workflow.trigger.filter),
|
|
582
|
+
workflow.trigger.visitors === true,
|
|
394
583
|
version ?? null
|
|
395
584
|
]);
|
|
396
585
|
else if (workflow.trigger.type === "segment") await tx.query(`INSERT INTO ${this.t.segmentTrigger} (workflow, hash, segment, version) VALUES ($1, $2, $3, $4)`, [
|
|
@@ -449,17 +638,19 @@ var PostgresJourneyStore = class {
|
|
|
449
638
|
});
|
|
450
639
|
}
|
|
451
640
|
async claimEntry(sql, entry) {
|
|
452
|
-
|
|
453
|
-
|
|
641
|
+
const claimedAt = Date.now();
|
|
642
|
+
if ((await sql.query(`INSERT INTO ${this.t.entry} (workflow, user_id, instance_id, hash, status, ts, claimed_at)
|
|
643
|
+
SELECT $1, $2, $3, $4, 'running', ${TS("$5")}, ${TS("$6")} ${this.notErased("$2")}
|
|
454
644
|
ON CONFLICT (workflow, user_id) DO NOTHING RETURNING instance_id`, [
|
|
455
645
|
entry.workflow,
|
|
456
646
|
entry.userId,
|
|
457
647
|
entry.instanceId,
|
|
458
648
|
entry.hash,
|
|
459
|
-
entry.ts
|
|
649
|
+
entry.ts,
|
|
650
|
+
claimedAt
|
|
460
651
|
])).length > 0) return "entered";
|
|
461
652
|
if ((await sql.query(`UPDATE ${this.t.entry} SET instance_id = $1, hash = $2, status = 'running', ts = ${TS("$3")},
|
|
462
|
-
admitted_at = NULL
|
|
653
|
+
admitted_at = NULL, claimed_at = ${TS("$6")}
|
|
463
654
|
WHERE workflow = $4 AND user_id = $5 AND status = 'failed'
|
|
464
655
|
AND NOT EXISTS (SELECT 1 FROM ${this.t.erasure} WHERE user_id = $5)
|
|
465
656
|
RETURNING instance_id`, [
|
|
@@ -467,7 +658,8 @@ var PostgresJourneyStore = class {
|
|
|
467
658
|
entry.hash,
|
|
468
659
|
entry.ts,
|
|
469
660
|
entry.workflow,
|
|
470
|
-
entry.userId
|
|
661
|
+
entry.userId,
|
|
662
|
+
claimedAt
|
|
471
663
|
])).length > 0) return "reclaimed";
|
|
472
664
|
await this.refuseIfErased(sql, entry.userId);
|
|
473
665
|
return null;
|
|
@@ -498,6 +690,30 @@ var PostgresJourneyStore = class {
|
|
|
498
690
|
async removeEntry(instanceId) {
|
|
499
691
|
await this.sql.query(`DELETE FROM ${this.t.entry} WHERE instance_id = $1`, [instanceId]);
|
|
500
692
|
}
|
|
693
|
+
async getEntry(workflow, userId) {
|
|
694
|
+
const row = (await this.sql.transaction((tx) => tx.query(`SELECT instance_id, status, admitted_at IS NOT NULL AS admitted
|
|
695
|
+
FROM ${this.t.entry} WHERE workflow = $1 AND user_id = $2`, [workflow, userId]))).at(0);
|
|
696
|
+
return row === void 0 ? void 0 : {
|
|
697
|
+
instanceId: row.instance_id,
|
|
698
|
+
status: row.status,
|
|
699
|
+
admitted: row.admitted
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
async unadmittedEntries(claimedBefore, limit) {
|
|
703
|
+
return (await this.sql.query(`SELECT instance_id, workflow, user_id FROM ${this.t.entry}
|
|
704
|
+
WHERE admitted_at IS NULL AND status = 'running'
|
|
705
|
+
AND COALESCE(claimed_at, ts) < ${TS("$1")}
|
|
706
|
+
ORDER BY COALESCE(claimed_at, ts) LIMIT $2`, [claimedBefore, limit])).map((row) => ({
|
|
707
|
+
instanceId: row.instance_id,
|
|
708
|
+
workflow: row.workflow,
|
|
709
|
+
userId: row.user_id
|
|
710
|
+
}));
|
|
711
|
+
}
|
|
712
|
+
async failUnadmittedEntry(instanceId) {
|
|
713
|
+
return (await this.sql.query(`UPDATE ${this.t.entry} SET status = 'failed'
|
|
714
|
+
WHERE instance_id = $1 AND status = 'running' AND admitted_at IS NULL
|
|
715
|
+
RETURNING instance_id`, [instanceId])).length > 0;
|
|
716
|
+
}
|
|
501
717
|
async setEntryStatus(instanceId, status) {
|
|
502
718
|
await this.sql.query(`UPDATE ${this.t.entry} SET status = $1 WHERE instance_id = $2`, [status, instanceId]);
|
|
503
719
|
}
|