@convex-dev/better-auth 0.12.0 → 0.12.2
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/dist/client/adapter-utils.d.ts.map +1 -1
- package/dist/client/adapter-utils.js +4 -2
- package/dist/client/adapter-utils.js.map +1 -1
- package/dist/client/adapter.d.ts +0 -1
- package/dist/client/adapter.d.ts.map +1 -1
- package/dist/client/adapter.js.map +1 -1
- package/dist/client/create-api.d.ts.map +1 -1
- package/dist/client/create-api.js +17 -7
- package/dist/client/create-api.js.map +1 -1
- package/dist/client/create-client.d.ts +0 -2
- package/dist/client/create-client.d.ts.map +1 -1
- package/dist/client/create-client.js.map +1 -1
- package/dist/component/_generated/api.d.ts +2 -2
- package/dist/component/_generated/api.d.ts.map +1 -1
- package/dist/component/_generated/component.d.ts +0 -3
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/testTriggerHandlers.d.ts +10 -0
- package/dist/component/testTriggerHandlers.d.ts.map +1 -0
- package/dist/component/testTriggerHandlers.js +25 -0
- package/dist/component/testTriggerHandlers.js.map +1 -0
- package/dist/nextjs/index.d.ts.map +1 -1
- package/dist/nextjs/index.js +24 -9
- package/dist/nextjs/index.js.map +1 -1
- package/dist/plugins/cross-domain/client.d.ts.map +1 -1
- package/dist/plugins/cross-domain/client.js +21 -1
- package/dist/plugins/cross-domain/client.js.map +1 -1
- package/dist/react-start/index.d.ts.map +1 -1
- package/dist/react-start/index.js +5 -0
- package/dist/react-start/index.js.map +1 -1
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +1 -0
- package/dist/utils/index.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +24 -24
- package/src/client/adapter-utils.ts +4 -2
- package/src/client/adapter.test.ts +215 -6
- package/src/client/adapter.ts +0 -1
- package/src/client/create-api.ts +36 -10
- package/src/client/create-client.ts +0 -2
- package/src/client/triggers.test.ts +58 -0
- package/src/component/_generated/api.ts +2 -2
- package/src/component/_generated/component.ts +0 -3
- package/src/component/testTriggerHandlers.ts +27 -0
- package/src/nextjs/index.test.ts +109 -0
- package/src/nextjs/index.ts +28 -9
- package/src/plugins/cross-domain/client.test.ts +26 -7
- package/src/plugins/cross-domain/client.ts +28 -2
- package/src/react-start/index.test.ts +91 -0
- package/src/react-start/index.ts +5 -0
- package/src/utils/index.ts +1 -0
- package/src/version.ts +1 -1
- package/dist/component/adapterTest.d.ts +0 -3
- package/dist/component/adapterTest.d.ts.map +0 -1
- package/dist/component/adapterTest.js +0 -202
- package/dist/component/adapterTest.js.map +0 -1
- package/src/component/adapterTest.ts +0 -275
|
@@ -2,8 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
import { describe, it } from "vitest";
|
|
4
4
|
import { convexTest } from "convex-test";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
testAdapter,
|
|
7
|
+
transactionsTestSuite,
|
|
8
|
+
} from "@better-auth/test-utils/adapter";
|
|
9
|
+
import type { BetterAuthOptions } from "better-auth";
|
|
10
|
+
import { internal } from "../component/_generated/api.js";
|
|
6
11
|
import schema from "../component/testProfiles/schema.profile-plugin-table.js";
|
|
12
|
+
import { createClient } from "./index.js";
|
|
13
|
+
import type { DataModel } from "../component/_generated/dataModel.js";
|
|
14
|
+
import type { ComponentApi } from "../component/_generated/component.js";
|
|
15
|
+
import {
|
|
16
|
+
additionalFieldsAuthFlowTestSuite,
|
|
17
|
+
additionalFieldsNormalTestSuite,
|
|
18
|
+
convexCustomTestSuite,
|
|
19
|
+
coreAuthFlowTestSuite,
|
|
20
|
+
coreNormalTestSuite,
|
|
21
|
+
multiJoinsMissingRowsTestSuite,
|
|
22
|
+
pluginTableNormalTestSuite,
|
|
23
|
+
renameFieldAndJoinTestSuite,
|
|
24
|
+
renameModelUserCustomTestSuite,
|
|
25
|
+
renameModelUserTableTestSuite,
|
|
26
|
+
} from "../test/adapter-factory/index.js";
|
|
7
27
|
|
|
8
28
|
const MIN_NODE_MAJOR = 24;
|
|
9
29
|
const currentNodeMajor = Number.parseInt(
|
|
@@ -11,16 +31,205 @@ const currentNodeMajor = Number.parseInt(
|
|
|
11
31
|
10
|
|
12
32
|
);
|
|
13
33
|
|
|
34
|
+
const NORMAL_DISABLED_TESTS = [
|
|
35
|
+
// dynamic-schema-plugin-table/dynamic-schema-additional-fields:
|
|
36
|
+
// Convex validators are static in this harness, so runtime schema mutation
|
|
37
|
+
// tests are validated in dedicated profiles instead.
|
|
38
|
+
"create - should apply default values to fields",
|
|
39
|
+
"create - should return null for nullable foreign keys",
|
|
40
|
+
"create - should support arrays",
|
|
41
|
+
"create - should support json",
|
|
42
|
+
// convex-id-generation:
|
|
43
|
+
// Convex controls generated IDs at write time.
|
|
44
|
+
"create - should use generateId if provided",
|
|
45
|
+
// offset-unsupported:
|
|
46
|
+
// Convex adapter rejects offset pagination.
|
|
47
|
+
"findMany - should be able to perform a complex limited join",
|
|
48
|
+
"findMany - should find many models with limit and offset",
|
|
49
|
+
"findMany - should find many models with offset",
|
|
50
|
+
"findMany - should find many models with sortBy and limit and offset",
|
|
51
|
+
"findMany - should find many models with sortBy and limit and offset and where",
|
|
52
|
+
"findMany - should find many models with sortBy and offset",
|
|
53
|
+
"findMany - should find many with both one-to-one and one-to-many joins",
|
|
54
|
+
"findMany - should find many with join and offset",
|
|
55
|
+
"findMany - should find many with join, where, limit, and offset",
|
|
56
|
+
"findMany - should find many with one-to-one join",
|
|
57
|
+
"findMany - should handle mixed joins correctly when some are missing",
|
|
58
|
+
"findMany - should return empty array when base records don't exist with joins",
|
|
59
|
+
"findMany - should return null for one-to-one join when joined records don't exist",
|
|
60
|
+
"findMany - should select fields with one-to-one join",
|
|
61
|
+
// profile-specific coverage:
|
|
62
|
+
// These are intentionally exercised in dedicated profile suites.
|
|
63
|
+
"findOne - backwards join with modified field name (session base, users-table join)",
|
|
64
|
+
"findOne - multiple joins should return result even when some joined tables have no matching rows",
|
|
65
|
+
"findOne - should find a model with modified field name",
|
|
66
|
+
"findOne - should find a model with modified model name",
|
|
67
|
+
"findOne - should join a model with modified field name",
|
|
68
|
+
"findOne - should not apply defaultValue if value not found",
|
|
69
|
+
"findOne - should return an object for one-to-one joins",
|
|
70
|
+
"findOne - should return null for failed base model lookup that has joins",
|
|
71
|
+
"findOne - should return null for one-to-one join when joined record doesn't exist",
|
|
72
|
+
"findOne - should select fields with one-to-one join",
|
|
73
|
+
"findOne - should work with both one-to-one and one-to-many joins",
|
|
74
|
+
] as const;
|
|
75
|
+
|
|
76
|
+
const toDisableMap = (testNames: readonly string[]) =>
|
|
77
|
+
Object.fromEntries(testNames.map((testName) => [testName, true]));
|
|
78
|
+
|
|
79
|
+
const getOverrideBetterAuthOptions = (
|
|
80
|
+
opts: BetterAuthOptions
|
|
81
|
+
): BetterAuthOptions => ({
|
|
82
|
+
...opts,
|
|
83
|
+
advanced: {
|
|
84
|
+
...opts.advanced,
|
|
85
|
+
database: {
|
|
86
|
+
...opts.advanced?.database,
|
|
87
|
+
generateId: "uuid",
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
type AdapterModule = ComponentApi["adapter"];
|
|
93
|
+
type TestProfileName =
|
|
94
|
+
| "adapterAdditionalFields"
|
|
95
|
+
| "adapterPluginTable"
|
|
96
|
+
| "adapterRenameField"
|
|
97
|
+
| "adapterRenameUserCustom"
|
|
98
|
+
| "adapterRenameUserTable"
|
|
99
|
+
| "adapterOrganizationJoins";
|
|
100
|
+
|
|
101
|
+
type InternalWithTestProfiles = {
|
|
102
|
+
adapter: AdapterModule;
|
|
103
|
+
testProfiles: Record<TestProfileName, AdapterModule>;
|
|
104
|
+
};
|
|
105
|
+
|
|
14
106
|
if (currentNodeMajor < MIN_NODE_MAJOR) {
|
|
15
107
|
describe("Better Auth Adapter Tests", () => {
|
|
16
|
-
it.skip(
|
|
17
|
-
`requires Node ${MIN_NODE_MAJOR}+ (adapter test-utils uses explicit resource management syntax)`,
|
|
18
|
-
() => {}
|
|
19
|
-
);
|
|
108
|
+
it.skip(`requires Node ${MIN_NODE_MAJOR}+ (adapter test-utils uses explicit resource management syntax)`, () => {});
|
|
20
109
|
});
|
|
21
110
|
} else {
|
|
22
111
|
describe("Better Auth Adapter Tests", async () => {
|
|
23
112
|
const t = convexTest(schema, import.meta.glob("../component/**/*.*s"));
|
|
24
|
-
|
|
113
|
+
|
|
114
|
+
// Bridge `convexAdapter`'s ctx surface to `convexTest`. Each adapter call
|
|
115
|
+
// re-enters convex-test's AsyncLocalStorage frame via t.query / t.mutation,
|
|
116
|
+
// which is required by convex-test ≥0.0.45. State persists across calls
|
|
117
|
+
// because the test database lives on `t`, not in ALS.
|
|
118
|
+
const wrappedCtx = {
|
|
119
|
+
runQuery: t.query.bind(t),
|
|
120
|
+
runMutation: t.mutation.bind(t),
|
|
121
|
+
} as any;
|
|
122
|
+
|
|
123
|
+
const internalWithTestProfiles =
|
|
124
|
+
internal as unknown as InternalWithTestProfiles;
|
|
125
|
+
const profileApi = (name: TestProfileName): { adapter: AdapterModule } => ({
|
|
126
|
+
adapter: internalWithTestProfiles.testProfiles[name],
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const baseProfileClient = createClient<DataModel>(
|
|
130
|
+
{ adapter: internalWithTestProfiles.adapter },
|
|
131
|
+
{ verbose: false }
|
|
132
|
+
);
|
|
133
|
+
const additionalFieldsProfileClient = createClient<DataModel>(
|
|
134
|
+
profileApi("adapterAdditionalFields"),
|
|
135
|
+
{ verbose: false }
|
|
136
|
+
);
|
|
137
|
+
const pluginTableProfileClient = createClient<DataModel>(
|
|
138
|
+
profileApi("adapterPluginTable"),
|
|
139
|
+
{ verbose: false }
|
|
140
|
+
);
|
|
141
|
+
const renameFieldProfileClient = createClient<DataModel>(
|
|
142
|
+
profileApi("adapterRenameField"),
|
|
143
|
+
{ verbose: false }
|
|
144
|
+
);
|
|
145
|
+
const renameUserCustomProfileClient = createClient<DataModel>(
|
|
146
|
+
profileApi("adapterRenameUserCustom"),
|
|
147
|
+
{ verbose: false }
|
|
148
|
+
);
|
|
149
|
+
const renameUserTableProfileClient = createClient<DataModel>(
|
|
150
|
+
profileApi("adapterRenameUserTable"),
|
|
151
|
+
{ verbose: false }
|
|
152
|
+
);
|
|
153
|
+
const organizationJoinsProfileClient = createClient<DataModel>(
|
|
154
|
+
profileApi("adapterOrganizationJoins"),
|
|
155
|
+
{ verbose: false }
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
const noMigrations = () => {
|
|
159
|
+
// Convex schema is static — no migrations needed.
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const { execute: executeBaseProfile } = await testAdapter({
|
|
163
|
+
adapter: () => baseProfileClient.adapter(wrappedCtx),
|
|
164
|
+
runMigrations: noMigrations,
|
|
165
|
+
overrideBetterAuthOptions: getOverrideBetterAuthOptions,
|
|
166
|
+
tests: [
|
|
167
|
+
coreNormalTestSuite({
|
|
168
|
+
disableTests: toDisableMap(NORMAL_DISABLED_TESTS),
|
|
169
|
+
}),
|
|
170
|
+
transactionsTestSuite({ disableTests: { ALL: true } }),
|
|
171
|
+
coreAuthFlowTestSuite(),
|
|
172
|
+
convexCustomTestSuite(),
|
|
173
|
+
],
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const { execute: executeAdditionalFieldsProfile } = await testAdapter({
|
|
177
|
+
adapter: () => additionalFieldsProfileClient.adapter(wrappedCtx),
|
|
178
|
+
runMigrations: noMigrations,
|
|
179
|
+
overrideBetterAuthOptions: getOverrideBetterAuthOptions,
|
|
180
|
+
prefixTests: "profile:additional-fields",
|
|
181
|
+
tests: [
|
|
182
|
+
additionalFieldsNormalTestSuite(),
|
|
183
|
+
additionalFieldsAuthFlowTestSuite(),
|
|
184
|
+
],
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
const { execute: executePluginTableProfile } = await testAdapter({
|
|
188
|
+
adapter: () => pluginTableProfileClient.adapter(wrappedCtx),
|
|
189
|
+
runMigrations: noMigrations,
|
|
190
|
+
overrideBetterAuthOptions: getOverrideBetterAuthOptions,
|
|
191
|
+
prefixTests: "profile:plugin-table",
|
|
192
|
+
tests: [pluginTableNormalTestSuite()],
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const { execute: executeRenameFieldProfile } = await testAdapter({
|
|
196
|
+
adapter: () => renameFieldProfileClient.adapter(wrappedCtx),
|
|
197
|
+
runMigrations: noMigrations,
|
|
198
|
+
overrideBetterAuthOptions: getOverrideBetterAuthOptions,
|
|
199
|
+
prefixTests: "profile:rename-field-join",
|
|
200
|
+
tests: [renameFieldAndJoinTestSuite()],
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
const { execute: executeRenameUserCustomProfile } = await testAdapter({
|
|
204
|
+
adapter: () => renameUserCustomProfileClient.adapter(wrappedCtx),
|
|
205
|
+
runMigrations: noMigrations,
|
|
206
|
+
overrideBetterAuthOptions: getOverrideBetterAuthOptions,
|
|
207
|
+
prefixTests: "profile:rename-user-custom",
|
|
208
|
+
tests: [renameModelUserCustomTestSuite()],
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
const { execute: executeRenameUserTableProfile } = await testAdapter({
|
|
212
|
+
adapter: () => renameUserTableProfileClient.adapter(wrappedCtx),
|
|
213
|
+
runMigrations: noMigrations,
|
|
214
|
+
overrideBetterAuthOptions: getOverrideBetterAuthOptions,
|
|
215
|
+
prefixTests: "profile:rename-user-table",
|
|
216
|
+
tests: [renameModelUserTableTestSuite()],
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
const { execute: executeOrganizationJoinsProfile } = await testAdapter({
|
|
220
|
+
adapter: () => organizationJoinsProfileClient.adapter(wrappedCtx),
|
|
221
|
+
runMigrations: noMigrations,
|
|
222
|
+
overrideBetterAuthOptions: getOverrideBetterAuthOptions,
|
|
223
|
+
prefixTests: "profile:organization-joins",
|
|
224
|
+
tests: [multiJoinsMissingRowsTestSuite()],
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
executeBaseProfile();
|
|
228
|
+
executeAdditionalFieldsProfile();
|
|
229
|
+
executePluginTableProfile();
|
|
230
|
+
executeRenameFieldProfile();
|
|
231
|
+
executeRenameUserCustomProfile();
|
|
232
|
+
executeRenameUserTableProfile();
|
|
233
|
+
executeOrganizationJoinsProfile();
|
|
25
234
|
});
|
|
26
235
|
}
|
package/src/client/adapter.ts
CHANGED
package/src/client/create-api.ts
CHANGED
|
@@ -55,9 +55,7 @@ const whereValidator = (
|
|
|
55
55
|
v.null()
|
|
56
56
|
),
|
|
57
57
|
connector: v.optional(v.union(v.literal("AND"), v.literal("OR"))),
|
|
58
|
-
mode: v.optional(
|
|
59
|
-
v.union(v.literal("sensitive"), v.literal("insensitive"))
|
|
60
|
-
),
|
|
58
|
+
mode: v.optional(v.union(v.literal("sensitive"), v.literal("insensitive"))),
|
|
61
59
|
});
|
|
62
60
|
|
|
63
61
|
export const createApi = <Schema extends SchemaDefinition<any, any>>(
|
|
@@ -91,7 +89,7 @@ export const createApi = <Schema extends SchemaDefinition<any, any>>(
|
|
|
91
89
|
args.input.model as any,
|
|
92
90
|
args.input.data
|
|
93
91
|
);
|
|
94
|
-
const doc = await ctx.db.get(id);
|
|
92
|
+
const doc = await ctx.db.get(args.input.model, id);
|
|
95
93
|
if (!doc) {
|
|
96
94
|
throw new Error(`Failed to create ${args.input.model}`);
|
|
97
95
|
}
|
|
@@ -104,6 +102,13 @@ export const createApi = <Schema extends SchemaDefinition<any, any>>(
|
|
|
104
102
|
doc,
|
|
105
103
|
}
|
|
106
104
|
);
|
|
105
|
+
const updatedDoc = await ctx.db.get(args.input.model, id);
|
|
106
|
+
if (!updatedDoc) {
|
|
107
|
+
throw new Error(
|
|
108
|
+
`Failed to create ${args.input.model} (deleted by onCreate trigger?)`
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
return selectFields(updatedDoc, args.select);
|
|
107
112
|
}
|
|
108
113
|
return result;
|
|
109
114
|
},
|
|
@@ -174,10 +179,14 @@ export const createApi = <Schema extends SchemaDefinition<any, any>>(
|
|
|
174
179
|
doc
|
|
175
180
|
);
|
|
176
181
|
await ctx.db.patch(
|
|
177
|
-
|
|
182
|
+
args.input.model,
|
|
183
|
+
doc._id as GenericId<TableNames>,
|
|
178
184
|
args.input.update as any
|
|
179
185
|
);
|
|
180
|
-
const updatedDoc = await ctx.db.get(
|
|
186
|
+
const updatedDoc = await ctx.db.get(
|
|
187
|
+
args.input.model,
|
|
188
|
+
doc._id as GenericId<TableNames>
|
|
189
|
+
);
|
|
181
190
|
if (!updatedDoc) {
|
|
182
191
|
throw new Error(`Failed to update ${args.input.model}`);
|
|
183
192
|
}
|
|
@@ -190,6 +199,16 @@ export const createApi = <Schema extends SchemaDefinition<any, any>>(
|
|
|
190
199
|
oldDoc: doc,
|
|
191
200
|
}
|
|
192
201
|
);
|
|
202
|
+
const innerUpdatedDoc = await ctx.db.get(
|
|
203
|
+
args.input.model,
|
|
204
|
+
doc._id as GenericId<TableNames>
|
|
205
|
+
);
|
|
206
|
+
if (!innerUpdatedDoc) {
|
|
207
|
+
throw new Error(
|
|
208
|
+
`Failed to update ${args.input.model} (deleted by onUpdate trigger?)`
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
return innerUpdatedDoc;
|
|
193
212
|
}
|
|
194
213
|
return updatedDoc;
|
|
195
214
|
},
|
|
@@ -245,7 +264,8 @@ export const createApi = <Schema extends SchemaDefinition<any, any>>(
|
|
|
245
264
|
doc
|
|
246
265
|
);
|
|
247
266
|
await ctx.db.patch(
|
|
248
|
-
|
|
267
|
+
args.input.model,
|
|
268
|
+
doc._id as GenericId<TableNames>,
|
|
249
269
|
args.input.update as any
|
|
250
270
|
);
|
|
251
271
|
|
|
@@ -254,7 +274,10 @@ export const createApi = <Schema extends SchemaDefinition<any, any>>(
|
|
|
254
274
|
args.onUpdateHandle as FunctionHandle<"mutation">,
|
|
255
275
|
{
|
|
256
276
|
model: args.input.model,
|
|
257
|
-
newDoc: await ctx.db.get(
|
|
277
|
+
newDoc: await ctx.db.get(
|
|
278
|
+
args.input.model,
|
|
279
|
+
doc._id as GenericId<TableNames>
|
|
280
|
+
),
|
|
258
281
|
oldDoc: doc,
|
|
259
282
|
}
|
|
260
283
|
);
|
|
@@ -286,7 +309,7 @@ export const createApi = <Schema extends SchemaDefinition<any, any>>(
|
|
|
286
309
|
if (!doc) {
|
|
287
310
|
return;
|
|
288
311
|
}
|
|
289
|
-
await ctx.db.delete(doc._id as GenericId<
|
|
312
|
+
await ctx.db.delete(args.input.model, doc._id as GenericId<TableNames>);
|
|
290
313
|
if (args.onDeleteHandle) {
|
|
291
314
|
await ctx.runMutation(
|
|
292
315
|
args.onDeleteHandle as FunctionHandle<"mutation">,
|
|
@@ -330,7 +353,10 @@ export const createApi = <Schema extends SchemaDefinition<any, any>>(
|
|
|
330
353
|
}
|
|
331
354
|
);
|
|
332
355
|
}
|
|
333
|
-
await ctx.db.delete(
|
|
356
|
+
await ctx.db.delete(
|
|
357
|
+
args.input.model,
|
|
358
|
+
doc._id as GenericId<TableNames>
|
|
359
|
+
);
|
|
334
360
|
});
|
|
335
361
|
return {
|
|
336
362
|
...result,
|
|
@@ -17,7 +17,6 @@ import type { Infer } from "convex/values";
|
|
|
17
17
|
import { convexAdapter } from "./adapter.js";
|
|
18
18
|
import { corsRouter } from "convex-helpers/server/cors";
|
|
19
19
|
import type defaultSchema from "../component/schema.js";
|
|
20
|
-
import type { ComponentApi } from "../component/_generated/component.js";
|
|
21
20
|
import type { CreateAuth, GenericCtx } from "./index.js";
|
|
22
21
|
import type { TrustedOriginsOption } from "../utils/index.js";
|
|
23
22
|
|
|
@@ -70,7 +69,6 @@ type SlimComponentApi = {
|
|
|
70
69
|
deleteOne: FunctionReference<"mutation", "internal">;
|
|
71
70
|
deleteMany: FunctionReference<"mutation", "internal">;
|
|
72
71
|
};
|
|
73
|
-
adapterTest?: ComponentApi["adapterTest"];
|
|
74
72
|
};
|
|
75
73
|
|
|
76
74
|
type RouteCorsOptions =
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
|
2
|
+
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
import { convexTest } from "convex-test";
|
|
5
|
+
import { createFunctionHandle } from "convex/server";
|
|
6
|
+
import schema from "../component/schema.js";
|
|
7
|
+
import { api, internal } from "../component/_generated/api.js";
|
|
8
|
+
|
|
9
|
+
const baseSessionData = () => ({
|
|
10
|
+
expiresAt: Date.now() + 60_000,
|
|
11
|
+
createdAt: Date.now(),
|
|
12
|
+
updatedAt: Date.now(),
|
|
13
|
+
userId: "user-1",
|
|
14
|
+
userAgent: "original",
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
describe("trigger result propagation", () => {
|
|
18
|
+
it("api.adapter.create returns the doc reflecting onCreateHandle writes", async () => {
|
|
19
|
+
const t = convexTest(schema, import.meta.glob("../component/**/*.*s"));
|
|
20
|
+
const result = await t.run(async (ctx) => {
|
|
21
|
+
const handle = await createFunctionHandle(
|
|
22
|
+
internal.testTriggerHandlers.sessionOnCreateUpdater
|
|
23
|
+
);
|
|
24
|
+
return await ctx.runMutation(api.adapter.create, {
|
|
25
|
+
input: {
|
|
26
|
+
model: "session",
|
|
27
|
+
data: { ...baseSessionData(), token: "create-token-1" },
|
|
28
|
+
},
|
|
29
|
+
onCreateHandle: handle,
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
expect(result.userAgent).toBe("trigger-ran-on-create");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("api.adapter.updateOne returns the doc reflecting onUpdateHandle writes", async () => {
|
|
36
|
+
const t = convexTest(schema, import.meta.glob("../component/**/*.*s"));
|
|
37
|
+
const result = await t.run(async (ctx) => {
|
|
38
|
+
await ctx.runMutation(api.adapter.create, {
|
|
39
|
+
input: {
|
|
40
|
+
model: "session",
|
|
41
|
+
data: { ...baseSessionData(), token: "update-token-1" },
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
const handle = await createFunctionHandle(
|
|
45
|
+
internal.testTriggerHandlers.sessionOnUpdateUpdater
|
|
46
|
+
);
|
|
47
|
+
return await ctx.runMutation(api.adapter.updateOne, {
|
|
48
|
+
input: {
|
|
49
|
+
model: "session",
|
|
50
|
+
update: { userAgent: "set-by-update" },
|
|
51
|
+
where: [{ field: "token", operator: "eq", value: "update-token-1" }],
|
|
52
|
+
},
|
|
53
|
+
onUpdateHandle: handle,
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
expect(result.userAgent).toBe("trigger-ran-on-update");
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -9,13 +9,13 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import type * as adapter from "../adapter.js";
|
|
12
|
-
import type * as adapterTest from "../adapterTest.js";
|
|
13
12
|
import type * as testProfiles_adapterAdditionalFields from "../testProfiles/adapterAdditionalFields.js";
|
|
14
13
|
import type * as testProfiles_adapterOrganizationJoins from "../testProfiles/adapterOrganizationJoins.js";
|
|
15
14
|
import type * as testProfiles_adapterPluginTable from "../testProfiles/adapterPluginTable.js";
|
|
16
15
|
import type * as testProfiles_adapterRenameField from "../testProfiles/adapterRenameField.js";
|
|
17
16
|
import type * as testProfiles_adapterRenameUserCustom from "../testProfiles/adapterRenameUserCustom.js";
|
|
18
17
|
import type * as testProfiles_adapterRenameUserTable from "../testProfiles/adapterRenameUserTable.js";
|
|
18
|
+
import type * as testTriggerHandlers from "../testTriggerHandlers.js";
|
|
19
19
|
|
|
20
20
|
import type {
|
|
21
21
|
ApiFromModules,
|
|
@@ -26,13 +26,13 @@ import { anyApi, componentsGeneric } from "convex/server";
|
|
|
26
26
|
|
|
27
27
|
const fullApi: ApiFromModules<{
|
|
28
28
|
adapter: typeof adapter;
|
|
29
|
-
adapterTest: typeof adapterTest;
|
|
30
29
|
"testProfiles/adapterAdditionalFields": typeof testProfiles_adapterAdditionalFields;
|
|
31
30
|
"testProfiles/adapterOrganizationJoins": typeof testProfiles_adapterOrganizationJoins;
|
|
32
31
|
"testProfiles/adapterPluginTable": typeof testProfiles_adapterPluginTable;
|
|
33
32
|
"testProfiles/adapterRenameField": typeof testProfiles_adapterRenameField;
|
|
34
33
|
"testProfiles/adapterRenameUserCustom": typeof testProfiles_adapterRenameUserCustom;
|
|
35
34
|
"testProfiles/adapterRenameUserTable": typeof testProfiles_adapterRenameUserTable;
|
|
35
|
+
testTriggerHandlers: typeof testTriggerHandlers;
|
|
36
36
|
}> = anyApi as any;
|
|
37
37
|
|
|
38
38
|
/**
|
|
@@ -1893,9 +1893,6 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
1893
1893
|
Name
|
|
1894
1894
|
>;
|
|
1895
1895
|
};
|
|
1896
|
-
adapterTest: {
|
|
1897
|
-
runTests: FunctionReference<"action", "internal", any, any, Name>;
|
|
1898
|
-
};
|
|
1899
1896
|
testProfiles: {
|
|
1900
1897
|
adapterAdditionalFields: {
|
|
1901
1898
|
create: FunctionReference<
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Stub trigger handles for tests that need a real FunctionReference to pass
|
|
2
|
+
// as onCreateHandle / onUpdateHandle on adapter writes.
|
|
3
|
+
|
|
4
|
+
import { internalMutationGeneric } from "convex/server";
|
|
5
|
+
import { v } from "convex/values";
|
|
6
|
+
|
|
7
|
+
export const sessionOnCreateUpdater = internalMutationGeneric({
|
|
8
|
+
args: { doc: v.any(), model: v.string() },
|
|
9
|
+
handler: async (ctx, args) => {
|
|
10
|
+
if (args.model === "session") {
|
|
11
|
+
await ctx.db.patch("session", args.doc._id, {
|
|
12
|
+
userAgent: "trigger-ran-on-create",
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export const sessionOnUpdateUpdater = internalMutationGeneric({
|
|
19
|
+
args: { newDoc: v.any(), oldDoc: v.any(), model: v.string() },
|
|
20
|
+
handler: async (ctx, args) => {
|
|
21
|
+
if (args.model === "session") {
|
|
22
|
+
await ctx.db.patch("session", args.newDoc._id, {
|
|
23
|
+
userAgent: "trigger-ran-on-update",
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
});
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { convexBetterAuthNextJs } from "./index.js";
|
|
3
|
+
|
|
4
|
+
const SITE_URL = "https://test.convex.site";
|
|
5
|
+
const CONVEX_URL = "https://test.convex.cloud";
|
|
6
|
+
|
|
7
|
+
const setup = () => {
|
|
8
|
+
const { handler } = convexBetterAuthNextJs({
|
|
9
|
+
convexUrl: CONVEX_URL,
|
|
10
|
+
convexSiteUrl: SITE_URL,
|
|
11
|
+
});
|
|
12
|
+
const fetchSpy = vi
|
|
13
|
+
.spyOn(globalThis, "fetch")
|
|
14
|
+
.mockResolvedValue(new Response());
|
|
15
|
+
return { handler, fetchSpy };
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const initOf = (spy: ReturnType<typeof vi.spyOn>): RequestInit =>
|
|
19
|
+
(spy.mock.calls[0]?.[1] as RequestInit) ?? {};
|
|
20
|
+
|
|
21
|
+
const headersOf = (spy: ReturnType<typeof vi.spyOn>): Headers =>
|
|
22
|
+
new Headers(initOf(spy).headers);
|
|
23
|
+
|
|
24
|
+
describe("convexBetterAuthNextJs handler", () => {
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
vi.restoreAllMocks();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("strips hop-by-hop headers from the forwarded request", async () => {
|
|
30
|
+
const { handler, fetchSpy } = setup();
|
|
31
|
+
const request = new Request(
|
|
32
|
+
"https://app.example.com/api/auth/sign-in/email",
|
|
33
|
+
{
|
|
34
|
+
method: "POST",
|
|
35
|
+
headers: {
|
|
36
|
+
"transfer-encoding": "chunked",
|
|
37
|
+
"content-length": "42",
|
|
38
|
+
connection: "keep-alive",
|
|
39
|
+
"content-type": "application/json",
|
|
40
|
+
},
|
|
41
|
+
body: JSON.stringify({ email: "test@example.com" }),
|
|
42
|
+
}
|
|
43
|
+
);
|
|
44
|
+
await handler.POST(request);
|
|
45
|
+
const headers = headersOf(fetchSpy);
|
|
46
|
+
expect(headers.get("transfer-encoding")).toBeNull();
|
|
47
|
+
expect(headers.get("content-length")).toBeNull();
|
|
48
|
+
expect(headers.get("connection")).toBeNull();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("forwards to upstream URL preserving path and query", async () => {
|
|
52
|
+
const { handler, fetchSpy } = setup();
|
|
53
|
+
const request = new Request(
|
|
54
|
+
"https://app.example.com/api/auth/sign-in/email?foo=bar",
|
|
55
|
+
{ method: "POST", body: "{}" }
|
|
56
|
+
);
|
|
57
|
+
await handler.POST(request);
|
|
58
|
+
expect(fetchSpy.mock.calls[0]?.[0]).toBe(
|
|
59
|
+
`${SITE_URL}/api/auth/sign-in/email?foo=bar`
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("sets host and forwarding headers", async () => {
|
|
64
|
+
const { handler, fetchSpy } = setup();
|
|
65
|
+
const request = new Request(
|
|
66
|
+
"https://app.example.com/api/auth/sign-in/email",
|
|
67
|
+
{ method: "POST", body: "{}" }
|
|
68
|
+
);
|
|
69
|
+
await handler.POST(request);
|
|
70
|
+
const headers = headersOf(fetchSpy);
|
|
71
|
+
expect(headers.get("host")).toBe(new URL(SITE_URL).host);
|
|
72
|
+
expect(headers.get("x-forwarded-host")).toBe("app.example.com");
|
|
73
|
+
expect(headers.get("x-forwarded-proto")).toBe("https");
|
|
74
|
+
expect(headers.get("x-better-auth-forwarded-host")).toBe("app.example.com");
|
|
75
|
+
expect(headers.get("x-better-auth-forwarded-proto")).toBe("https");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("buffers POST body and forwards as ArrayBuffer", async () => {
|
|
79
|
+
const { handler, fetchSpy } = setup();
|
|
80
|
+
const body = JSON.stringify({ email: "test@example.com" });
|
|
81
|
+
const request = new Request(
|
|
82
|
+
"https://app.example.com/api/auth/sign-in/email",
|
|
83
|
+
{ method: "POST", body }
|
|
84
|
+
);
|
|
85
|
+
await handler.POST(request);
|
|
86
|
+
const init = initOf(fetchSpy);
|
|
87
|
+
expect(init.body).toBeInstanceOf(ArrayBuffer);
|
|
88
|
+
expect(new TextDecoder().decode(init.body as ArrayBuffer)).toBe(body);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("does not set body for GET requests", async () => {
|
|
92
|
+
const { handler, fetchSpy } = setup();
|
|
93
|
+
const request = new Request(
|
|
94
|
+
"https://app.example.com/api/auth/get-session",
|
|
95
|
+
{ method: "GET" }
|
|
96
|
+
);
|
|
97
|
+
await handler.GET(request);
|
|
98
|
+
expect(initOf(fetchSpy).body).toBeUndefined();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("does not set body for empty POST", async () => {
|
|
102
|
+
const { handler, fetchSpy } = setup();
|
|
103
|
+
const request = new Request("https://app.example.com/api/auth/sign-out", {
|
|
104
|
+
method: "POST",
|
|
105
|
+
});
|
|
106
|
+
await handler.POST(request);
|
|
107
|
+
expect(initOf(fetchSpy).body).toBeUndefined();
|
|
108
|
+
});
|
|
109
|
+
});
|
package/src/nextjs/index.ts
CHANGED
|
@@ -40,17 +40,36 @@ const parseConvexSiteUrl = (url: string) => {
|
|
|
40
40
|
return url;
|
|
41
41
|
};
|
|
42
42
|
|
|
43
|
-
const handler = (request: Request, siteUrl: string) => {
|
|
43
|
+
const handler = async (request: Request, siteUrl: string) => {
|
|
44
44
|
const requestUrl = new URL(request.url);
|
|
45
45
|
const nextUrl = `${siteUrl}${requestUrl.pathname}${requestUrl.search}`;
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
46
|
+
|
|
47
|
+
const headers = new Headers(request.headers);
|
|
48
|
+
// Strip hop-by-hop headers; undici rejects outbound `transfer-encoding: chunked`.
|
|
49
|
+
headers.delete("transfer-encoding");
|
|
50
|
+
headers.delete("content-length");
|
|
51
|
+
headers.delete("connection");
|
|
52
|
+
headers.set("accept-encoding", "application/json");
|
|
53
|
+
headers.set("host", new URL(siteUrl).host);
|
|
54
|
+
headers.set("x-forwarded-host", requestUrl.host);
|
|
55
|
+
headers.set("x-forwarded-proto", requestUrl.protocol.replace(/:$/, ""));
|
|
56
|
+
headers.set("x-better-auth-forwarded-host", requestUrl.host);
|
|
57
|
+
headers.set("x-better-auth-forwarded-proto", requestUrl.protocol.replace(/:$/, ""));
|
|
58
|
+
|
|
59
|
+
const init: RequestInit = {
|
|
60
|
+
headers,
|
|
61
|
+
method: request.method,
|
|
62
|
+
redirect: "manual",
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
if (request.method !== "GET" && request.method !== "HEAD") {
|
|
66
|
+
const body = await request.arrayBuffer();
|
|
67
|
+
if (body.byteLength > 0) {
|
|
68
|
+
init.body = body;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return fetch(nextUrl, init);
|
|
54
73
|
};
|
|
55
74
|
|
|
56
75
|
const nextJsHandler = (siteUrl: string) => ({
|