@convex-dev/workos-authkit 0.2.8 → 0.2.10
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 +4 -5
- package/dist/client/index.d.ts +5 -0
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +3 -2
- package/dist/client/index.js.map +1 -1
- package/dist/component/_generated/api.d.ts +0 -1
- package/dist/component/_generated/api.d.ts.map +1 -1
- package/dist/component/_generated/api.js.map +1 -1
- package/dist/component/_generated/component.d.ts +0 -2
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/_generated/server.d.ts +12 -0
- package/dist/component/_generated/server.d.ts.map +1 -1
- package/dist/component/_generated/server.js +2 -0
- package/dist/component/_generated/server.js.map +1 -1
- package/dist/component/backfill.d.ts +6 -6
- package/dist/component/backfill.d.ts.map +1 -1
- package/dist/component/backfill.js +12 -5
- package/dist/component/backfill.js.map +1 -1
- package/dist/component/convex.config.d.ts.map +1 -1
- package/dist/component/convex.config.js +0 -2
- package/dist/component/convex.config.js.map +1 -1
- package/dist/component/lib.d.ts +7 -27
- package/dist/component/lib.d.ts.map +1 -1
- package/dist/component/lib.js +94 -122
- package/dist/component/lib.js.map +1 -1
- package/dist/component/schema.d.ts +13 -4
- package/dist/component/schema.d.ts.map +1 -1
- package/dist/component/schema.js +7 -14
- package/dist/component/schema.js.map +1 -1
- package/dist/validators.d.ts +30 -0
- package/dist/validators.d.ts.map +1 -0
- package/dist/validators.js +17 -0
- package/dist/validators.js.map +1 -0
- package/package.json +5 -5
- package/src/client/index.test.ts +18 -0
- package/src/client/index.ts +10 -2
- package/src/component/_generated/api.ts +0 -1
- package/src/component/_generated/component.ts +0 -2
- package/src/component/_generated/server.ts +13 -0
- package/src/component/backfill.test.ts +19 -11
- package/src/component/backfill.ts +25 -18
- package/src/component/convex.config.ts +0 -2
- package/src/component/lib.test.ts +370 -0
- package/src/component/lib.ts +97 -133
- package/src/component/schema.ts +7 -14
- package/src/test.ts +0 -2
- package/src/validators.ts +17 -0
|
@@ -79,7 +79,6 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
79
79
|
"mutation",
|
|
80
80
|
"internal",
|
|
81
81
|
{
|
|
82
|
-
apiKey: string;
|
|
83
82
|
event: {
|
|
84
83
|
context?: Record<string, any>;
|
|
85
84
|
createdAt: string;
|
|
@@ -87,7 +86,6 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
87
86
|
event: string;
|
|
88
87
|
id: string;
|
|
89
88
|
};
|
|
90
|
-
eventTypes?: Array<string>;
|
|
91
89
|
logLevel?: "DEBUG";
|
|
92
90
|
onEventHandle?: string;
|
|
93
91
|
},
|
|
@@ -30,6 +30,17 @@ import {
|
|
|
30
30
|
} from "convex/server";
|
|
31
31
|
import type { DataModel } from "./dataModel.js";
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Typesafe environment variables.
|
|
35
|
+
*
|
|
36
|
+
* This includes platform-provided env vars and any variables declared in
|
|
37
|
+
* `convex.config.ts`.
|
|
38
|
+
*/
|
|
39
|
+
type Env = {
|
|
40
|
+
readonly CONVEX_CLOUD_URL: string;
|
|
41
|
+
readonly CONVEX_SITE_URL: string;
|
|
42
|
+
};
|
|
43
|
+
|
|
33
44
|
/**
|
|
34
45
|
* Define a query in this Convex app's public API.
|
|
35
46
|
*
|
|
@@ -106,6 +117,8 @@ export const internalAction: ActionBuilder<DataModel, "internal"> =
|
|
|
106
117
|
* @returns The wrapped function. Import this function from `convex/http.js` and route it to hook it up.
|
|
107
118
|
*/
|
|
108
119
|
export const httpAction: HttpActionBuilder = httpActionGeneric;
|
|
120
|
+
export const env: Env = (globalThis as unknown as { process: { env: Env } })
|
|
121
|
+
.process.env;
|
|
109
122
|
|
|
110
123
|
/**
|
|
111
124
|
* A set of services for use within Convex query functions.
|
|
@@ -3,7 +3,6 @@ import { convexTest } from "convex-test";
|
|
|
3
3
|
import { vi, describe, test, expect, beforeEach, afterEach } from "vitest";
|
|
4
4
|
import { modules } from "./setup.test.js";
|
|
5
5
|
import schema from "./schema.js";
|
|
6
|
-
import workpool from "@convex-dev/workpool/test";
|
|
7
6
|
import workflow from "@convex-dev/workflow/test";
|
|
8
7
|
import { api, internal } from "./_generated/api.js";
|
|
9
8
|
|
|
@@ -44,7 +43,6 @@ function makeUser(overrides: Partial<typeof defaultUser> = {}) {
|
|
|
44
43
|
/** Initialize a convex-test instance with sub-component registrations. */
|
|
45
44
|
function initConvexTest() {
|
|
46
45
|
const t = convexTest(schema, modules);
|
|
47
|
-
workpool.register(t, "eventWorkpool");
|
|
48
46
|
workflow.register(t, "backfillWorkflow");
|
|
49
47
|
return t;
|
|
50
48
|
}
|
|
@@ -152,6 +150,22 @@ describe("backfill", () => {
|
|
|
152
150
|
expect(dbUsers).toHaveLength(1);
|
|
153
151
|
});
|
|
154
152
|
|
|
153
|
+
test("upsertUsersPage skips users with a tombstone", async () => {
|
|
154
|
+
const t = initConvexTest();
|
|
155
|
+
const user = makeUser({ id: "user_deleted", email: "gone@example.com" });
|
|
156
|
+
|
|
157
|
+
await t.run(async (ctx) => {
|
|
158
|
+
await ctx.db.insert("deletedUsers", { id: user.id });
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
await t.mutation(internal.backfill.upsertUsersPage, { users: [user] });
|
|
162
|
+
|
|
163
|
+
const dbUsers = await t.run(async (ctx) => {
|
|
164
|
+
return ctx.db.query("users").collect();
|
|
165
|
+
});
|
|
166
|
+
expect(dbUsers).toHaveLength(0);
|
|
167
|
+
});
|
|
168
|
+
|
|
155
169
|
test("full workflow processes single page", async () => {
|
|
156
170
|
const users = [
|
|
157
171
|
makeUser({ id: "user_w1", email: "w1@example.com" }),
|
|
@@ -224,12 +238,8 @@ describe("backfill", () => {
|
|
|
224
238
|
});
|
|
225
239
|
|
|
226
240
|
test("pagination data flows through processUsersPage", async () => {
|
|
227
|
-
const page1Users = [
|
|
228
|
-
|
|
229
|
-
];
|
|
230
|
-
const page2Users = [
|
|
231
|
-
makeUser({ id: "user_p2", email: "p2@example.com" }),
|
|
232
|
-
];
|
|
241
|
+
const page1Users = [makeUser({ id: "user_p1", email: "p1@example.com" })];
|
|
242
|
+
const page2Users = [makeUser({ id: "user_p2", email: "p2@example.com" })];
|
|
233
243
|
|
|
234
244
|
const { WorkOS } = await import("@workos-inc/node");
|
|
235
245
|
const listUsersMock = vi
|
|
@@ -300,9 +310,7 @@ describe("backfill", () => {
|
|
|
300
310
|
});
|
|
301
311
|
|
|
302
312
|
test("upsert is idempotent after processUsersPage inserts", async () => {
|
|
303
|
-
const users = [
|
|
304
|
-
makeUser({ id: "user_idem", email: "idem@example.com" }),
|
|
305
|
-
];
|
|
313
|
+
const users = [makeUser({ id: "user_idem", email: "idem@example.com" })];
|
|
306
314
|
|
|
307
315
|
const { WorkOS } = await import("@workos-inc/node");
|
|
308
316
|
(WorkOS as unknown as ReturnType<typeof vi.fn>).mockImplementation(
|
|
@@ -8,16 +8,13 @@ import {
|
|
|
8
8
|
import { components, internal } from "./_generated/api.js";
|
|
9
9
|
import { WorkOS } from "@workos-inc/node";
|
|
10
10
|
import type { FunctionHandle } from "convex/server";
|
|
11
|
-
import { WorkflowManager } from "@convex-dev/workflow";
|
|
12
|
-
import {
|
|
13
|
-
import schema from "./schema.js";
|
|
11
|
+
import { vResultValidator, WorkflowManager } from "@convex-dev/workflow";
|
|
12
|
+
import { vUser } from "../validators.js";
|
|
14
13
|
|
|
15
14
|
const workflow = new WorkflowManager(components.backfillWorkflow, {
|
|
16
15
|
workpoolOptions: { maxParallelism: 1 },
|
|
17
16
|
});
|
|
18
17
|
|
|
19
|
-
const vUser = schema.tables.users.validator;
|
|
20
|
-
|
|
21
18
|
export const getBackfillApiKey = internalQuery({
|
|
22
19
|
args: {},
|
|
23
20
|
returns: v.union(v.string(), v.null()),
|
|
@@ -37,10 +34,7 @@ export const processUsersPage = internalAction({
|
|
|
37
34
|
returns: v.object({
|
|
38
35
|
nextCursor: v.optional(v.string()),
|
|
39
36
|
}),
|
|
40
|
-
handler: async (
|
|
41
|
-
ctx,
|
|
42
|
-
args
|
|
43
|
-
): Promise<{ nextCursor: string | undefined }> => {
|
|
37
|
+
handler: async (ctx, args): Promise<{ nextCursor: string | undefined }> => {
|
|
44
38
|
const apiKey: string | null = await ctx.runQuery(
|
|
45
39
|
internal.backfill.getBackfillApiKey,
|
|
46
40
|
{}
|
|
@@ -85,7 +79,23 @@ export const upsertUsersPage = internalMutation({
|
|
|
85
79
|
)
|
|
86
80
|
);
|
|
87
81
|
|
|
88
|
-
|
|
82
|
+
// A user.deleted webhook can arrive between the page fetch and this
|
|
83
|
+
// mutation, so skip anyone recorded in deletedUsers or the backfill
|
|
84
|
+
// would insert them again.
|
|
85
|
+
const deletedUsers = await Promise.all(
|
|
86
|
+
args.users.map((user, i) =>
|
|
87
|
+
existingUsers[i]
|
|
88
|
+
? null
|
|
89
|
+
: ctx.db
|
|
90
|
+
.query("deletedUsers")
|
|
91
|
+
.withIndex("id", (q) => q.eq("id", user.id))
|
|
92
|
+
.unique()
|
|
93
|
+
)
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
const newUsers = args.users.filter(
|
|
97
|
+
(_, i) => !existingUsers[i] && !deletedUsers[i]
|
|
98
|
+
);
|
|
89
99
|
|
|
90
100
|
if (args.logLevel === "DEBUG") {
|
|
91
101
|
console.log(
|
|
@@ -124,14 +134,11 @@ export const backfillBatch = workflow.define({
|
|
|
124
134
|
let pagesProcessed = 0;
|
|
125
135
|
|
|
126
136
|
while (pagesProcessed < MAX_PAGES_PER_BATCH) {
|
|
127
|
-
const result = await step.runAction(
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
logLevel: args.logLevel,
|
|
133
|
-
}
|
|
134
|
-
);
|
|
137
|
+
const result = await step.runAction(internal.backfill.processUsersPage, {
|
|
138
|
+
after: cursor,
|
|
139
|
+
onEventHandle: args.onEventHandle,
|
|
140
|
+
logLevel: args.logLevel,
|
|
141
|
+
});
|
|
135
142
|
|
|
136
143
|
cursor = result.nextCursor;
|
|
137
144
|
pagesProcessed++;
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import { defineComponent } from "convex/server";
|
|
2
|
-
import workpool from "@convex-dev/workpool/convex.config";
|
|
3
2
|
import workflow from "@convex-dev/workflow/convex.config";
|
|
4
3
|
|
|
5
4
|
const component = defineComponent("workOSAuthKit");
|
|
6
5
|
|
|
7
|
-
component.use(workpool, { name: "eventWorkpool" });
|
|
8
6
|
component.use(workflow, { name: "backfillWorkflow" });
|
|
9
7
|
|
|
10
8
|
export default component;
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
|
2
|
+
import { convexTest } from "convex-test";
|
|
3
|
+
import { describe, test, expect } from "vitest";
|
|
4
|
+
import { modules } from "./setup.test.js";
|
|
5
|
+
import schema from "./schema.js";
|
|
6
|
+
import workflow from "@convex-dev/workflow/test";
|
|
7
|
+
import { api } from "./_generated/api.js";
|
|
8
|
+
|
|
9
|
+
/** Default test user values. */
|
|
10
|
+
const defaultUser = {
|
|
11
|
+
id: "user_01ABC",
|
|
12
|
+
email: "alice@example.com",
|
|
13
|
+
name: "Alice Smith" as string | null,
|
|
14
|
+
firstName: "Alice" as string | null,
|
|
15
|
+
lastName: "Smith" as string | null,
|
|
16
|
+
emailVerified: true,
|
|
17
|
+
profilePictureUrl: null as string | null,
|
|
18
|
+
lastSignInAt: null as string | null,
|
|
19
|
+
externalId: null as string | null,
|
|
20
|
+
metadata: {} as Record<string, string>,
|
|
21
|
+
locale: null as string | null,
|
|
22
|
+
createdAt: "2024-01-01T00:00:00.000Z",
|
|
23
|
+
updatedAt: "2024-01-01T00:00:00.000Z",
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/** Create a test user fixture. */
|
|
27
|
+
function makeUser(overrides: Partial<typeof defaultUser> = {}) {
|
|
28
|
+
return { ...defaultUser, ...overrides };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Create a webhook event payload fixture. */
|
|
32
|
+
function makeEvent(
|
|
33
|
+
event: string,
|
|
34
|
+
user: ReturnType<typeof makeUser>,
|
|
35
|
+
overrides: Partial<{ id: string; createdAt: string }> = {}
|
|
36
|
+
) {
|
|
37
|
+
return {
|
|
38
|
+
id: overrides.id ?? `event_${event}`,
|
|
39
|
+
createdAt: overrides.createdAt ?? user.updatedAt,
|
|
40
|
+
event,
|
|
41
|
+
data: { object: "user", ...user },
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Create a webhook event payload for any event type. */
|
|
46
|
+
function makeRawEvent(event: string, data: Record<string, unknown>) {
|
|
47
|
+
return {
|
|
48
|
+
id: `event_${event}`,
|
|
49
|
+
createdAt: "2024-01-01T00:00:00.000Z",
|
|
50
|
+
event,
|
|
51
|
+
data,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Initialize a convex-test instance with sub-component registrations. */
|
|
56
|
+
function initConvexTest() {
|
|
57
|
+
const t = convexTest(schema, modules);
|
|
58
|
+
workflow.register(t, "backfillWorkflow");
|
|
59
|
+
return t;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
describe("onWebhookEvent", () => {
|
|
63
|
+
test("user.created inserts the user", async () => {
|
|
64
|
+
const t = initConvexTest();
|
|
65
|
+
const user = makeUser();
|
|
66
|
+
|
|
67
|
+
await t.mutation(api.lib.onWebhookEvent, {
|
|
68
|
+
event: makeEvent("user.created", user),
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const dbUsers = await t.run(async (ctx) => {
|
|
72
|
+
return ctx.db.query("users").collect();
|
|
73
|
+
});
|
|
74
|
+
expect(dbUsers).toHaveLength(1);
|
|
75
|
+
expect(dbUsers[0].id).toBe(user.id);
|
|
76
|
+
expect(dbUsers[0].email).toBe(user.email);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("user.updated patches the user from the delivered payload", async () => {
|
|
80
|
+
const t = initConvexTest();
|
|
81
|
+
const user = makeUser();
|
|
82
|
+
await t.run(async (ctx) => {
|
|
83
|
+
await ctx.db.insert("users", user);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const updated = makeUser({
|
|
87
|
+
name: "Alice Jones",
|
|
88
|
+
lastName: "Jones",
|
|
89
|
+
updatedAt: "2024-01-02T00:00:00.000Z",
|
|
90
|
+
});
|
|
91
|
+
await t.mutation(api.lib.onWebhookEvent, {
|
|
92
|
+
event: makeEvent("user.updated", updated),
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const dbUser = await t.run(async (ctx) => {
|
|
96
|
+
return ctx.db.query("users").unique();
|
|
97
|
+
});
|
|
98
|
+
expect(dbUser?.name).toBe("Alice Jones");
|
|
99
|
+
expect(dbUser?.updatedAt).toBe("2024-01-02T00:00:00.000Z");
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("user.deleted removes the user from the delivered payload", async () => {
|
|
103
|
+
const t = initConvexTest();
|
|
104
|
+
const user = makeUser();
|
|
105
|
+
await t.run(async (ctx) => {
|
|
106
|
+
await ctx.db.insert("users", user);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
await t.mutation(api.lib.onWebhookEvent, {
|
|
110
|
+
event: makeEvent("user.deleted", user),
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
const dbUsers = await t.run(async (ctx) => {
|
|
114
|
+
return ctx.db.query("users").collect();
|
|
115
|
+
});
|
|
116
|
+
expect(dbUsers).toHaveLength(0);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("retried deliveries dedupe on eventId", async () => {
|
|
120
|
+
const t = initConvexTest();
|
|
121
|
+
const user = makeUser();
|
|
122
|
+
const event = makeEvent("user.created", user);
|
|
123
|
+
|
|
124
|
+
await t.mutation(api.lib.onWebhookEvent, {
|
|
125
|
+
event,
|
|
126
|
+
});
|
|
127
|
+
await t.mutation(api.lib.onWebhookEvent, {
|
|
128
|
+
event,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const { dbUsers, dbEvents } = await t.run(async (ctx) => {
|
|
132
|
+
return {
|
|
133
|
+
dbUsers: await ctx.db.query("users").collect(),
|
|
134
|
+
dbEvents: await ctx.db.query("events").collect(),
|
|
135
|
+
};
|
|
136
|
+
});
|
|
137
|
+
expect(dbUsers).toHaveLength(1);
|
|
138
|
+
expect(dbEvents).toHaveLength(1);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("user.updated delivered before user.created upserts the user", async () => {
|
|
142
|
+
const t = initConvexTest();
|
|
143
|
+
const created = makeUser();
|
|
144
|
+
const updated = makeUser({
|
|
145
|
+
name: "Alice Jones",
|
|
146
|
+
lastName: "Jones",
|
|
147
|
+
updatedAt: "2024-01-02T00:00:00.000Z",
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
await t.mutation(api.lib.onWebhookEvent, {
|
|
151
|
+
event: makeEvent("user.updated", updated),
|
|
152
|
+
});
|
|
153
|
+
await t.mutation(api.lib.onWebhookEvent, {
|
|
154
|
+
event: makeEvent("user.created", created),
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const dbUsers = await t.run(async (ctx) => {
|
|
158
|
+
return ctx.db.query("users").collect();
|
|
159
|
+
});
|
|
160
|
+
expect(dbUsers).toHaveLength(1);
|
|
161
|
+
expect(dbUsers[0].name).toBe("Alice Jones");
|
|
162
|
+
expect(dbUsers[0].updatedAt).toBe("2024-01-02T00:00:00.000Z");
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("stale user.updated deliveries are skipped", async () => {
|
|
166
|
+
const t = initConvexTest();
|
|
167
|
+
const user = makeUser({
|
|
168
|
+
name: "Alice Jones",
|
|
169
|
+
updatedAt: "2024-01-02T00:00:00.000Z",
|
|
170
|
+
});
|
|
171
|
+
await t.run(async (ctx) => {
|
|
172
|
+
await ctx.db.insert("users", user);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const stale = makeUser({
|
|
176
|
+
name: "Alice Smith",
|
|
177
|
+
updatedAt: "2024-01-01T00:00:00.000Z",
|
|
178
|
+
});
|
|
179
|
+
await t.mutation(api.lib.onWebhookEvent, {
|
|
180
|
+
event: makeEvent("user.updated", stale),
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
const dbUser = await t.run(async (ctx) => {
|
|
184
|
+
return ctx.db.query("users").unique();
|
|
185
|
+
});
|
|
186
|
+
expect(dbUser?.name).toBe("Alice Jones");
|
|
187
|
+
expect(dbUser?.updatedAt).toBe("2024-01-02T00:00:00.000Z");
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test("user events record the user id", async () => {
|
|
191
|
+
const t = initConvexTest();
|
|
192
|
+
const user = makeUser();
|
|
193
|
+
|
|
194
|
+
await t.mutation(api.lib.onWebhookEvent, {
|
|
195
|
+
event: makeEvent("user.created", user),
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
const dbEvent = await t.run(async (ctx) => {
|
|
199
|
+
return ctx.db.query("events").unique();
|
|
200
|
+
});
|
|
201
|
+
expect(dbEvent?.userId).toBe(user.id);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test("events referencing a user prefer userId over the object id", async () => {
|
|
205
|
+
const t = initConvexTest();
|
|
206
|
+
|
|
207
|
+
await t.mutation(api.lib.onWebhookEvent, {
|
|
208
|
+
event: {
|
|
209
|
+
id: "event_session_created",
|
|
210
|
+
createdAt: "2024-01-01T00:00:00.000Z",
|
|
211
|
+
event: "session.created",
|
|
212
|
+
data: {
|
|
213
|
+
object: "session",
|
|
214
|
+
id: "session_01XYZ",
|
|
215
|
+
userId: "user_01ABC",
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
const dbEvent = await t.run(async (ctx) => {
|
|
221
|
+
return ctx.db.query("events").unique();
|
|
222
|
+
});
|
|
223
|
+
expect(dbEvent?.userId).toBe("user_01ABC");
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test("events without a usable id record no userId", async () => {
|
|
227
|
+
const t = initConvexTest();
|
|
228
|
+
|
|
229
|
+
await t.mutation(api.lib.onWebhookEvent, {
|
|
230
|
+
event: {
|
|
231
|
+
id: "event_connection_activated",
|
|
232
|
+
createdAt: "2024-01-01T00:00:00.000Z",
|
|
233
|
+
event: "connection.activated",
|
|
234
|
+
data: { object: "connection", id: null },
|
|
235
|
+
},
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
const dbEvent = await t.run(async (ctx) => {
|
|
239
|
+
return ctx.db.query("events").unique();
|
|
240
|
+
});
|
|
241
|
+
expect(dbEvent?.userId).toBeUndefined();
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test("user.deleted records a tombstone", async () => {
|
|
245
|
+
const t = initConvexTest();
|
|
246
|
+
const user = makeUser();
|
|
247
|
+
await t.run(async (ctx) => {
|
|
248
|
+
await ctx.db.insert("users", user);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
await t.mutation(api.lib.onWebhookEvent, {
|
|
252
|
+
event: makeEvent("user.deleted", user),
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
const { dbUsers, dbDeletedUsers } = await t.run(async (ctx) => {
|
|
256
|
+
return {
|
|
257
|
+
dbUsers: await ctx.db.query("users").collect(),
|
|
258
|
+
dbDeletedUsers: await ctx.db.query("deletedUsers").collect(),
|
|
259
|
+
};
|
|
260
|
+
});
|
|
261
|
+
expect(dbUsers).toHaveLength(0);
|
|
262
|
+
expect(dbDeletedUsers).toHaveLength(1);
|
|
263
|
+
expect(dbDeletedUsers[0].id).toBe(user.id);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test("user.deleted works with only an id in the payload", async () => {
|
|
267
|
+
const t = initConvexTest();
|
|
268
|
+
const user = makeUser();
|
|
269
|
+
await t.run(async (ctx) => {
|
|
270
|
+
await ctx.db.insert("users", user);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
await t.mutation(api.lib.onWebhookEvent, {
|
|
274
|
+
event: makeRawEvent("user.deleted", { object: "user", id: user.id }),
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
const { dbUsers, dbDeletedUsers } = await t.run(async (ctx) => {
|
|
278
|
+
return {
|
|
279
|
+
dbUsers: await ctx.db.query("users").collect(),
|
|
280
|
+
dbDeletedUsers: await ctx.db.query("deletedUsers").collect(),
|
|
281
|
+
};
|
|
282
|
+
});
|
|
283
|
+
expect(dbUsers).toHaveLength(0);
|
|
284
|
+
expect(dbDeletedUsers).toHaveLength(1);
|
|
285
|
+
expect(dbDeletedUsers[0].id).toBe(user.id);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
test("user.created after user.deleted is skipped", async () => {
|
|
289
|
+
const t = initConvexTest();
|
|
290
|
+
const user = makeUser();
|
|
291
|
+
|
|
292
|
+
await t.mutation(api.lib.onWebhookEvent, {
|
|
293
|
+
event: makeEvent("user.deleted", user),
|
|
294
|
+
});
|
|
295
|
+
await t.mutation(api.lib.onWebhookEvent, {
|
|
296
|
+
event: makeEvent("user.created", user),
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
const { dbUsers, dbDeletedUsers } = await t.run(async (ctx) => {
|
|
300
|
+
return {
|
|
301
|
+
dbUsers: await ctx.db.query("users").collect(),
|
|
302
|
+
dbDeletedUsers: await ctx.db.query("deletedUsers").collect(),
|
|
303
|
+
};
|
|
304
|
+
});
|
|
305
|
+
expect(dbUsers).toHaveLength(0);
|
|
306
|
+
expect(dbDeletedUsers).toHaveLength(1);
|
|
307
|
+
expect(dbDeletedUsers[0].id).toBe(user.id);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
test("user.updated after user.deleted is skipped", async () => {
|
|
311
|
+
const t = initConvexTest();
|
|
312
|
+
const user = makeUser();
|
|
313
|
+
|
|
314
|
+
await t.mutation(api.lib.onWebhookEvent, {
|
|
315
|
+
event: makeEvent("user.deleted", user),
|
|
316
|
+
});
|
|
317
|
+
await t.mutation(api.lib.onWebhookEvent, {
|
|
318
|
+
event: makeEvent("user.updated", user),
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
const { dbUsers, dbDeletedUsers } = await t.run(async (ctx) => {
|
|
322
|
+
return {
|
|
323
|
+
dbUsers: await ctx.db.query("users").collect(),
|
|
324
|
+
dbDeletedUsers: await ctx.db.query("deletedUsers").collect(),
|
|
325
|
+
};
|
|
326
|
+
});
|
|
327
|
+
expect(dbUsers).toHaveLength(0);
|
|
328
|
+
expect(dbDeletedUsers).toHaveLength(1);
|
|
329
|
+
expect(dbDeletedUsers[0].id).toBe(user.id);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
test("organization.created leaves userId unset", async () => {
|
|
333
|
+
const t = initConvexTest();
|
|
334
|
+
|
|
335
|
+
await t.mutation(api.lib.onWebhookEvent, {
|
|
336
|
+
event: makeRawEvent("organization.created", {
|
|
337
|
+
object: "organization",
|
|
338
|
+
id: "org_01ABC",
|
|
339
|
+
name: "Acme",
|
|
340
|
+
}),
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
const dbEvents = await t.run(async (ctx) => {
|
|
344
|
+
return ctx.db.query("events").collect();
|
|
345
|
+
});
|
|
346
|
+
expect(dbEvents).toHaveLength(1);
|
|
347
|
+
expect(dbEvents[0].userId).toBeUndefined();
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
test("authentication events with a null userId are recorded", async () => {
|
|
351
|
+
const t = initConvexTest();
|
|
352
|
+
|
|
353
|
+
await t.mutation(api.lib.onWebhookEvent, {
|
|
354
|
+
event: makeRawEvent("authentication.password_failed", {
|
|
355
|
+
userId: null,
|
|
356
|
+
email: "alice@example.com",
|
|
357
|
+
status: "failed",
|
|
358
|
+
type: "password",
|
|
359
|
+
ipAddress: null,
|
|
360
|
+
userAgent: null,
|
|
361
|
+
}),
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
const dbEvents = await t.run(async (ctx) => {
|
|
365
|
+
return ctx.db.query("events").collect();
|
|
366
|
+
});
|
|
367
|
+
expect(dbEvents).toHaveLength(1);
|
|
368
|
+
expect(dbEvents[0].userId).toBeUndefined();
|
|
369
|
+
});
|
|
370
|
+
});
|