Developing sophisticated search entities through coding and inputting diverse search attributes

Exploring New Territory

In the process of developing a Next app, I'm delving into the realm of advanced search forms. To streamline this task, I've stumbled upon a handy library called search-query-parser. With the magic of search-query-parser, an input string like

const input = "foo bar -baz answer:42"
can be transformed into a structured object such as
const output = { text: ["foo", "bar"], answer: ["42"], exclude: { text: ["baz"] } }
. This library enables the definition of keywords and ranges in array form, facilitating the construction of the output object. Additionally, the usage of zod is incorporated.

The next step involves converting this output object into a fully typed entity that can be tailored for diverse search forms. The ultimate object will encompass standard properties for pagination control and format rendering. It will preserve both the raw input string and the meticulously parsed and typed conditions extracted from the search query.

A Solution in Progress

import parser, { SearchParserResult } from "search-query-parser";
import { z } from "zod";

const UNPARSED_QUERY_WITH_DEFAULTS = z.object({
    q: z.string().describe("serialized search query").optional().default(""),
    o: z.coerce.number().describe("offset").optional().default(0),
    l: z.coerce
        .number()
        .describe(
            "limit - determines how many results per page (clamp to max value)"
        )
        .optional()
        .default(0),
    e: z.coerce
        .boolean()
        .describe(
            "expand - determines if result set should be expanded to one record per version or collapsed to group multiple versions with the same values"
        )
        .optional()
        .default(false),
    v: z.enum(["list", "table"]).describe("view").optional().default("list")
});

type UnparsedSearchParams = z.infer<typeof UNPARSED_QUERY_WITH_DEFAULTS>;

type Keywords = readonly [string, ...Array<string>];

type Ranges = readonly [string, ...Array<string>];

type RawQuery<
    TKeywords extends Keywords = Keywords,
    TRanges extends Ranges = Ranges
> = SearchParserResult & {
    text?: string[];
} & Partial<Record<TKeywords[number], Array<string>>> &
    Partial<
        Record<
            TRanges[number],
            {
                from: string;
                to?: string | undefined;
            }
        >
    >;

// SEE https://stackoverflow.com/a/71912306/265558
type BulkRename<
    T extends Record<string, string>,
    U extends Record<string, unknown>
> = {
    [K in keyof U as K extends keyof T
        ? T[K] extends string
            ? T[K]
            : never
        : K]: K extends keyof U ? U[K] : never;
};

type Foo = { one: number; two: string; three: boolean };
type Bar = BulkRename<{ one: "first"; two: "second" }, Foo>;

type FullSearchQuery<
    TKeywords extends Keywords = Keywords,
    TRanges extends Ranges = Ranges,
    RenameMap extends Record<string, string> = {}
> = {
    include: Partial<
        BulkRename<
            RenameMap,
            Record<"text" | TKeywords[number] | TRanges[number], unknown>
        >
    >;
    exclude?: Partial<
        BulkRename<
            RenameMap,
            Record<"text" | TKeywords[number] | TRanges[number], unknown>
        >
    >;
};

const buildSearchQuery = <
    RenameMap extends Record<string, string> = {},
    TKeywords extends Keywords = Keywords,
    TRanges extends Ranges = Ranges,
    ExtractConditions extends (
        rawQuery: RawQuery<TKeywords, TRanges>
    ) => FullSearchQuery<TKeywords, TRanges, RenameMap> = (
        rawQuery: RawQuery<TKeywords, TRanges>
    ) => FullSearchQuery<TKeywords, TRanges, RenameMap>
>(
    searchParams: UnparsedSearchParams,
    keywords: TKeywords,
    ranges: TRanges,
    extractConditions: ExtractConditions
) => {
    const query = parser.parse(searchParams.q, {
        offsets: true,
        tokenize: true,
        keywords: [...keywords],
        ranges: [...ranges],
        alwaysArray: true
    }) as RawQuery<TKeywords, TRanges>;

    return {
        ...searchParams,
        conditions: extractConditions(query),
        query
    };
};

const KEYWORDS = [
    "edition",
    "version",
    "is-earliest-in-cycle",
    "is-latest-in-cycle",
    "is-latest"
] as const;

