#Dynamic query build

18 messages · Page 1 of 1 (latest)

green moss
#

export const getBookings = query({
args: {
studentId: v.optional(v.id("users")),
tutorId: v.optional(v.id("users")),
status: v.optional(
v.union(
v.literal(BookingStatus.AWAITING_TUTOR_CONFIRMATION),
v.literal(BookingStatus.AWAITING_STUDENT_CONFIRMATION),
v.literal(BookingStatus.AWAITING_PAYMENT),
v.literal(BookingStatus.PAYMENT_FAILED),
v.literal(BookingStatus.SCHEDULED),
v.literal(BookingStatus.CANCELED),
v.literal(BookingStatus.COMPLETED)
)
),
startDate: v.optional(v.number()), // Unix timestamp in milliseconds
endDate: v.optional(v.number()), // Unix timestamp in milliseconds
},
handler: async (ctx, args) => {
let query = ctx.db.query("bookings");

if (args.studentId) {
  query = query.withIndex("by_student", (q) =>
    q.eq("student", args.studentId!)
  );
}

if (args.tutorId) {
  query = query.withIndex("by_tutor", (q) => q.eq("tutor", args.tutorId!));
}

if (args.status) {
  query = query.withIndex("by_status", (q) => q.eq("status", args.status!));
}

if (args.startDate !== undefined) {
  query = query.filter((q) => q.gte(q.field("startTime"), args.startDate!));
}

if (args.endDate !== undefined) {
  query = query.filter((q) => q.lte(q.field("startTime"), args.endDate!));
}

return await query.collect();

},
});

schema
bookings: defineTable({
student: v.id("users"), // Reference to the student (user with role STUDENT)
tutor: v.id("users"), // Reference to the tutor (user with role TUTOR)
service: v.id("services"), // Reference to the service being booked
type: v.union(
v.literal(BookingType.FREE_MEETING),
v.literal(BookingType.LESSON)
), // Booking type
status: v.union(
v.literal(BookingStatus.AWAITING_TUTOR_CONFIRMATION),
v.literal(BookingStatus.AWAITING_STUDENT_CONFIRMATION),
v.literal(BookingStatus.AWAITING_PAYMENT),
v.literal(BookingStatus.PAYMENT_FAILED),
v.literal(BookingStatus.SCHEDULED),
v.literal(BookingStatus.CANCELED),
v.literal(BookingStatus.COMPLETED)
), // Booking status
startTime: v.number(), // Start time of the booking (timestamp)
endTime: v.number(), // End time of the booking (timestamp)
createdAt: v.number(), // Creation time of the booking (timestamp)
updatedAt: v.number(), // Last updated time of the booking (timestamp)
})
.index("by_student", ["student", "startTime"])
.index("by_tutor", ["tutor", "startTime"])
.index("by_status", ["status"]),

I want to build a dynamic query build based on the query args if not nullable.

I'm getting this error
Type 'Query<{ document: { _id: Id<"bookings">; _creationTime: number; type: BookingType; tutor: Id<"users">; student: Id<"users">; service: Id<"services">; status: BookingStatus; startTime: number; endTime: number; createdAt: number; updatedAt: number; }; fieldPaths: ExtractFieldPaths<...> | "_id"; indexes: { ...; }; sear...' is missing the following properties from type 'QueryInitializer<{ document: { _id: Id<"bookings">; _creationTime: number; type: BookingType; tutor: Id<"users">; student: Id<"users">; service: Id<"services">; status: BookingStatus; startTime: number; endTime: number; createdAt: number; updatedAt: number; }; fieldPaths: ExtractFieldPaths<...> | "_id"; indexes: { ....': fullTableScan, withIndex, withSearchIndexts(2739)

acoustic wingBOT
#

Hi @green moss. I'm kapa.ai, your friendly support bot. I will try to answer your question by searching through the knowledge base. :wave:

#

The error you're encountering is related to TypeScript's type inference when building dynamic queries in Convex. This is a common issue when trying to conditionally apply filters and indexes to a query. Let's address this problem and provide a solution based on the knowledge sources.

To resolve this issue, you can use a pattern that involves explicitly typing your query variables and using conditional logic to build your query. Here's how you can modify your code:

import { QueryInitializer } from "./_generated/server";

