Skip to content

Commit 02376a3

Browse files
NicolappsConvex, Inc.
authored andcommitted
[Explicit table names] Fix using IDs with a supertype table (#44408)
The new `GenericDatabaseReader`/`GenericDatabaseWriter` signatures added in 1.30.0 have a bug in their types: they calls like `db.get("table1", id)`, with `id: Id<"table1"> | Id<"table2">`, incorrectly type check. Thanks for termosa on Discord for reporting the bug (https://discord.com/channels/1019350475847499849/1448756823107702805/1448781976466227250)! ### Note about making a breaking change Changing the TypeScript types can be a breaking change in rare cases. It affects users that have custom implementations of GenericDatabaseWriter/Reader in their codebase. However, this change is simple to apply, and it is only necessary for users that have custom GenericDatabaseWriter/Reader implementations _and_ have already updated to 1.31.0. I will update convex-test and convex-helpers after 1.31.2 is released to update the types they use. GitOrigin-RevId: 9b9c9b6ba96eec6e959cd3eba06ef4a3e3a122d7
1 parent 04c7058 commit 02376a3

File tree

3 files changed

+99
-13
lines changed

3 files changed

+99
-13
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# Changelog
22

3+
## 1.31.2 (upcoming)
4+
5+
- Bug fix: the TypeScript types of the new `ctx.db` APIs introduced in 1.31.0
6+
incorrectly allowed passing IDs with types broader than the table name
7+
argument (e.g. `db.get("table1", id)` where `id` is
8+
`Id<"table1"> | Id<"table2">`). This issue is fixed in 1.31.2.
9+
310
## 1.31.0
411

512
- `db.get`, `db.patch`, `db.replace`, and `db.delete` now accept a table name as

src/server/database.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ interface BaseDatabaseReader<DataModel extends GenericDataModel> {
2121
* @returns - The {@link GenericDocument} of the document at the given {@link values.GenericId}, or `null` if it no longer exists.
2222
*/
2323
get<TableName extends TableNamesInDataModel<DataModel>>(
24-
table: NonUnion<TableName>,
25-
id: GenericId<TableName>,
24+
table: TableName,
25+
id: GenericId<NonUnion<TableName>>,
2626
): Promise<DocumentByName<DataModel, TableName> | null>;
2727

2828
/**
@@ -185,8 +185,8 @@ export interface GenericDatabaseWriter<DataModel extends GenericDataModel>
185185
* specifies system fields like `_id`, they must match the document's existing field values.
186186
*/
187187
patch<TableName extends TableNamesInDataModel<DataModel>>(
188-
table: NonUnion<TableName>,
189-
id: GenericId<TableName>,
188+
table: TableName,
189+
id: GenericId<NonUnion<TableName>>,
190190
value: PatchValue<DocumentByName<DataModel, TableName>>,
191191
): Promise<void>;
192192

@@ -215,8 +215,8 @@ export interface GenericDatabaseWriter<DataModel extends GenericDataModel>
215215
* and the database will fill them in.
216216
*/
217217
replace<TableName extends TableNamesInDataModel<DataModel>>(
218-
table: NonUnion<TableName>,
219-
id: GenericId<TableName>,
218+
table: TableName,
219+
id: GenericId<NonUnion<TableName>>,
220220
value: WithOptionalSystemFields<DocumentByName<DataModel, TableName>>,
221221
): Promise<void>;
222222

@@ -239,8 +239,8 @@ export interface GenericDatabaseWriter<DataModel extends GenericDataModel>
239239
* @param id - The {@link values.GenericId} of the document to remove.
240240
*/
241241
delete<TableName extends TableNamesInDataModel<DataModel>>(
242-
table: NonUnion<TableName>,
243-
id: GenericId<TableName>,
242+
table: TableName,
243+
id: GenericId<NonUnion<TableName>>,
244244
): Promise<void>;
245245

246246
/**

src/server/database_api.test.ts

Lines changed: 84 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,39 @@ describe("new DB APIs work with explicit table names", () => {
224224
});
225225
});
226226

227+
test("new DB APIs can be used with multiple tables at once", () => {
228+
type AllowedTableNames = "table1" | "table2";
229+
230+
const _schema = defineSchema({
231+
table1: defineTable({
232+
name: v.string(),
233+
}),
234+
table2: defineTable({
235+
name: v.string(),
236+
}),
237+
});
238+
type DataModel = DataModelFromSchemaDefinition<typeof _schema>;
239+
240+
async function _shouldTypecheck(
241+
tableName: AllowedTableNames,
242+
id: GenericId<AllowedTableNames>,
243+
) {
244+
const db: GenericDatabaseWriter<DataModel> = setupWriter();
245+
await db.get(tableName, id);
246+
await db.patch(tableName, id, {
247+
name: "test",
248+
});
249+
await db.insert(tableName, {
250+
name: "test",
251+
});
252+
await db.insert(tableName, {
253+
// @ts-expect-error -- field doesn’t exist on table1 or table2
254+
incorrect: "field",
255+
});
256+
await db.delete(tableName, id);
257+
}
258+
});
259+
227260
describe("new DB APIs don’t type check if used with the wrong table", () => {
228261
test("get", async () => {
229262
const db = setupWriter();
@@ -269,17 +302,16 @@ describe("new DB APIs don’t type check if used with the wrong table", () => {
269302
await db.replace(tableName, id, {});
270303
});
271304

272-
test("can use Id<string> + a specific table name", async () => {
273-
// Maybe we should disallow this??
274-
305+
test("can’t use Id<string> + a specific table name", async () => {
275306
const id = "my_id" as GenericId<string>;
276307

277308
const db = setupWriter();
309+
// @ts-expect-error
278310
await db.replace("testTable", id, {});
279311
});
280312

281313
test("can use some union types", async () => {
282-
const tableName: "a" | "b" = "a";
314+
const tableName = "a" as "a" | "b";
283315
const id = "my_id" as GenericId<"a" | "b">;
284316

285317
const db = setupWriter();
@@ -292,9 +324,56 @@ describe("new DB APIs don’t type check if used with the wrong table", () => {
292324

293325
const tableName: string = "some table";
294326

327+
// Unfortunately, the types accept `string` as a table name
328+
// when using a GenericDataModel, so let’s define a specific
329+
// data model for this type test
330+
const _schema = defineSchema({
331+
someTable: defineTable({
332+
name: v.string(),
333+
}),
334+
someTable2: defineTable({
335+
name: v.string(),
336+
}),
337+
});
338+
type DataModel = DataModelFromSchemaDefinition<typeof _schema>;
339+
340+
const db = setupWriter() as GenericDatabaseWriter<DataModel>;
341+
await db.replace(
342+
// @ts-expect-error -- `string` shouldn’t be a valid table name type!
343+
tableName,
344+
testId,
345+
{},
346+
);
347+
});
348+
349+
test("can’t use a table name + (Id<'a' | 'b'>)", async () => {
350+
const id = "my_id" as GenericId<"a" | "b">;
351+
352+
const db = setupWriter();
353+
354+
// @ts-expect-error
355+
await db.get("a", id);
356+
// @ts-expect-error
357+
await db.patch("a", id, {});
358+
// @ts-expect-error
359+
await db.replace("a", id, {});
360+
// @ts-expect-error
361+
await db.delete("a", id);
362+
});
363+
364+
test("can’t use a table name + (Id<'a'> | Id<'b'>)", async () => {
365+
const id = "my_id" as GenericId<"a"> | GenericId<"b">;
366+
295367
const db = setupWriter();
368+
369+
// @ts-expect-error
370+
await db.get("a", id);
371+
// @ts-expect-error
372+
await db.patch("a", id, {});
373+
// @ts-expect-error
374+
await db.replace("a", id, {});
296375
// @ts-expect-error
297-
await db.replace(tableName, testId, {});
376+
await db.delete("a", id);
298377
});
299378
});
300379

0 commit comments

Comments
 (0)