const RANGES = ["cycle"] as const;

const BOOL = z.coerce.boolean().optional();

const releaseConditions = (
    rawQuery: SearchParserResult & {
        text?: string[];
    } & Partial<Record<(typeof KEYWORDS)[number], Array<string>>> &
        Partial<
            Record<
                (typeof RANGES)[number],
                {
                    from: string;
                    to?: string | undefined;
                }
            >
        >
) => ({
    include: {
        text: rawQuery.text,
        edition: rawQuery.edition,
        version: rawQuery.version,
        cycle: rawQuery.cycle,
        isEarliestInCycle: BOOL.parse(
            rawQuery["is-earliest-in-cycle"]?.[
                rawQuery["is-earliest-in-cycle"].length - 1
            ]
        ),
        isLatestInCycle: BOOL.parse(
            rawQuery["is-latest-in-cycle"]?.[
                rawQuery["is-latest-in-cycle"].length - 1
            ]
        ),
        isLatest: BOOL.parse(
            rawQuery["is-latest"]?.[rawQuery["is-latest"].length - 1]
        )
    },
    exclude: rawQuery.exclude
});

type Params = { [key: string]: string | Array<string> };

interface PageProps {
    params?: Params;
    searchParams?: { [key: string]: string | Array<string> | undefined };
}

const searchParams: PageProps["searchParams"] = {};

const searchParamsWithDefaults =
    UNPARSED_QUERY_WITH_DEFAULTS.parse(searchParams);

const releaseQuery = buildSearchQuery<{
    "is-earliest-in-cycle": "isEarliestInCycle";
    "is-latest-in-cycle": "isLatestInCycle";
    "is-latest": "isLatest";
}>(searchParamsWithDefaults, KEYWORDS, RANGES, releaseConditions);

releaseQuery.conditions.include;
//                      ^?

releaseQuery.conditions.include.version;
//.                             ^?

releaseQuery.conditions.include.cycle;
//.                             ^?

releaseQuery.conditions.include.isLatest;
//.                             ^?

releaseQuery.conditions.exclude;
//                      ^?

Culminating at the end of the code are various type checks. Despite what console.log reveals regarding the runtime behavior within my app, the validity of releaseQuery stands firm. It serves as a prototype of the desired search object structure. The configuration and actual input contribute to the customized shape of releaseQuery.conditions.include and releaseQuery.conditions.exclude.

Seeking Assistance

My struggle primarily lies in typing the final output object, particularly with respect to the properties under releaseQuery.conditions.include.

  1. The property names originate from the input keywords and ranges. Although I came across a property renaming type based on a simple mapping, it doesn't function correctly. Ideally, the ultimate property names should mirror the input to avoid unnecessary rebranding.
  2. Currently, property names do not auto-populate. Previously, they did auto-complete, but suggestions were aligned with keywords and ranges, rather than reflecting real options.
  3. All property values are classified as unknown. My objective is for these values to be inferred based on the actual output of releaseConditions.
  4. releaseConditions ought to be typed to mandate the presence of include and exclude properties. In case an explicit property renaming type parameter is necessary, it should guide the nomenclature of these two properties.

That's all my overworked mind can conjure up at the moment. After refining this project tirelessly for days, and especially several hours today, I'm reaching out for support. If you have any clarifications to seek or questions to pose, feel free to ask. Thank you!

Answer №1

I have successfully achieved my goal by customizing property names for each form. With this approach, I benefit from complete type safety and auto-complete features internally with SEACH_QUERY and releaseInclude, as well as externally when working with the final object.

import parser, { SearchParserResult } from "search-query-parser";
import { z } from "zod";

type Keywords = readonly [string, ...Array<string>];

type Ranges = readonly [string, ...Array<string>];

type RawQuery<
    TKeywords extends Keywords = Keywords,
    TRanges extends Ranges = Ranges
> = SearchParserResult & {
    text?: string[];
} & Partial<Record<TKeywords[number], Array<string>>> &
    Partial<
        Record<
            TRanges[number],
            {
                from: string;
                to?: string | undefined;
            }
        >
    >;

type Include = {
    text?: Array<string>;
    [key: string]: unknown;
};