export const getBookings = query({
  args: {
    // ... your existing args ...
  },
  handler: async (ctx, args) => {
    let preIndexQuery = ctx.db.query("bookings");
    let postIndexQuery: QueryInitializer<{
      document: Doc<"bookings">;
      fieldPaths: string;
      indexes: {
        by_student: ["student", "startTime"];
        by_tutor: ["tutor", "startTime"];
        by_status: ["status"];
      };
    }> = preIndexQuery;

    if (args.studentId) {
      postIndexQuery = preIndexQuery.withIndex("by_student", (q) =>
        q.eq("student", args.studentId)
      );
    } else if (args.tutorId) {
      postIndexQuery = preIndexQuery.withIndex("by_tutor", (q) =>
        q.eq("tutor", args.tutorId)
      );
    } else if (args.status) {
      postIndexQuery = preIndexQuery.withIndex("by_status", (q) =>
        q.eq("status", args.status)
      );
    }

    let query = postIndexQuery;

    if (args.startDate !== undefined) {
      query = query.filter((q) => q.gte(q.field("startTime"), args.startDate!));
    }

    if (args.endDate !== undefined) {
      query = query.filter((q) => q.lte(q.field("startTime"), args.endDate!));
    }

    return await query.collect();
  },
});
#

This approach is based on the pattern described in the Complex filters in Convex (https://stack.convex.dev/complex-filters-in-convex#combining-indexes-with-typescript-filters) article. It involves:

  1. Creating a preIndexQuery variable to hold the initial query.
  2. Creating a postIndexQuery variable with an explicit type that includes all possible indexes.
  3. Applying the index-based filters conditionally.
  4. Applying the non-index filters after the index-based filters.

This pattern should resolve the TypeScript error you're encountering while still allowing you to build a dynamic query based on the provided arguments.

Note that this approach uses only one index at a time. If you need to combine multiple indexes, you might need to consider denormalizing your data or using a different query structure. For more complex queries, you might want to explore the denormalizing properties (https://stack.convex.dev/complex-filters-in-convex#denormalizing-properties) approach mentioned in the knowledge sources.

Remember to import the necessary types from your Convex generated files and adjust the type definitions according to your specific schema if needed.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@green moss Kapa.ai is still learning and improving, please let me know how I did by reacting below.

green moss
#

@acoustic wing It keeps with type error

acoustic wingBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

I apologize for the continued type error. Let's try a different approach that has been shown to work in similar situations. Based on the knowledge sources, we can use the filter helper function from the "convex-helpers" package to build a more flexible dynamic query. Here's how you can modify your code:

import { filter } from "convex-helpers/server/filter";

export const getBookings = query({
  args: {
    // ... your existing args ...
  },
  handler: async (ctx, args) => {
    let query = ctx.db.query("bookings");

    if (args.studentId) {
      query = query.withIndex("by_student", (q) => q.eq("student", args.studentId));
    } else if (args.tutorId) {
      query = query.withIndex("by_tutor", (q) => q.eq("tutor", args.tutorId));
    } else if (args.status) {
      query = query.withIndex("by_status", (q) => q.eq("status", args.status));
    }

    return filter(query, (booking) => {
      if (args.startDate !== undefined && booking.startTime < args.startDate) {
        return false;
      }
      if (args.endDate !== undefined && booking.startTime > args.endDate) {
        return false;
      }
      return true;
    }).collect();
  },
});

This approach uses the filter helper function from "convex-helpers", which allows you to attach a custom filter to any query without TypeScript errors Complex filters in Convex (https://stack.convex.dev/complex-filters-in-convex#filter-too-complex-do-it-in-typescript). The filter function works with indexes and can handle complex filtering logic.

Here's what this code does:

  1. It starts with the base query on the "bookings" table.
  2. It applies the appropriate index based on the provided arguments (studentId, tutorId, or status).
  3. It then uses the filter helper to apply additional filtering based on the startDate and endDate arguments.
#

This approach should resolve the TypeScript errors while still allowing you to build a dynamic query based on the provided arguments. If you're still encountering issues, please provide more details about the specific error message you're seeing.

Remember to install the "convex-helpers" package if you haven't already:

npm install convex-helpers

or

yarn add convex-helpers

If you're still facing issues, it might be helpful to create a minimal reproducible example and consider opening an issue on the Convex GitHub repository for more specific assistance.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@green moss Kapa.ai is still learning and improving, please let me know how I did by reacting below.

green moss
#

why this ts error happens?

Type 'Query<{ document: { _id: Id<"bookings">; _creationTime: number; type: BookingType; tutor: Id<"users">; student: Id<"users">; service: Id<"services">; status: BookingStatus; startTime: number; endTime: number; createdAt: number; updatedAt: number; }; fieldPaths: ExtractFieldPaths<...> | "_id"; indexes: { ...; }; sear...' is missing the following properties from type 'QueryInitializer<{ document: { _id: Id<"bookings">; _creationTime: number; type: BookingType; tutor: Id<"users">; student: Id<"users">; service: Id<"services">; status: BookingStatus; startTime: number; endTime: number; createdAt: number; updatedAt: number; }; fieldPaths: ExtractFieldPaths<...> | "_id"; indexes: { ....': fullTableScan, withIndex, withSearchIndexts(2739)

#

@acoustic wing

acoustic wingBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

Sorry @green moss, I seem to be having a bit of trouble. Don't worry though, I've pinged my support and they will try to fix me up! :technologist:

Perhaps try again in 15mins?

green moss
#

@acoustic wing why this error happens?"

Type 'Query<{ document: { _id: Id<"bookings">; _creationTime: number; type: BookingType; tutor: Id<"users">; student: Id<"users">; service: Id<"services">; status: BookingStatus; startTime: number; endTime: number; createdAt: number; updatedAt: number; }; fieldPaths: ExtractFieldPaths<...> | "_id"; indexes: { ...; }; sear...' is missing the following properties from type 'QueryInitializer<{ document: { _id: Id<"bookings">; _creationTime: number; type: BookingType; tutor: Id<"users">; student: Id<"users">; service: Id<"services">; status: BookingStatus; startTime: number; endTime: number; createdAt: number; updatedAt: number; }; fieldPaths: ExtractFieldPaths<...> | "_id"; indexes: { ....': fullTableScan, withIndex, withSearchIndexts(2739)

acoustic wingBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

This error occurs due to TypeScript's type inference when building dynamic queries in Convex. The error message suggests that the type of the query you're building is missing some properties that are expected to be present in a QueryInitializer.

The root cause of this error is that when you conditionally modify a query (e.g., by adding filters or using different indexes), TypeScript loses track of the exact type of the query at each step. This is because the type of the query changes with each operation, and TypeScript can't infer the correct type through conditional logic.

According to the Convex documentation on the QueryInitializer interface (https://docs.convex.dev/api/interfaces/server.QueryInitializer), a QueryInitializer is the entry point for building a Query over a Convex database table. It includes methods like fullTableScan, withIndex, and withSearchIndex, which are mentioned in the error message as missing properties.

To resolve this issue, you typically need to explicitly type your query variables at different stages of query construction. This helps TypeScript understand the type of the query at each step, especially when dealing with conditional logic.

Here's a pattern that can help avoid this error, based on the Complex filters in Convex (https://stack.convex.dev/complex-filters-in-convex#combining-indexes-with-typescript-filters) article:

let preIndexQuery = ctx.db.query("bookings");
let postIndexQuery: QueryInitializer<{
  document: Doc<"bookings">;
  fieldPaths: string;
  indexes: {
    by_student: ["student", "startTime"];
    by_tutor: ["tutor", "startTime"];
    by_status: ["status"];
  };
}> = preIndexQuery;
#

// Apply index-based filters
if (args.studentId) {
postIndexQuery = preIndexQuery.withIndex("by_student", (q) =>
q.eq("student", args.studentId)
);
} else if (args.tutorId) {
postIndexQuery = preIndexQuery.withIndex("by_tutor", (q) =>
q.eq("tutor", args.tutorId)
);
} else if (args.status) {
postIndexQuery = preIndexQuery.withIndex("by_status", (q) =>
q.eq("status", args.status)
);
}

// Apply non-index filters
let query = postIndexQuery;
if (args.startDate !== undefined) {
query = query.filter((q) => q.gte(q.field("startTime"), args.startDate!));
}
if (args.endDate !== undefined) {
query = query.filter((q) => q.lte(q.field("startTime"), args.endDate!));
}


By explicitly typing `postIndexQuery` as a `QueryInitializer` with the correct structure, you help TypeScript understand the type of the query at each stage, which should resolve the error.

If this approach doesn't solve your issue, you might want to consider using the `filter` helper from the "convex-helpers" package, as mentioned in my previous response, which can handle more complex filtering scenarios without TypeScript errors.
----------------------
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
----------------------