diff --git a/backend/drizzle/20261111212139_author_index/migration.sql b/backend/drizzle/20261111212139_author_index/migration.sql new file mode 100644 index 00000000..6bfb4f66 --- /dev/null +++ b/backend/drizzle/20261111212139_author_index/migration.sql @@ -0,0 +1 @@ +CREATE INDEX "books_author_trgm_idx" ON "books" USING gin ("author" gin_trgm_ops); \ No newline at end of file diff --git a/backend/drizzle/20261111212139_author_index/snapshot.json b/backend/drizzle/20261111212139_author_index/snapshot.json new file mode 100644 index 00000000..3352ccb5 --- /dev/null +++ b/backend/drizzle/20261111212139_author_index/snapshot.json @@ -0,0 +1,1446 @@ +{ + "version": "8", + "dialect": "postgres", + "id": "442822f4-8d37-421a-bce7-0c72b40d8c4b", + "prevIds": [ + "cc1b05d5-b0a3-484c-bc2c-f2a57c417942" + ], + "ddl": [ + { + "isRlsEnabled": false, + "name": "account", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "session", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "user", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "verification", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "book_genres", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "book_to_genres", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "books", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "favorite_books", + "entityType": "tables", + "schema": "public" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "account_id", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "provider_id", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "access_token", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "refresh_token", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id_token", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "access_token_expires_at", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "refresh_token_expires_at", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "scope", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "password", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expires_at", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "token", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ip_address", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_agent", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "email", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "email_verified", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "image", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "bio", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "verification" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "identifier", + "entityType": "columns", + "schema": "public", + "table": "verification" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "value", + "entityType": "columns", + "schema": "public", + "table": "verification" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expires_at", + "entityType": "columns", + "schema": "public", + "table": "verification" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "verification" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "verification" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": { + "type": "always", + "name": "book_genres_id_seq", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": 1, + "cycle": false + }, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "book_genres" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "genre", + "entityType": "columns", + "schema": "public", + "table": "book_genres" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "book_id", + "entityType": "columns", + "schema": "public", + "table": "book_to_genres" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "genre_id", + "entityType": "columns", + "schema": "public", + "table": "book_to_genres" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": { + "type": "always", + "name": "books_id_seq", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": 1, + "cycle": false + }, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "books" + }, + { + "type": "varchar(13)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "isbn", + "entityType": "columns", + "schema": "public", + "table": "books" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "title", + "entityType": "columns", + "schema": "public", + "table": "books" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "series", + "entityType": "columns", + "schema": "public", + "table": "books" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "author", + "entityType": "columns", + "schema": "public", + "table": "books" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "description", + "entityType": "columns", + "schema": "public", + "table": "books" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "language", + "entityType": "columns", + "schema": "public", + "table": "books" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "book_format", + "entityType": "columns", + "schema": "public", + "table": "books" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "edition", + "entityType": "columns", + "schema": "public", + "table": "books" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pages", + "entityType": "columns", + "schema": "public", + "table": "books" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "publisher", + "entityType": "columns", + "schema": "public", + "table": "books" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "publish_date", + "entityType": "columns", + "schema": "public", + "table": "books" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "publish_year", + "entityType": "columns", + "schema": "public", + "table": "books" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "cover_img_url", + "entityType": "columns", + "schema": "public", + "table": "books" + }, + { + "type": "real", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "rating", + "entityType": "columns", + "schema": "public", + "table": "books" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "num_ratings", + "entityType": "columns", + "schema": "public", + "table": "books" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "public", + "table": "favorite_books" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "book_id", + "entityType": "columns", + "schema": "public", + "table": "favorite_books" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "favorite_books" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "account_userId_idx", + "entityType": "indexes", + "schema": "public", + "table": "account" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "session_userId_idx", + "entityType": "indexes", + "schema": "public", + "table": "session" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "identifier", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "verification_identifier_idx", + "entityType": "indexes", + "schema": "public", + "table": "verification" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "genre_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "book_to_genres_genre_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "book_to_genres" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "language", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "books_language_idx", + "entityType": "indexes", + "schema": "public", + "table": "books" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "book_format", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "books_book_format_idx", + "entityType": "indexes", + "schema": "public", + "table": "books" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "rating", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + }, + { + "value": "num_ratings", + "isExpression": false, + "asc": false, + "nullsFirst": true, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "books_rating_num_ratings_idx", + "entityType": "indexes", + "schema": "public", + "table": "books" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "title", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "books_title_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "books" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "num_ratings", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "books_num_ratings_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "books" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "pages", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "books_pages_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "books" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "publish_year", + "isExpression": false, + "asc": false, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "books_publish_year_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "books" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "publish_year", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "books_publish_year_asc_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "books" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "title", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": { + "name": "gin_trgm_ops", + "default": false + } + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "gin", + "concurrently": false, + "name": "books_title_trgm_idx", + "entityType": "indexes", + "schema": "public", + "table": "books" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "author", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": { + "name": "gin_trgm_ops", + "default": false + } + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "gin", + "concurrently": false, + "name": "books_author_trgm_idx", + "entityType": "indexes", + "schema": "public", + "table": "books" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "created_at", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "favorite_books_user_id_created_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "favorite_books" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "book_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "favorite_books_book_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "favorite_books" + }, + { + "nameExplicit": false, + "columns": [ + "user_id" + ], + "schemaTo": "public", + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "account_user_id_user_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "account" + }, + { + "nameExplicit": false, + "columns": [ + "user_id" + ], + "schemaTo": "public", + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "session_user_id_user_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "session" + }, + { + "nameExplicit": false, + "columns": [ + "book_id" + ], + "schemaTo": "public", + "tableTo": "books", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "book_to_genres_book_id_books_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "book_to_genres" + }, + { + "nameExplicit": false, + "columns": [ + "genre_id" + ], + "schemaTo": "public", + "tableTo": "book_genres", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "book_to_genres_genre_id_book_genres_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "book_to_genres" + }, + { + "nameExplicit": false, + "columns": [ + "user_id" + ], + "schemaTo": "public", + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "favorite_books_user_id_user_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "favorite_books" + }, + { + "nameExplicit": false, + "columns": [ + "book_id" + ], + "schemaTo": "public", + "tableTo": "books", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "favorite_books_book_id_books_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "favorite_books" + }, + { + "columns": [ + "book_id", + "genre_id" + ], + "nameExplicit": false, + "name": "book_to_genres_pkey", + "entityType": "pks", + "schema": "public", + "table": "book_to_genres" + }, + { + "columns": [ + "user_id", + "book_id" + ], + "nameExplicit": false, + "name": "favorite_books_pkey", + "entityType": "pks", + "schema": "public", + "table": "favorite_books" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "account_pkey", + "schema": "public", + "table": "account", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "session_pkey", + "schema": "public", + "table": "session", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "user_pkey", + "schema": "public", + "table": "user", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "verification_pkey", + "schema": "public", + "table": "verification", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "book_genres_pkey", + "schema": "public", + "table": "book_genres", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "books_pkey", + "schema": "public", + "table": "books", + "entityType": "pks" + }, + { + "nameExplicit": false, + "columns": [ + "token" + ], + "nullsNotDistinct": false, + "name": "session_token_key", + "schema": "public", + "table": "session", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "email" + ], + "nullsNotDistinct": false, + "name": "user_email_key", + "schema": "public", + "table": "user", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "genre" + ], + "nullsNotDistinct": false, + "name": "book_genres_genre_key", + "schema": "public", + "table": "book_genres", + "entityType": "uniques" + } + ], + "renames": [] +} \ No newline at end of file diff --git a/backend/src/db/schema/books.ts b/backend/src/db/schema/books.ts index 1c36b26a..e16358f2 100644 --- a/backend/src/db/schema/books.ts +++ b/backend/src/db/schema/books.ts @@ -62,6 +62,8 @@ const books = pgTable( // Index for fuzzy title search (using the pg_trgm extension). // Also speeds up the ILIKE '%...%' title search index("books_title_trgm_idx").using("gin", t.title.op("gin_trgm_ops")), + // Same as above, for searching by author + index("books_author_trgm_idx").using("gin", t.author.op("gin_trgm_ops")), ], ); diff --git a/backend/src/schema/books/resolvers/Query/searchBooks.ts b/backend/src/schema/books/resolvers/Query/searchBooks.ts index 82a86d8d..b3a39f99 100644 --- a/backend/src/schema/books/resolvers/Query/searchBooks.ts +++ b/backend/src/schema/books/resolvers/Query/searchBooks.ts @@ -27,7 +27,7 @@ const MOST_RATINGS_FIRST = [ ]; const ORDER_BY: Record> = { - // The default order. Relevance has no direction. A fuzzy title search is + // The default order. Relevance has no direction. A fuzzy search is // sorted by similarity instead, see below RELEVANCE: { ASC: MOST_RATINGS_FIRST, @@ -104,11 +104,18 @@ export const searchBooks: NonNullable = async ( const conditions: SQL[] = []; - if (args.title && args.fuzzy) { - // <% is the word similarity operator using the pg_trgm extension - conditions.push(sql`${args.title} <% ${books.title}`); - } else if (args.title) { - conditions.push(ilike(books.title, `%${args.title}%`)); + // <% is the word similarity operator using the pg_trgm extension + if (args.query) { + const queryFilter = args.fuzzy + ? or( + sql`${args.query} <% ${books.title}`, + sql`${args.query} <% ${books.author}`, + ) + : or( + ilike(books.title, `%${args.query}%`), + ilike(books.author, `%${args.query}%`), + ); + if (queryFilter) conditions.push(queryFilter); } if (args.series) conditions.push(ilike(books.series, `%${args.series}%`)); @@ -150,11 +157,12 @@ export const searchBooks: NonNullable = async ( const sortBy = args.sortBy; let orderBy = ORDER_BY[sortBy][args.sortDirection]; - // The most relevant books for a fuzzy search are the most similar titles - if (sortBy === "RELEVANCE" && args.fuzzy && args.title) { + // The most relevant books for a fuzzy search are the most similar titles and + // authors. A book counts by its best match + if (sortBy === "RELEVANCE" && args.fuzzy && args.query) { orderBy = [ // Safe from SQL injection. Values are passed as parameters to the query by Drizzle. - sql`word_similarity(${args.title}, ${books.title}) DESC`, + sql`GREATEST(word_similarity(${args.query}, ${books.title}), word_similarity(${args.query}, ${books.author})) DESC`, asc(books.id), ]; } diff --git a/backend/src/schema/books/schema.graphql b/backend/src/schema/books/schema.graphql index 58e3e871..8a07253c 100644 --- a/backend/src/schema/books/schema.graphql +++ b/backend/src/schema/books/schema.graphql @@ -40,8 +40,9 @@ extend type Query { bookFormats: [String!]! searchBooks( - title: String - "Match titles that are similar to the title, for example with typos, ordered by similarity" + "Match books where either the title or the author contains the query" + query: String + "Match titles and authors that are similar to the query, for example with typos, ordered by similarity" fuzzy: Boolean! = false series: String author: String diff --git a/backend/src/schema/schema.generated.graphqls b/backend/src/schema/schema.generated.graphqls index 8c11c0b2..49cb6cf5 100644 --- a/backend/src/schema/schema.generated.graphqls +++ b/backend/src/schema/schema.generated.graphqls @@ -80,7 +80,7 @@ type Query { bookFormat: String """ - Match titles that are similar to the title, for example with typos, ordered by similarity + Match titles and authors that are similar to the query, for example with typos, ordered by similarity """ fuzzy: Boolean! = false genres: [String!] @@ -91,10 +91,12 @@ type Query { minRatings: Int offset: Int! = 0 publisher: String + + """Match books where either the title or the author contains the query""" + query: String series: String sortBy: BookSortField! = RELEVANCE sortDirection: SortDirection! = ASC - title: String ): BookSearchResult! } diff --git a/backend/src/schema/typeDefs.generated.ts b/backend/src/schema/typeDefs.generated.ts index 5aecaf09..0953c849 100644 --- a/backend/src/schema/typeDefs.generated.ts +++ b/backend/src/schema/typeDefs.generated.ts @@ -1,2 +1,2 @@ import type { DocumentNode } from 'graphql'; - export const typeDefs = {"kind":"Document","definitions":[{"name":{"kind":"Name","value":"Query"},"kind":"ObjectTypeDefinition","fields":[{"kind":"FieldDefinition","name":{"kind":"Name","value":"book"},"arguments":[{"kind":"InputValueDefinition","name":{"kind":"Name","value":"id"},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}}],"type":{"kind":"NamedType","name":{"kind":"Name","value":"Book"}}},{"kind":"FieldDefinition","name":{"kind":"Name","value":"genres"},"type":{"kind":"NonNullType","type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}}},{"kind":"FieldDefinition","name":{"kind":"Name","value":"languages"},"type":{"kind":"NonNullType","type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}}},{"kind":"FieldDefinition","name":{"kind":"Name","value":"bookFormats"},"type":{"kind":"NonNullType","type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}}},{"kind":"FieldDefinition","name":{"kind":"Name","value":"searchBooks"},"arguments":[{"kind":"InputValueDefinition","name":{"kind":"Name","value":"title"},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"InputValueDefinition","description":{"kind":"StringValue","value":"Match titles that are similar to the title, for example with typos, ordered by similarity","block":false},"name":{"kind":"Name","value":"fuzzy"},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Boolean"}}},"defaultValue":{"kind":"BooleanValue","value":false}},{"kind":"InputValueDefinition","name":{"kind":"Name","value":"series"},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"InputValueDefinition","name":{"kind":"Name","value":"author"},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"InputValueDefinition","name":{"kind":"Name","value":"language"},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"InputValueDefinition","name":{"kind":"Name","value":"bookFormat"},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"InputValueDefinition","name":{"kind":"Name","value":"publisher"},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"InputValueDefinition","name":{"kind":"Name","value":"genres"},"type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}},{"kind":"InputValueDefinition","name":{"kind":"Name","value":"minPages"},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},{"kind":"InputValueDefinition","name":{"kind":"Name","value":"maxPages"},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},{"kind":"InputValueDefinition","name":{"kind":"Name","value":"minRatings"},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},{"kind":"InputValueDefinition","name":{"kind":"Name","value":"limit"},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},"defaultValue":{"kind":"IntValue","value":"12"}},{"kind":"InputValueDefinition","name":{"kind":"Name","value":"offset"},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},"defaultValue":{"kind":"IntValue","value":"0"}},{"kind":"InputValueDefinition","name":{"kind":"Name","value":"sortBy"},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"BookSortField"}}},"defaultValue":{"kind":"EnumValue","value":"ID"}},{"kind":"InputValueDefinition","name":{"kind":"Name","value":"sortDirection"},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SortDirection"}}},"defaultValue":{"kind":"EnumValue","value":"ASC"}}],"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"BookSearchResult"}}}},{"kind":"FieldDefinition","description":{"kind":"StringValue","value":"Favorite books of the logged-in user, newest first.","block":false},"name":{"kind":"Name","value":"favoriteBooks"},"arguments":[{"kind":"InputValueDefinition","name":{"kind":"Name","value":"limit"},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},"defaultValue":{"kind":"IntValue","value":"12"}},{"kind":"InputValueDefinition","name":{"kind":"Name","value":"offset"},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},"defaultValue":{"kind":"IntValue","value":"0"}}],"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"BookSearchResult"}}}},{"kind":"FieldDefinition","description":{"kind":"StringValue","value":"ID of every book the logged-in user that has been added to their favorites.","block":false},"name":{"kind":"Name","value":"favoriteBookIds"},"type":{"kind":"NonNullType","type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}}}},{"kind":"FieldDefinition","description":{"kind":"StringValue","value":"The logged-in user's most common genres among their favorite books.","block":false},"name":{"kind":"Name","value":"favoriteGenres"},"arguments":[{"kind":"InputValueDefinition","name":{"kind":"Name","value":"limit"},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},"defaultValue":{"kind":"IntValue","value":"3"}}],"type":{"kind":"NonNullType","type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}}},{"kind":"FieldDefinition","description":{"kind":"StringValue","value":"Reviews of a book, newest first.","block":false},"name":{"kind":"Name","value":"bookReviews"},"arguments":[{"kind":"InputValueDefinition","name":{"kind":"Name","value":"bookId"},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}},{"kind":"InputValueDefinition","name":{"kind":"Name","value":"limit"},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},"defaultValue":{"kind":"IntValue","value":"10"}},{"kind":"InputValueDefinition","name":{"kind":"Name","value":"offset"},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},"defaultValue":{"kind":"IntValue","value":"0"}}],"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ReviewSearchResult"}}}},{"kind":"FieldDefinition","description":{"kind":"StringValue","value":"The logged-in user's review of a book, or null if they have not reviewed it.","block":false},"name":{"kind":"Name","value":"myReview"},"arguments":[{"kind":"InputValueDefinition","name":{"kind":"Name","value":"bookId"},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}}],"type":{"kind":"NamedType","name":{"kind":"Name","value":"Review"}}},{"kind":"FieldDefinition","description":{"kind":"StringValue","value":"Reviews written by the logged-in user, newest first.","block":false},"name":{"kind":"Name","value":"myReviews"},"arguments":[{"kind":"InputValueDefinition","name":{"kind":"Name","value":"limit"},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},"defaultValue":{"kind":"IntValue","value":"10"}},{"kind":"InputValueDefinition","name":{"kind":"Name","value":"offset"},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},"defaultValue":{"kind":"IntValue","value":"0"}}],"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ReviewSearchResult"}}}}],"directives":[],"interfaces":[]},{"name":{"kind":"Name","value":"Mutation"},"kind":"ObjectTypeDefinition","fields":[{"kind":"FieldDefinition","name":{"kind":"Name","value":"addFavoriteBook"},"arguments":[{"kind":"InputValueDefinition","name":{"kind":"Name","value":"bookId"},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}}],"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Book"}}}},{"kind":"FieldDefinition","name":{"kind":"Name","value":"removeFavoriteBook"},"arguments":[{"kind":"InputValueDefinition","name":{"kind":"Name","value":"bookId"},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}}],"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Boolean"}}}},{"kind":"FieldDefinition","description":{"kind":"StringValue","value":"Create the logged-in user's review of a book, or update it if it already exists.","block":false},"name":{"kind":"Name","value":"upsertReview"},"arguments":[{"kind":"InputValueDefinition","name":{"kind":"Name","value":"bookId"},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}},{"kind":"InputValueDefinition","name":{"kind":"Name","value":"rating"},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}},{"kind":"InputValueDefinition","name":{"kind":"Name","value":"comment"},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Review"}}}},{"kind":"FieldDefinition","description":{"kind":"StringValue","value":"Delete the logged-in user's review of a book. Returns the book with its updated rating.","block":false},"name":{"kind":"Name","value":"deleteReview"},"arguments":[{"kind":"InputValueDefinition","name":{"kind":"Name","value":"bookId"},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}}],"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Book"}}}}],"directives":[],"interfaces":[]},{"kind":"ObjectTypeDefinition","name":{"kind":"Name","value":"Book"},"fields":[{"kind":"FieldDefinition","name":{"kind":"Name","value":"id"},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}},{"kind":"FieldDefinition","name":{"kind":"Name","value":"isbn"},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"FieldDefinition","name":{"kind":"Name","value":"title"},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"FieldDefinition","name":{"kind":"Name","value":"series"},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"FieldDefinition","name":{"kind":"Name","value":"author"},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"FieldDefinition","name":{"kind":"Name","value":"description"},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"FieldDefinition","name":{"kind":"Name","value":"language"},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"FieldDefinition","name":{"kind":"Name","value":"bookFormat"},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"FieldDefinition","name":{"kind":"Name","value":"edition"},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"FieldDefinition","name":{"kind":"Name","value":"pages"},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}},{"kind":"FieldDefinition","name":{"kind":"Name","value":"publisher"},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"FieldDefinition","name":{"kind":"Name","value":"publishDate"},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"FieldDefinition","name":{"kind":"Name","value":"coverImgUrl"},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"FieldDefinition","name":{"kind":"Name","value":"genres"},"type":{"kind":"NonNullType","type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}}},{"kind":"FieldDefinition","name":{"kind":"Name","value":"rating"},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Float"}}},{"kind":"FieldDefinition","name":{"kind":"Name","value":"numRatings"},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}]},{"kind":"EnumTypeDefinition","name":{"kind":"Name","value":"BookSortField"},"values":[{"kind":"EnumValueDefinition","name":{"kind":"Name","value":"ID"}},{"kind":"EnumValueDefinition","name":{"kind":"Name","value":"RATING"}}]},{"kind":"EnumTypeDefinition","name":{"kind":"Name","value":"SortDirection"},"values":[{"kind":"EnumValueDefinition","name":{"kind":"Name","value":"ASC"}},{"kind":"EnumValueDefinition","name":{"kind":"Name","value":"DESC"}}]},{"kind":"ObjectTypeDefinition","name":{"kind":"Name","value":"BookSearchResult"},"fields":[{"kind":"FieldDefinition","name":{"kind":"Name","value":"books"},"type":{"kind":"NonNullType","type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Book"}}}}}},{"kind":"FieldDefinition","name":{"kind":"Name","value":"totalCount"},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}}]},{"kind":"ObjectTypeDefinition","name":{"kind":"Name","value":"Review"},"fields":[{"kind":"FieldDefinition","name":{"kind":"Name","value":"id"},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}},{"kind":"FieldDefinition","name":{"kind":"Name","value":"rating"},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}},{"kind":"FieldDefinition","name":{"kind":"Name","value":"comment"},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"FieldDefinition","description":{"kind":"StringValue","value":"ISO 8601 timestamp","block":false},"name":{"kind":"Name","value":"createdAt"},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"FieldDefinition","description":{"kind":"StringValue","value":"ISO 8601 timestamp","block":false},"name":{"kind":"Name","value":"updatedAt"},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"FieldDefinition","name":{"kind":"Name","value":"user"},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ReviewAuthor"}}}},{"kind":"FieldDefinition","name":{"kind":"Name","value":"book"},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Book"}}}}]},{"kind":"ObjectTypeDefinition","name":{"kind":"Name","value":"ReviewAuthor"},"fields":[{"kind":"FieldDefinition","name":{"kind":"Name","value":"id"},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"FieldDefinition","name":{"kind":"Name","value":"name"},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"FieldDefinition","name":{"kind":"Name","value":"image"},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}]},{"kind":"ObjectTypeDefinition","name":{"kind":"Name","value":"ReviewSearchResult"},"fields":[{"kind":"FieldDefinition","name":{"kind":"Name","value":"reviews"},"type":{"kind":"NonNullType","type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Review"}}}}}},{"kind":"FieldDefinition","name":{"kind":"Name","value":"totalCount"},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}}]},{"kind":"SchemaDefinition","operationTypes":[{"kind":"OperationTypeDefinition","type":{"kind":"NamedType","name":{"kind":"Name","value":"Query"}},"operation":"query"},{"kind":"OperationTypeDefinition","type":{"kind":"NamedType","name":{"kind":"Name","value":"Mutation"}},"operation":"mutation"}]}]} as unknown as DocumentNode + export const typeDefs = {"kind":"Document","definitions":[{"name":{"kind":"Name","value":"Query"},"kind":"ObjectTypeDefinition","fields":[{"kind":"FieldDefinition","name":{"kind":"Name","value":"book"},"arguments":[{"kind":"InputValueDefinition","name":{"kind":"Name","value":"id"},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}}],"type":{"kind":"NamedType","name":{"kind":"Name","value":"Book"}}},{"kind":"FieldDefinition","name":{"kind":"Name","value":"genres"},"type":{"kind":"NonNullType","type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}}},{"kind":"FieldDefinition","name":{"kind":"Name","value":"languages"},"type":{"kind":"NonNullType","type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}}},{"kind":"FieldDefinition","name":{"kind":"Name","value":"bookFormats"},"type":{"kind":"NonNullType","type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}}},{"kind":"FieldDefinition","name":{"kind":"Name","value":"searchBooks"},"arguments":[{"kind":"InputValueDefinition","description":{"kind":"StringValue","value":"Match books where either the title or the author contains the query","block":false},"name":{"kind":"Name","value":"query"},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"InputValueDefinition","description":{"kind":"StringValue","value":"Match titles and authors that are similar to the query, for example with typos, ordered by similarity","block":false},"name":{"kind":"Name","value":"fuzzy"},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Boolean"}}},"defaultValue":{"kind":"BooleanValue","value":false}},{"kind":"InputValueDefinition","name":{"kind":"Name","value":"series"},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"InputValueDefinition","name":{"kind":"Name","value":"author"},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"InputValueDefinition","name":{"kind":"Name","value":"language"},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"InputValueDefinition","name":{"kind":"Name","value":"bookFormat"},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"InputValueDefinition","name":{"kind":"Name","value":"publisher"},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"InputValueDefinition","name":{"kind":"Name","value":"genres"},"type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}},{"kind":"InputValueDefinition","name":{"kind":"Name","value":"minPages"},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},{"kind":"InputValueDefinition","name":{"kind":"Name","value":"maxPages"},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},{"kind":"InputValueDefinition","name":{"kind":"Name","value":"minRatings"},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},{"kind":"InputValueDefinition","name":{"kind":"Name","value":"limit"},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},"defaultValue":{"kind":"IntValue","value":"12"}},{"kind":"InputValueDefinition","name":{"kind":"Name","value":"offset"},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},"defaultValue":{"kind":"IntValue","value":"0"}},{"kind":"InputValueDefinition","name":{"kind":"Name","value":"sortBy"},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"BookSortField"}}},"defaultValue":{"kind":"EnumValue","value":"RELEVANCE"}},{"kind":"InputValueDefinition","name":{"kind":"Name","value":"sortDirection"},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SortDirection"}}},"defaultValue":{"kind":"EnumValue","value":"ASC"}}],"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"BookSearchResult"}}}},{"kind":"FieldDefinition","description":{"kind":"StringValue","value":"Favorite books of the logged-in user, newest first.","block":false},"name":{"kind":"Name","value":"favoriteBooks"},"arguments":[{"kind":"InputValueDefinition","name":{"kind":"Name","value":"limit"},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},"defaultValue":{"kind":"IntValue","value":"12"}},{"kind":"InputValueDefinition","name":{"kind":"Name","value":"offset"},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},"defaultValue":{"kind":"IntValue","value":"0"}}],"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"BookSearchResult"}}}},{"kind":"FieldDefinition","description":{"kind":"StringValue","value":"ID of every book the logged-in user that has been added to their favorites.","block":false},"name":{"kind":"Name","value":"favoriteBookIds"},"type":{"kind":"NonNullType","type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}}}},{"kind":"FieldDefinition","description":{"kind":"StringValue","value":"The logged-in user's most common genres among their favorite books.","block":false},"name":{"kind":"Name","value":"favoriteGenres"},"arguments":[{"kind":"InputValueDefinition","name":{"kind":"Name","value":"limit"},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},"defaultValue":{"kind":"IntValue","value":"3"}}],"type":{"kind":"NonNullType","type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}}},{"kind":"FieldDefinition","description":{"kind":"StringValue","value":"Reviews of a book, newest first.","block":false},"name":{"kind":"Name","value":"bookReviews"},"arguments":[{"kind":"InputValueDefinition","name":{"kind":"Name","value":"bookId"},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}},{"kind":"InputValueDefinition","name":{"kind":"Name","value":"limit"},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},"defaultValue":{"kind":"IntValue","value":"10"}},{"kind":"InputValueDefinition","name":{"kind":"Name","value":"offset"},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},"defaultValue":{"kind":"IntValue","value":"0"}}],"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ReviewSearchResult"}}}},{"kind":"FieldDefinition","description":{"kind":"StringValue","value":"The logged-in user's review of a book, or null if they have not reviewed it.","block":false},"name":{"kind":"Name","value":"myReview"},"arguments":[{"kind":"InputValueDefinition","name":{"kind":"Name","value":"bookId"},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}}],"type":{"kind":"NamedType","name":{"kind":"Name","value":"Review"}}},{"kind":"FieldDefinition","description":{"kind":"StringValue","value":"Reviews written by the logged-in user, newest first.","block":false},"name":{"kind":"Name","value":"myReviews"},"arguments":[{"kind":"InputValueDefinition","name":{"kind":"Name","value":"limit"},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},"defaultValue":{"kind":"IntValue","value":"10"}},{"kind":"InputValueDefinition","name":{"kind":"Name","value":"offset"},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},"defaultValue":{"kind":"IntValue","value":"0"}}],"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ReviewSearchResult"}}}}],"directives":[],"interfaces":[]},{"name":{"kind":"Name","value":"Mutation"},"kind":"ObjectTypeDefinition","fields":[{"kind":"FieldDefinition","name":{"kind":"Name","value":"addFavoriteBook"},"arguments":[{"kind":"InputValueDefinition","name":{"kind":"Name","value":"bookId"},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}}],"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Book"}}}},{"kind":"FieldDefinition","name":{"kind":"Name","value":"removeFavoriteBook"},"arguments":[{"kind":"InputValueDefinition","name":{"kind":"Name","value":"bookId"},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}}],"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Boolean"}}}},{"kind":"FieldDefinition","description":{"kind":"StringValue","value":"Create the logged-in user's review of a book, or update it if it already exists.","block":false},"name":{"kind":"Name","value":"upsertReview"},"arguments":[{"kind":"InputValueDefinition","name":{"kind":"Name","value":"bookId"},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}},{"kind":"InputValueDefinition","name":{"kind":"Name","value":"rating"},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}},{"kind":"InputValueDefinition","name":{"kind":"Name","value":"comment"},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Review"}}}},{"kind":"FieldDefinition","description":{"kind":"StringValue","value":"Delete the logged-in user's review of a book. Returns the book with its updated rating.","block":false},"name":{"kind":"Name","value":"deleteReview"},"arguments":[{"kind":"InputValueDefinition","name":{"kind":"Name","value":"bookId"},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}}],"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Book"}}}}],"directives":[],"interfaces":[]},{"kind":"ObjectTypeDefinition","name":{"kind":"Name","value":"Book"},"fields":[{"kind":"FieldDefinition","name":{"kind":"Name","value":"id"},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}},{"kind":"FieldDefinition","name":{"kind":"Name","value":"isbn"},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"FieldDefinition","name":{"kind":"Name","value":"title"},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"FieldDefinition","name":{"kind":"Name","value":"series"},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"FieldDefinition","name":{"kind":"Name","value":"author"},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"FieldDefinition","name":{"kind":"Name","value":"description"},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"FieldDefinition","name":{"kind":"Name","value":"language"},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"FieldDefinition","name":{"kind":"Name","value":"bookFormat"},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"FieldDefinition","name":{"kind":"Name","value":"edition"},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"FieldDefinition","name":{"kind":"Name","value":"pages"},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}},{"kind":"FieldDefinition","name":{"kind":"Name","value":"publisher"},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"FieldDefinition","name":{"kind":"Name","value":"publishDate"},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"FieldDefinition","name":{"kind":"Name","value":"publishYear"},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},{"kind":"FieldDefinition","name":{"kind":"Name","value":"coverImgUrl"},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"FieldDefinition","name":{"kind":"Name","value":"genres"},"type":{"kind":"NonNullType","type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}}},{"kind":"FieldDefinition","name":{"kind":"Name","value":"rating"},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Float"}}},{"kind":"FieldDefinition","name":{"kind":"Name","value":"numRatings"},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}]},{"kind":"EnumTypeDefinition","name":{"kind":"Name","value":"BookSortField"},"values":[{"kind":"EnumValueDefinition","name":{"kind":"Name","value":"RELEVANCE"}},{"kind":"EnumValueDefinition","name":{"kind":"Name","value":"ID"}},{"kind":"EnumValueDefinition","name":{"kind":"Name","value":"RATING"}},{"kind":"EnumValueDefinition","name":{"kind":"Name","value":"TITLE"}},{"kind":"EnumValueDefinition","name":{"kind":"Name","value":"NUM_RATINGS"}},{"kind":"EnumValueDefinition","name":{"kind":"Name","value":"PAGES"}},{"kind":"EnumValueDefinition","name":{"kind":"Name","value":"PUBLISH_YEAR"}}]},{"kind":"EnumTypeDefinition","name":{"kind":"Name","value":"SortDirection"},"values":[{"kind":"EnumValueDefinition","name":{"kind":"Name","value":"ASC"}},{"kind":"EnumValueDefinition","name":{"kind":"Name","value":"DESC"}}]},{"kind":"ObjectTypeDefinition","name":{"kind":"Name","value":"BookSearchResult"},"fields":[{"kind":"FieldDefinition","name":{"kind":"Name","value":"books"},"type":{"kind":"NonNullType","type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Book"}}}}}},{"kind":"FieldDefinition","name":{"kind":"Name","value":"totalCount"},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}}]},{"kind":"ObjectTypeDefinition","name":{"kind":"Name","value":"Review"},"fields":[{"kind":"FieldDefinition","name":{"kind":"Name","value":"id"},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}},{"kind":"FieldDefinition","name":{"kind":"Name","value":"rating"},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}},{"kind":"FieldDefinition","name":{"kind":"Name","value":"comment"},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"FieldDefinition","description":{"kind":"StringValue","value":"ISO 8601 timestamp","block":false},"name":{"kind":"Name","value":"createdAt"},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"FieldDefinition","description":{"kind":"StringValue","value":"ISO 8601 timestamp","block":false},"name":{"kind":"Name","value":"updatedAt"},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"FieldDefinition","name":{"kind":"Name","value":"user"},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ReviewAuthor"}}}},{"kind":"FieldDefinition","name":{"kind":"Name","value":"book"},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Book"}}}}]},{"kind":"ObjectTypeDefinition","name":{"kind":"Name","value":"ReviewAuthor"},"fields":[{"kind":"FieldDefinition","name":{"kind":"Name","value":"id"},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"FieldDefinition","name":{"kind":"Name","value":"name"},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"FieldDefinition","name":{"kind":"Name","value":"image"},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}]},{"kind":"ObjectTypeDefinition","name":{"kind":"Name","value":"ReviewSearchResult"},"fields":[{"kind":"FieldDefinition","name":{"kind":"Name","value":"reviews"},"type":{"kind":"NonNullType","type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Review"}}}}}},{"kind":"FieldDefinition","name":{"kind":"Name","value":"totalCount"},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}}]},{"kind":"SchemaDefinition","operationTypes":[{"kind":"OperationTypeDefinition","type":{"kind":"NamedType","name":{"kind":"Name","value":"Query"}},"operation":"query"},{"kind":"OperationTypeDefinition","type":{"kind":"NamedType","name":{"kind":"Name","value":"Mutation"}},"operation":"mutation"}]}]} as unknown as DocumentNode \ No newline at end of file diff --git a/backend/src/schema/types.generated.ts b/backend/src/schema/types.generated.ts index a64c8507..26cdc3c9 100644 --- a/backend/src/schema/types.generated.ts +++ b/backend/src/schema/types.generated.ts @@ -152,10 +152,10 @@ export type QuerysearchBooksArgs = { minRatings?: InputMaybe; offset?: Scalars['Int']['input']; publisher?: InputMaybe; + query?: InputMaybe; series?: InputMaybe; sortBy?: BookSortField; sortDirection?: SortDirection; - title?: InputMaybe; }; export type Review = { diff --git a/docs/choices-and-reasoning.md b/docs/choices-and-reasoning.md index ac78cbef..47271ccc 100644 --- a/docs/choices-and-reasoning.md +++ b/docs/choices-and-reasoning.md @@ -92,4 +92,3 @@ Instead, we use CI for other tasks, such as linting and formatting. - **Filters:** We filter on genres, language, book format and number of pages. The specification also mentioned author, publisher, place and person, which we have not implemented yet. - **Login:** Instead of Google login, we support email/password and GitHub and Discord login. Google was rejected since it required https connection, which we do not provide. - **Reviews:** Reviews with a star rating are planned for the next delivery. -- **Search:** The specification mentioned search on both title and author. We currently search on titles only, and searching by author is planned for the next delivery. diff --git a/frontend/app/api/types/graphql.generated.ts b/frontend/app/api/types/graphql.generated.ts index 8aae4209..d29b5fd7 100644 --- a/frontend/app/api/types/graphql.generated.ts +++ b/frontend/app/api/types/graphql.generated.ts @@ -95,7 +95,7 @@ export type DeleteReviewMutationVariables = Exact<{ export type DeleteReviewMutation = { deleteReview: { __typename: 'Book', id: number, rating: number | null, numRatings: number | null } }; export type SearchBooksQueryVariables = Exact<{ - title?: string | null | undefined; + query?: string | null | undefined; fuzzy?: boolean; genres?: Array | string | null | undefined; language?: string | null | undefined; diff --git a/frontend/app/components/books/SearchBar/SearchBar.tsx b/frontend/app/components/books/SearchBar/SearchBar.tsx index 4bfa4fc3..3ba8be29 100644 --- a/frontend/app/components/books/SearchBar/SearchBar.tsx +++ b/frontend/app/components/books/SearchBar/SearchBar.tsx @@ -80,7 +80,7 @@ function SearchBar({ resultCount }: SearchBarProps) { type="search" name="search" aria-label="Search for books" - placeholder="Search books..." + placeholder="Search by title or author..." value={inputField} onChange={(e) => { if ( diff --git a/frontend/app/components/home/ForYouSection/ForYouSection.tsx b/frontend/app/components/home/ForYouSection/ForYouSection.tsx index 38cd1054..4b7ee034 100644 --- a/frontend/app/components/home/ForYouSection/ForYouSection.tsx +++ b/frontend/app/components/home/ForYouSection/ForYouSection.tsx @@ -40,7 +40,7 @@ function ForYouSection() { genres: genre ? [genre] : undefined, minRatings: MIN_RATINGS, }, - title: "", + query: "", // Fetch extra, since books the user has already saved are left out. limit: RECOMMENDATION_LIMIT * 3, offset: 0, diff --git a/frontend/app/components/home/HeroSection/HeroSection.tsx b/frontend/app/components/home/HeroSection/HeroSection.tsx index 0cef8142..91cb3a3b 100644 --- a/frontend/app/components/home/HeroSection/HeroSection.tsx +++ b/frontend/app/components/home/HeroSection/HeroSection.tsx @@ -76,7 +76,7 @@ function HeroSection({ type="search" name={field.name} aria-label="Search for books" - placeholder="Search books..." + placeholder="Search by title or author..." value={field.state.value} onBlur={field.handleBlur} onChange={(e) => field.handleChange(e.target.value)} diff --git a/frontend/app/components/home/HowItWorksSection/HowItWorksSection.tsx b/frontend/app/components/home/HowItWorksSection/HowItWorksSection.tsx index 0c9c5d58..18c82118 100644 --- a/frontend/app/components/home/HowItWorksSection/HowItWorksSection.tsx +++ b/frontend/app/components/home/HowItWorksSection/HowItWorksSection.tsx @@ -7,7 +7,7 @@ const STEPS = [ icon: SearchIcon, title: "Search and filter", description: - "Find books by title, then narrow them down by genre, language, format and length.", + "Find books by title or author, then narrow them down by genre, language, format and length.", }, { icon: HeartIcon, diff --git a/frontend/app/components/home/SurpriseMeButton/SurpriseMeButton.tsx b/frontend/app/components/home/SurpriseMeButton/SurpriseMeButton.tsx index cd963207..f0b3393a 100644 --- a/frontend/app/components/home/SurpriseMeButton/SurpriseMeButton.tsx +++ b/frontend/app/components/home/SurpriseMeButton/SurpriseMeButton.tsx @@ -23,7 +23,7 @@ function SurpriseMeButton({ bookCount }: SurpriseMeButtonProps) { try { const { data } = await fetchBook({ variables: { - title: "", + query: "", limit: 1, offset: Math.floor(Math.random() * bookCount), }, diff --git a/frontend/app/hooks/useSearchBooks.ts b/frontend/app/hooks/useSearchBooks.ts index 9d1aea85..bec275e9 100644 --- a/frontend/app/hooks/useSearchBooks.ts +++ b/frontend/app/hooks/useSearchBooks.ts @@ -12,7 +12,7 @@ const SEARCH_BOOKS: TypedDocumentNode< SearchBooksQueryVariables > = gql` query SearchBooks( - $title: String + $query: String $fuzzy: Boolean! = false $genres: [String!] $language: String @@ -26,7 +26,7 @@ const SEARCH_BOOKS: TypedDocumentNode< $offset: Int! ) { searchBooks( - title: $title + query: $query fuzzy: $fuzzy genres: $genres language: $language @@ -55,7 +55,7 @@ const SEARCH_BOOKS: TypedDocumentNode< function useSearchBooks({ filters, - title, + query, fuzzy, limit, offset, @@ -63,7 +63,7 @@ function useSearchBooks({ enabled = true, }: { filters: BookFilters; - title: string; + query: string; fuzzy?: boolean; limit: number; offset: number; @@ -75,7 +75,7 @@ function useSearchBooks({ enabled ? { variables: { - title, + query, fuzzy, genres: filters.genres, language: filters.language, diff --git a/frontend/app/hooks/useSuggestedBooks.ts b/frontend/app/hooks/useSuggestedBooks.ts index 618b6916..54361736 100644 --- a/frontend/app/hooks/useSuggestedBooks.ts +++ b/frontend/app/hooks/useSuggestedBooks.ts @@ -6,11 +6,11 @@ import { BOOK_PAGE_LIMIT, MIN_RATINGS } from "~/lib/constants"; /** * Suggested books when a search has no results. * - * The suggested books are books with a similar title to the search query. + * The suggested books are books with a title or author similar to the search query. * If none are found, we use top-rated books instead. * For top-rated books, we remove the filters if there is no search query, * since that means the filters are the reason there are no results. - * The sort only applies to similar titles, top-rated books are always sorted by rating. + * The sort only applies to similar books, top-rated books are always sorted by rating. */ function useSuggestedBooks({ query, @@ -29,7 +29,7 @@ function useSuggestedBooks({ const similar = useSearchBooks({ filters, - title: query, + query, fuzzy: true, limit: BOOK_PAGE_LIMIT, offset, @@ -45,7 +45,7 @@ function useSuggestedBooks({ ...keptFilters, minRatings: Math.max(keptFilters.minRatings ?? 0, MIN_RATINGS), }, - title: "", + query: "", limit: BOOK_PAGE_LIMIT, offset, sort: "rating_desc", diff --git a/frontend/app/routes/book/selectedBook.tsx b/frontend/app/routes/book/selectedBook.tsx index 657b9e17..5a2b393e 100644 --- a/frontend/app/routes/book/selectedBook.tsx +++ b/frontend/app/routes/book/selectedBook.tsx @@ -35,7 +35,7 @@ export default function SelectedBookPage({ params }: Route.ComponentProps) { const { books: searchBooks } = useSearchBooks({ filters, - title: query, + query, limit: BOOK_PAGE_LIMIT, offset: (page - 1) * BOOK_PAGE_LIMIT, sort, diff --git a/frontend/app/routes/books.test.tsx b/frontend/app/routes/books.test.tsx index 365f8e8e..680512e8 100644 --- a/frontend/app/routes/books.test.tsx +++ b/frontend/app/routes/books.test.tsx @@ -52,7 +52,7 @@ describe("BooksPage", () => { mocks: [ searchBooksMock( { - title: "blade", + query: "blade", genres: ["Fantasy", "Romance"], language: "English", bookFormat: "Paperback", @@ -80,14 +80,14 @@ describe("BooksPage", () => { renderRoutes(routes, { initialEntries: ["/books?q=zzzz"], mocks: [ - searchBooksMock({ title: "zzzz", limit: 12, offset: 0 }, []), + searchBooksMock({ query: "zzzz", limit: 12, offset: 0 }, []), searchBooksMock( - { title: "zzzz", fuzzy: true, limit: 12, offset: 0 }, + { query: "zzzz", fuzzy: true, limit: 12, offset: 0 }, [], ), searchBooksMock( { - title: "", + query: "", minRatings: 10000, sortBy: "RATING", sortDirection: "DESC", @@ -109,9 +109,9 @@ describe("BooksPage", () => { renderRoutes(routes, { initialEntries: ["/books?q=blade+itslef"], mocks: [ - searchBooksMock({ title: "blade itslef", limit: 12, offset: 0 }, []), + searchBooksMock({ query: "blade itslef", limit: 12, offset: 0 }, []), searchBooksMock({ - title: "blade itslef", + query: "blade itslef", fuzzy: true, limit: 12, offset: 0, @@ -124,7 +124,7 @@ describe("BooksPage", () => { ).toBeInTheDocument(); expect( screen.getByText( - "No books found for this search. However, we found some books with similar titles.", + "No books found for this search. However, we found some books with similar titles or authors.", ), ).toBeInTheDocument(); expect(await findBookCards()).toHaveLength(3); @@ -136,7 +136,7 @@ describe("BooksPage", () => { mocks: [ searchBooksMock( { - title: "blade itslef", + query: "blade itslef", sortBy: "RATING", sortDirection: "DESC", limit: 12, @@ -146,7 +146,7 @@ describe("BooksPage", () => { ), searchBooksMock( { - title: "blade itslef", + query: "blade itslef", fuzzy: true, sortBy: "RATING", sortDirection: "DESC", @@ -173,12 +173,12 @@ describe("BooksPage", () => { initialEntries: ["/books?q=zzzz&genres=Fantasy"], mocks: [ searchBooksMock( - { title: "zzzz", genres: ["Fantasy"], limit: 12, offset: 0 }, + { query: "zzzz", genres: ["Fantasy"], limit: 12, offset: 0 }, [], ), searchBooksMock( { - title: "zzzz", + query: "zzzz", fuzzy: true, genres: ["Fantasy"], limit: 12, @@ -187,7 +187,7 @@ describe("BooksPage", () => { [], ), searchBooksMock({ - title: "", + query: "", genres: ["Fantasy"], minRatings: 10000, sortBy: "RATING", @@ -218,7 +218,7 @@ describe("BooksPage", () => { mocks: [ searchBooksMock( { - title: "", + query: "", genres: ["Fantasy"], maxPages: 1, limit: 12, @@ -227,7 +227,7 @@ describe("BooksPage", () => { [], ), searchBooksMock({ - title: "", + query: "", minRatings: 10000, sortBy: "RATING", sortDirection: "DESC", @@ -253,12 +253,12 @@ describe("BooksPage", () => { initialEntries: ["/books?q=zzzz&minRatings=50000"], mocks: [ searchBooksMock( - { title: "zzzz", minRatings: 50000, limit: 12, offset: 0 }, + { query: "zzzz", minRatings: 50000, limit: 12, offset: 0 }, [], ), searchBooksMock( { - title: "zzzz", + query: "zzzz", fuzzy: true, minRatings: 50000, limit: 12, @@ -267,7 +267,7 @@ describe("BooksPage", () => { [], ), searchBooksMock({ - title: "", + query: "", minRatings: 50000, sortBy: "RATING", sortDirection: "DESC", @@ -286,13 +286,13 @@ describe("BooksPage", () => { initialEntries: ["/books?q=zzzz"], // The suggestions are reused from Apollo's cache on the details page. mocks: [ - searchBooksMock({ title: "zzzz", limit: 12, offset: 0 }, []), + searchBooksMock({ query: "zzzz", limit: 12, offset: 0 }, []), searchBooksMock( - { title: "zzzz", fuzzy: true, limit: 12, offset: 0 }, + { query: "zzzz", fuzzy: true, limit: 12, offset: 0 }, [], ), searchBooksMock({ - title: "", + query: "", minRatings: 10000, sortBy: "RATING", sortDirection: "DESC", @@ -332,7 +332,7 @@ describe("BooksPage", () => { { request: { query: SEARCH_BOOKS, - variables: { title: "", limit: 12, offset: 0 }, + variables: { query: "", limit: 12, offset: 0 }, }, error: new Error("Network error"), }, @@ -346,7 +346,7 @@ describe("BooksPage", () => { it("opens a book's details and goes back to the same search", async () => { const user = userEvent.setup(); - const variables = { title: "blade", limit: 12, offset: 0 }; + const variables = { query: "blade", limit: 12, offset: 0 }; renderRoutes(routes, { initialEntries: ["/books?q=blade"], // Each request is mocked once: the search results are reused from diff --git a/frontend/app/routes/books.tsx b/frontend/app/routes/books.tsx index c08e3932..d76571ce 100644 --- a/frontend/app/routes/books.tsx +++ b/frontend/app/routes/books.tsx @@ -39,7 +39,7 @@ export default function BooksPage() { const { books, totalCount, loading, error } = useSearchBooks({ filters, - title: query, + query, limit: BOOK_PAGE_LIMIT, offset: (page - 1) * BOOK_PAGE_LIMIT, sort, @@ -111,7 +111,7 @@ export default function BooksPage() {

No books found for this search. However, we found some{" "} {similarTitles - ? "books with similar titles" + ? "books with similar titles or authors" : query && hasFilters ? "top-rated books matching your filters" : "top-rated books you might like"} diff --git a/frontend/app/routes/home.test.tsx b/frontend/app/routes/home.test.tsx index 3bfc448a..e63c7371 100644 --- a/frontend/app/routes/home.test.tsx +++ b/frontend/app/routes/home.test.tsx @@ -58,7 +58,7 @@ const favoritesMocks = (favoriteGenres: string[], bookIds = [savedBook.id]) => [ const recommendationsMock = (genre: string) => searchBooksMock({ - title: "", + query: "", genres: [genre], minRatings: MIN_RATINGS, sortBy: "RATING", @@ -68,7 +68,7 @@ const recommendationsMock = (genre: string) => }); const topRatedMock = searchBooksMock({ - title: "", + query: "", minRatings: MIN_RATINGS, sortBy: "RATING", sortDirection: "DESC", @@ -78,7 +78,7 @@ const topRatedMock = searchBooksMock({ // The number of books in the whole library, for the stats and "Surprise me". const bookCountMock = searchBooksMock( - { title: "", limit: 0, offset: 0 }, + { query: "", limit: 0, offset: 0 }, [], apiSearchBooks.length, ); @@ -108,7 +108,7 @@ describe("HomePage", () => { topRatedMock, bookCountMock, bookFiltersMock(), - searchBooksMock({ title: "blade", limit: 12, offset: 0 }), + searchBooksMock({ query: "blade", limit: 12, offset: 0 }), ], }); @@ -162,7 +162,7 @@ describe("HomePage", () => { bookCountMock, bookFiltersMock(), searchBooksMock({ - title: "", + query: "", genres: ["Fantasy"], limit: 12, offset: 0, @@ -227,7 +227,7 @@ describe("HomePage", () => { bookCountMock, bookFiltersMock(), // 0.5 of 3 books is offset 1, the second book. - searchBooksMock({ title: "", limit: 1, offset: 1 }, [ + searchBooksMock({ query: "", limit: 1, offset: 1 }, [ apiSearchBooks[1], ]), ], diff --git a/frontend/app/routes/home.tsx b/frontend/app/routes/home.tsx index 498d4274..a74a71da 100644 --- a/frontend/app/routes/home.tsx +++ b/frontend/app/routes/home.tsx @@ -23,7 +23,7 @@ export default function HomePage() { const { books, loading } = useSearchBooks({ filters: { minRatings: MIN_RATINGS }, - title: "", + query: "", limit: TOP_RATED_LIMIT, offset: 0, sort: "rating_desc", @@ -32,7 +32,7 @@ export default function HomePage() { // Only the number of books in the whole library, for the stats and "Surprise me" const { totalCount: bookCount } = useSearchBooks({ filters: {}, - title: "", + query: "", limit: 0, offset: 0, }); diff --git a/frontend/app/test/mocks.ts b/frontend/app/test/mocks.ts index 84c580d4..859f4446 100644 --- a/frontend/app/test/mocks.ts +++ b/frontend/app/test/mocks.ts @@ -11,7 +11,7 @@ import { apiSearchBooks } from "~/test/fixtures/books"; // The variables the pages send for "/" (no search, filters, sort or page). const defaultSearchVariables: SearchBooksQueryVariables = { - title: "", + query: "", limit: BOOK_PAGE_LIMIT, offset: 0, }; diff --git a/frontend/e2e/books.spec.ts b/frontend/e2e/books.spec.ts index 2d8cd58e..5796ec09 100644 --- a/frontend/e2e/books.spec.ts +++ b/frontend/e2e/books.spec.ts @@ -16,6 +16,26 @@ test("searching updates the URL and the results", async ({ page }) => { ); }); +test("searching finds books by author", async ({ page }) => { + await page.goto("books"); + + await page + .getByRole("searchbox", { name: "Search for books" }) + .fill("tolkien"); + + await expect(page).toHaveURL("books?q=tolkien"); + await expect(bookCards(page).first()).toContainText("Tolkien"); +}); + +test("an author with a typo gives suggestions", async ({ page }) => { + await page.goto("books?q=tolkin"); + + await expect( + page.getByText("we found some books with similar titles or authors"), + ).toBeVisible(); + await expect(bookCards(page).first()).toContainText("Tolkien"); +}); + test("filtering by genre can be reset", async ({ page }) => { await page.goto("books");