export const SEACH_QUERY = <
    TKeywords extends Keywords = Keywords,
    TRanges extends Ranges = Ranges,
    TInclude extends Include = Include
>(
    keywords: TKeywords,
    ranges: TRanges,
    extractInclude: (rawQuery: RawQuery<TKeywords, TRanges>) => TInclude
) =>
    z
        .object({
            q: z.string().optional().default("").catch(""),
            o: z.coerce
                .number()
                .nonnegative()
                .int()
                .optional()
                .default(0)
                .catch(0),
            l: z.coerce
                .number()
                .nonnegative()
                .int()
                .optional()
                .default(0)
                .catch(0),
            e: z.coerce.boolean().optional().default(false).catch(false),
            v: z
                .enum(["list", "table"])
                .optional()
                .default("list")
                .catch("list")
        })
        .transform(({ q, o, l, e, v }) => {
            const query = parser.parse(q, {
                offsets: true,
                tokenize: true,
                keywords: [...keywords],
                ranges: [...ranges],
                alwaysArray: true
            }) as RawQuery<TKeywords, TRanges>;

            return {
                query: q,
                offset: o,
                limit: l,
                expand: e,
                view: v,
                conditions: {
                    include: extractInclude(query),
                    exclude: query.exclude
                }
            };
        });

export const KEYWORDS = [
    "edition",
    "version",
    "is-earliest-in-cycle",
    "is-latest-in-cycle",
    "is-latest"
] as const;

export const RANGES = ["cycle"] as const;

const BOOL = z.coerce.boolean().optional();

export const releaseInclude = (
    rawQuery: SearchParserResult & {
        text?: string[];
    } & Partial<Record<(typeof KEYWORDS)[number], Array<string>>> &
        Partial<
            Record<
                (typeof RANGES)[number],
                {
                    from: string;
                    to?: string | undefined;
                }
            >
        >
) => ({
    text: rawQuery.text,
    edition: rawQuery.edition,
    version: rawQuery.version,
    cycle: rawQuery.cycle,
    isEarliestInCycle: BOOL.parse(
        rawQuery["is-earliest-in-cycle"]?.[
            rawQuery["is-earliest-in-cycle"].length - 1
        ]
    ),
    isLatestInCycle: BOOL.parse(
        rawQuery["is-latest-in-cycle"]?.[
            rawQuery["is-latest-in-cycle"].length - 1
        ]
    ),
    isLatest: BOOL.parse(
        rawQuery["is-latest"]?.[rawQuery["is-latest"].length - 1]
    )
});

/** Customizing Nextjs search params */
const searchParameters: { [key: string]: string | Array<string> | undefined } | undefined = {};

const customizedQuery = SEACH_QUERY(KEYWORDS, RANGES, releaseInclude).parse(searchParameters);

customizedQuery.conditions.include;
//                      ^?

customizedQuery.conditions.include.version;
//                              ^?

customizedQuery.conditions.include.cycle;
//                              ^?

customizedQuery.conditions.include.isLatest;
//                              ^?

customizedQuery.conditions.exclude;
//                      ^?

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Azure Active Directory B2C integration with Next.js using next-auth.js encounters an issue with an unregistered redirect URI

I came across the issue of "redirect_uri_mismatch" while using next-auth.js for Azure AD B2C sign-in. The error message states: "The redirect URI 'http://localhost:3000/api/auth/callback/azure-ad-b2c' provided in the request is not registered for ...

What is the correct way to handle errors in TypeScript when using Axios?

I've got this code snippet that's currently working: try { self.loadingFrameMarkup = true; const { data }: AxiosResponse<IMarkupFrameData> = yield axios.post<IMarkupFrameData>( ...

Is there a way to convey a function existing on the Server Component to the Client Component?

Seeking assistance with NEXTJS 13 'app Dir' Is there a way to transfer a function from the Server Component to the Client Component? I have a Button as my Client component and it is wrapped by another Server Component. I want the Button to exec ...

What is the best way to incorporate my own custom component into the Mui Theme options in order to efficiently modify it?

I've been grappling with this issue for a while now and I just can't seem to get past these type errors. It feels like there's a crucial piece of the puzzle that I'm missing. My goal is to develop my own custom component and then have ...

Typescript type definitions - understanding inheritance

My typescript interface defines the structure of my database data as follows: interface Foo { bar: { fish: { _id: string, name: string, }[], }, starwars: string[], } I want to be able to reference specific parts of this inter ...

`Switching from Fetch to Axios: A step-by-step guide`

Currently in the process of refactoring some code and need to transition from using fetch to axios. Here's the original code snippet: const createAttachment = async (formData: FormData): Promise<boolean | string> => { try { const respon ...

How can I create dynamic columns in an Angular Kendo Grid with Typescript in a Pivot table format?

Is there a way to generate dynamic columns in Angular 4 and higher with Kendo Grid using TypeScript, similar to a pivot style? I attempted to use the Kendo Auto-Generated column examples available on the Telerik/Progress website. You can find the sample ...

Is there a way for me to retrieve the callback parameters?

Can the parameters of the callback function be accessed within the 'outer' function? function f(callback: (par1: string)=>void): void { // Is it possible to access 'par1' here? } ...

Showing data related to a certain value - what's the best way?

I am currently working on a page where I need to display and edit specific user information at /users/524.json. However, I also want to include the working days from another table on the same page. I have provided most of the code below for reference. Unfo ...

After updating to Angular 10, the class constructor can only be called using the 'new' keyword

After following the official procedure at update.angular.io, I successfully upgraded my app from Angular 9 to Angular 10 using ng update. However, upon completion, I encountered numerous errors such as: ERROR Error: Uncaught (in promise): TypeError: Cl ...

Guide on creating proxy functions with parameter tuples for complex functions in TypeScript

I am currently in the process of converting a JavaScript library to TypeScript and need to define proxy functions. These functions should be able to accept any type and number of parameters and pass them to the original function like so: async function any ...

Utilize generic typings to interact with the Array object

I'm facing a challenge in developing an interface that is dependent on another interface. Everything was going smoothly until I reached the Array of Objects. Let me elaborate, I have an 'Entity' that defines how a document is stored in a ...

When utilizing Typescript with React Reduxjs toolkit, there seems to be an issue with reading the state in useSelector. An error message is displayed indicating that the variable loggedIn

I have encountered an error while passing a state from the store.tsx file to the component. The issue lies in the const loggedIn where state.loggedIn.loggedIn is not recognized as a boolean value. Below is the code snippet for the component: import React ...

Changes trigger the re-rendering of inputs

My modal is causing all inputs to be re-rendered with every change I make while typing. I have a Formik object set up like this: const formik = useFormik({ enableReinitialize: true, initialValues: { sp_id: data?.id, ...

Importing JSON files dynamically in Typescript with Quasar/Vue using different variable names

Currently, I am in the process of developing an application utilizing the Quasar framework with Typescript. Managing a large number of JSON files for dynamic import has proven to be quite challenging due to the impracticality of manually importing hundreds ...

What are the two different ways to declare a property?

I am trying to update my interface as shown below interface Student{ Name: String; age: Number; } However, instead of the current structure, I would like it to be like this interface Student{ Name: String; age | DOB: Number | Date; } This means t ...

Sending a parameter through a route to a child component as an input in Angular 2

My parent component receives an id value from a route parameter and I need to pass this value to a child component using the Input() decorator. The issue I'm facing is that I can't seem to get the route param value to be passed to the child compo ...

Fetch the preview URL generated by Vercel in Nextjs

Hello there! I'm currently in the process of deploying a Nextjs 13 app to Vercel, and I'm looking for a way to obtain the Preview generated URL directly from my code. According to Vercel's documentation: Whenever a new deployment is initia ...

Encountering an issue with testing CKEditor in Jest

Testing my project configured with vite (Typescript) and using jest showed an error related to ckeditor. The error is displayed as follows: [![enter image description here][1]][1] The contents of package.json: { "name": "test-project" ...

The impact of placing a method in the constructor versus outside it within a class (yielding identical outcomes)

These two code snippets appear to produce the same result. However, I am curious about the differences between the two approaches and when it would be more appropriate to use one over the other. Can someone provide insight into this matter? Snippet 1: c ...