Fix tag queries.

master
Sander Vocke 4 years ago
parent 4b45610648
commit e8c043b08d
  1. 6
      client/src/api/endpoints/data.ts
  2. 4
      client/src/api/endpoints/query.ts
  3. 12
      client/src/api/endpoints/resources.ts
  4. 9
      client/src/api/types/resources.ts
  5. 38
      client/src/components/querybuilder/QBAddElemMenu.tsx
  6. 4
      client/src/components/querybuilder/QBNodeElem.tsx
  7. 1
      client/src/components/querybuilder/QBPlaceholder.tsx
  8. 10
      client/src/components/querybuilder/QueryBuilder.tsx
  9. 19
      client/src/components/windows/query/QueryWindow.tsx
  10. 50
      client/src/lib/query/Query.tsx
  11. 8
      server/db/Album.ts
  12. 8
      server/db/Artist.ts
  13. 4
      server/db/Data.ts
  14. 12
      server/db/Tag.ts
  15. 8
      server/db/Track.ts

@ -7,7 +7,7 @@
// Upon import, they might be replaced, and upon export, they might be randomly
// generated.
import { Album, Id, AlbumRefs, Artist, ArtistRefs, Tag, TagRefs, Track, TrackRefs, isTrackRefs, isAlbumRefs, isArtistRefs, isTagRefs } from "../types/resources";
import { Album, Id, AlbumRefs, Artist, ArtistRefs, Tag, TagParentId, Track, TrackRefs, isTrackRefs, isAlbumRefs, isArtistRefs, isTagParentId } from "../types/resources";
// The import/export DB format is just a set of lists of objects.
// Each object has an ID and references others by ID.
@ -17,7 +17,7 @@ export interface DBDataFormat {
tracks: (Track & Id & TrackRefs)[],
albums: (Album & Id & AlbumRefs)[],
artists: (Artist & Id & ArtistRefs)[],
tags: (Tag & Id & TagRefs)[],
tags: (Tag & Id & TagParentId)[],
}
// Get a full export of a user's database (GET).
@ -49,7 +49,7 @@ export const checkDBImportRequest: (v: any) => boolean = (v: any) => {
return prev && isArtistRefs(cur);
}, true) &&
v.tags.reduce((prev: boolean, cur: any) => {
return prev && isTagRefs(cur);
return prev && isTagParentId(cur);
}, true);
}

@ -1,6 +1,6 @@
// Query for items (POST).
import { Album, Id, Artist, Tag, Track, Name, StoreLinks, TagRefs, AlbumRefs, TrackDetails, ArtistDetails } from "../types/resources";
import { Album, Id, Artist, Tag, Track, Name, StoreLinks, TagParentId, AlbumRefs, TrackDetails, ArtistDetails } from "../types/resources";
export const QueryEndpoint = '/query';
@ -79,7 +79,7 @@ export interface QueryRequest {
// Query response structure
export type QueryResponseTrackDetails = (Track & Name & StoreLinks & TrackDetails & Id);
export type QueryResponseArtistDetails = (Artist & Name & StoreLinks & Id);
export type QueryResponseTagDetails = (Tag & Name & TagRefs & Id);
export type QueryResponseTagDetails = (Tag & Name & TagParentId & Id);
export type QueryResponseAlbumDetails = (Album & Name & StoreLinks & Id);
export interface QueryResponse {
tracks: QueryResponseTrackDetails[] | number[] | number, // Details | IDs | count, depending on QueryResponseType

@ -12,11 +12,11 @@ import {
TrackRefs,
ArtistRefs,
AlbumRefs,
TagRefs,
TagParentId,
isTrackRefs,
isAlbumRefs,
isArtistRefs,
isTagRefs,
isTagParentId,
isName,
Name,
isTrack,
@ -83,9 +83,9 @@ export const checkPostAlbumRequest: (v: any) => boolean = (v: any) => isAlbumRef
// Post new tag (POST).
export const PostTagEndpoint = "/tag";
export type PostTagRequest = (Tag & TagRefs & Name);
export type PostTagRequest = (Tag & TagParentId & Name);
export interface PostTagResponse { id: number };
export const checkPostTagRequest: (v: any) => boolean = (v: any) => isTagRefs(v) && isName(v);
export const checkPostTagRequest: (v: any) => boolean = (v: any) => isTagParentId(v) && isName(v);
// Post new integration (POST).
export const PostIntegrationEndpoint = "/integration";
@ -113,9 +113,9 @@ export const checkPutAlbumRequest: (v: any) => boolean = (v: any) => isAlbumRefs
// Replace tag (PUT).
export const PutTagEndpoint = "/tag/:id";
export type PutTagRequest = (Tag & TagRefs);
export type PutTagRequest = (Tag & TagParentId);
export type PutTagResponse = void;
export const checkPutTagRequest: (v: any) => boolean = (v: any) => isTagRefs(v) && isName(v);;
export const checkPutTagRequest: (v: any) => boolean = (v: any) => isTagParentId(v) && isName(v);;
// Replace integration (PUT).
export const PutIntegrationEndpoint = "/integration/:id";

@ -147,12 +147,17 @@ export interface Tag {
id?: number,
parentId?: number | null,
parent?: (Tag & Id) | null,
childIds?: number[],
}
export interface TagRefs {
export interface TagParentId {
parentId: number | null,
}
export interface TagChildIds {
childIds: number[],
}
export interface TagDetails {
parent: (Tag & Id) | null,
}
@ -161,7 +166,7 @@ export function isTag(q: any): q is Tag {
return q.mbApi_typename && q.mbApi_typename === "tag";
}
export function isTagRefs(q: any): q is TagRefs {
export function isTagParentId(q: any): q is TagParentId {
return isTag(q) && 'parentId' in q;
}

@ -3,7 +3,7 @@ import { Menu, MenuItem } from '@material-ui/core';
import NestedMenuItem from "material-ui-nested-menu-item";
import { QueryElem, QueryLeafBy, QueryLeafOp, TagQueryInfo } from '../../lib/query/Query';
import QBSelectWithRequest from './QBSelectWithRequest';
import { Requests } from './QueryBuilder';
import { Requests, QueryBuilderTag } from './QueryBuilder';
export interface MenuProps {
anchorEl: null | HTMLElement,
@ -12,19 +12,19 @@ export interface MenuProps {
requestFunctions: Requests,
}
export function createTagInfo(tag: any, allTags: any[]): TagQueryInfo {
const resolveName: (t: any) => string[] = (t: any) => {
export function createTagInfo(tag: QueryBuilderTag, allTags: QueryBuilderTag[]): TagQueryInfo {
const resolveName: (t: QueryBuilderTag) => string[] = (t: QueryBuilderTag) => {
if (t.parentId) {
const parent = allTags.filter((o: any) => o.tagId === t.parentId)[0];
return [resolveName(parent), t.name];
const parent = allTags.filter((o: QueryBuilderTag) => o.id === t.parentId)[0];
return resolveName(parent).concat(t.name);
}
return [t.name];
}
const resolveChildren: (t: any) => Set<number> = (t: any) => {
const resolveChildren: (t: QueryBuilderTag) => Set<number> = (t: QueryBuilderTag) => {
if (t.childIds.length > 0) {
const childSets: Set<number>[] = allTags.filter((o: any) => t.childIds.includes(o.tagId))
.map((child: any) => resolveChildren(child));
const childSets: Set<number>[] = allTags.filter((o: QueryBuilderTag) => t.childIds.includes(o.id))
.map((child: QueryBuilderTag) => resolveChildren(child));
var r = new Set<number>();
childSets.forEach((c: any) => {
@ -33,7 +33,7 @@ export function createTagInfo(tag: any, allTags: any[]): TagQueryInfo {
return r;
}
return new Set([t.tagId]);
return new Set([t.id]);
}
return {
@ -47,13 +47,14 @@ export function QBAddElemMenu(props: MenuProps) {
let onClose = props.onClose;
interface TagItemProps {
tag: any,
allTags: any[],
tag: QueryBuilderTag,
allTags: QueryBuilderTag[],
}
const TagItem = (_props: TagItemProps) => {
if (_props.tag.childIds.length > 0) {
const children = _props.allTags.filter(
(tag: any) => _props.tag.childIds.includes(tag.tagId)
(tag: QueryBuilderTag) =>
_props.tag.childIds.includes(tag.id)
);
return <NestedMenuItem
@ -68,12 +69,19 @@ export function QBAddElemMenu(props: MenuProps) {
});
}}
>
{children.map((child: any) => <TagItem tag={child} allTags={_props.allTags} />)}
{children.map((child: QueryBuilderTag) => <TagItem tag={child} allTags={_props.allTags} />)}
</NestedMenuItem>
}
return <MenuItem
onClick={() => {
console.log("onCreateQuery: adding:",{
a: QueryLeafBy.TagInfo,
leafOp: QueryLeafOp.Equals,
b: createTagInfo(_props.tag, _props.allTags),
} );
onClose();
props.onCreateQuery({
a: QueryLeafBy.TagInfo,
@ -87,7 +95,7 @@ export function QBAddElemMenu(props: MenuProps) {
}
const BaseTagsItem = (_props: any) => {
const [tags, setTags] = useState<any[] | null>(null);
const [tags, setTags] = useState<QueryBuilderTag[] | null>(null);
useEffect(() => {
(async () => {
@ -97,7 +105,7 @@ export function QBAddElemMenu(props: MenuProps) {
return tags ?
<>
{tags.filter((tag: any) => !tag.parentId).map((tag: any) => {
{tags.filter((tag: QueryBuilderTag) => !tag.parentId).map((tag: QueryBuilderTag) => {
return <TagItem tag={tag} allTags={tags} />
})}
</>

@ -22,7 +22,9 @@ export function QBNodeElem(props: NodeProps) {
} else {
ops.splice(idx, 1);
}
let newNode = simplify({ operands: ops, nodeOp: e.nodeOp }, null);
let newq = { operands: ops, nodeOp: e.nodeOp };
console.log("onReplace:", newq, simplify(newq, null));
let newNode = simplify(newq, null);
props.onReplace(newNode);
}

@ -19,6 +19,7 @@ export function QBPlaceholder(props: IProps & any) {
setAnchorEl(null);
};
const onCreate = (q: QueryElem) => {
console.log("Replacing placeholder by:", q);
props.onReplace(q);
};

@ -3,19 +3,15 @@ import { Box } from '@material-ui/core';
import QBQueryButton from './QBEditButton';
import { QBQueryElem } from './QBQueryElem';
import { QueryElem, addPlaceholders, removePlaceholders, simplify } from '../../lib/query/Query';
import { Tag, TagChildIds, TagParentId, Name, Id } from '../../api/api';
export interface TagItem {
name: string,
id: number,
childIds: number[],
parentId?: number,
}
export type QueryBuilderTag = (Tag & TagChildIds & TagParentId & Name & Id);
export interface Requests {
getArtists: (filter: string) => Promise<string[]>,
getAlbums: (filter: string) => Promise<string[]>,
getTrackNames: (filter: string) => Promise<string[]>,
getTags: () => Promise<TagItem[]>,
getTags: () => Promise<QueryBuilderTag[]>,
}
export interface IProps {

@ -1,7 +1,7 @@
import React, { useEffect, useReducer, useCallback } from 'react';
import { Box, LinearProgress, Typography } from '@material-ui/core';
import { QueryElem, QueryLeafBy, QueryLeafElem, QueryLeafOp } from '../../../lib/query/Query';
import QueryBuilder from '../../querybuilder/QueryBuilder';
import QueryBuilder, { QueryBuilderTag } from '../../querybuilder/QueryBuilder';
import { AlbumsTable, ArtistsTable, ColumnType, ItemsTable, TracksTable } from '../../tables/ResultsTable';
import { queryArtists, queryTracks, queryAlbums, queryTags } from '../../../lib/backend/queries';
import { WindowState } from '../Windows';
@ -87,13 +87,22 @@ async function getTrackNames(filter: string) {
return [...(new Set([...(tracks.map((s: any) => s.name))]))];
}
async function getTagItems(): Promise<any> {
let tags: any = await queryTags(
async function getTagItems(): Promise<QueryBuilderTag[]> {
let tags: QueryResponseTagDetails[] = (await queryTags(
undefined,
0, -1, QueryResponseType.Details
);
)) as QueryResponseTagDetails[];
// We need to resolve the child ids.
let tags_with_children : QueryBuilderTag[] = tags.map((t: QueryResponseTagDetails) => {
return {
...t,
childIds: tags.filter((t2: QueryResponseTagDetails) => t2.parentId === t.id)
.map((t2: QueryResponseTagDetails) => t2.id)
}
})
return tags;
return tags_with_children;
}
export interface FireNewQueriesData {

@ -1,28 +1,29 @@
import * as serverApi from '../../api/api';
export enum QueryFor {
Artists = 0,
Albums,
Tags,
Tracks,
Artists = "artists",
Albums = "albums",
Tags = "tags",
Tracks = "tracks",
}
export enum QueryLeafBy {
ArtistName = 0,
ArtistId,
AlbumName,
AlbumId,
TagInfo,
TagId,
TrackName,
TrackId,
StoreLinks,
ArtistName = "artistName",
ArtistId = "artistId",
AlbumName = "albumName",
AlbumId = "albumId",
TagInfo = "tagInfo",
TagId = "tagId",
TrackName = "trackName",
TrackId = "trackId",
StoreLinks = "storeLinks",
NotApplicable = "n/a", // Some query nodes don't need an operand.
}
export enum QueryLeafOp {
Equals = 0,
Like,
Placeholder, // Special op which indicates that this leaf is not filled in yet.
Equals = "equals",
Like = "like",
Placeholder = "placeholder", // Special op which indicates that this leaf is not filled in yet.
}
export interface TagQueryInfo {
@ -98,6 +99,7 @@ function mapToServerProperty(l: QueryLeafBy, queryFor: QueryFor | null) :
(queryFor == QueryFor.Tracks) ? serverApi.QueryElemProperty.trackStoreLinks :
null,
[QueryLeafBy.TagInfo]: null,
[QueryLeafBy.NotApplicable]: null,
}[l];
}
@ -129,9 +131,9 @@ export function addPlaceholders(
inNode: null | QueryNodeOp,
): QueryElem {
const makePlaceholder = () => {
const makePlaceholder : () => QueryElem = () => {
return {
a: 0,
a: QueryLeafBy.NotApplicable,
leafOp: QueryLeafOp.Placeholder,
b: ""
}
@ -226,12 +228,12 @@ export function simplify(q: QueryElem | null, queryFor: QueryFor | null): QueryE
}
// This shouldn't be part of simplification.
if (q && isLeafElem(q)) {
if (mapToServerLeafOp(q.leafOp, queryFor) === null ||
mapToServerProperty(q.a, queryFor) === null) {
return null;
}
}
// if (q && isLeafElem(q)) {
// if (mapToServerLeafOp(q.leafOp, queryFor) === null ||
// mapToServerProperty(q.a, queryFor) === null) {
// return null;
// }
// }
return q;
}

@ -1,5 +1,5 @@
import Knex from "knex";
import { Album, AlbumRefs, Id, Name, AlbumDetails, StoreLinks, Tag, TagRefs, Track, Artist } from "../../client/src/api/api";
import { Album, AlbumRefs, Id, Name, AlbumDetails, StoreLinks, Tag, TagParentId, Track, Artist } from "../../client/src/api/api";
import * as api from '../../client/src/api/api';
import asJson from "../lib/asJson";
import { DBError, DBErrorKind } from "../endpoints/types";
@ -14,7 +14,7 @@ export async function getAlbum(id: number, userId: number, knex: Knex):
// Start transfers for tracks, tags and artists.
// Also request the album itself.
const tagsPromise: Promise<(Tag & Id & Name & TagRefs)[]> =
const tagsPromise: Promise<(Tag & Id & Name & TagParentId)[]> =
knex.select('tagId')
.from('albums_tags')
.where({ 'albumId': id })
@ -23,8 +23,8 @@ export async function getAlbum(id: number, userId: number, knex: Knex):
knex.select(['id', 'name', 'parentId'])
.from('tags')
.whereIn('id', ids)
.then((tags: (Id & Name & TagRefs)[]) =>
tags.map((tag : (Id & Name & TagRefs)) =>
.then((tags: (Id & Name & TagParentId)[]) =>
tags.map((tag : (Id & Name & TagParentId)) =>
{ return {...tag, mbApi_typename: "tag"}}
))
);

@ -1,5 +1,5 @@
import Knex from "knex";
import { Artist, ArtistDetails, Tag, Track, TagRefs, Id, Name, StoreLinks, Album, ArtistRefs } from "../../client/src/api/api";
import { Artist, ArtistDetails, Tag, Track, TagParentId, Id, Name, StoreLinks, Album, ArtistRefs } from "../../client/src/api/api";
import * as api from '../../client/src/api/api';
import asJson from "../lib/asJson";
import { DBError, DBErrorKind } from "../endpoints/types";
@ -11,7 +11,7 @@ export async function getArtist(id: number, userId: number, knex: Knex):
Promise<(Artist & ArtistDetails & Name & StoreLinks)> {
// Start transfers for tags and albums.
// Also request the artist itself.
const tagsPromise: Promise<(Tag & Name & Id & TagRefs)[]> =
const tagsPromise: Promise<(Tag & Name & Id & TagParentId)[]> =
knex.select('tagId')
.from('artists_tags')
.where({ 'artistId': id })
@ -20,8 +20,8 @@ export async function getArtist(id: number, userId: number, knex: Knex):
knex.select(['id', 'name', 'parentId'])
.from('tags')
.whereIn('id', ids)
.then((tags: (Id & Name & TagRefs)[]) =>
tags.map((tag : (Id & Name & TagRefs)) =>
.then((tags: (Id & Name & TagParentId)[]) =>
tags.map((tag : (Id & Name & TagParentId)) =>
{ return {...tag, mbApi_typename: "tag"}}
))
);

@ -1,5 +1,5 @@
import Knex from "knex";
import { Track, TrackRefs, Id, Name, StoreLinks, Album, AlbumRefs, Artist, ArtistRefs, Tag, TagRefs, isTrackRefs, isAlbumRefs, DBImportResponse, IDMappings } from "../../client/src/api/api";
import { Track, TrackRefs, Id, Name, StoreLinks, Album, AlbumRefs, Artist, ArtistRefs, Tag, TagParentId, isTrackRefs, isAlbumRefs, DBImportResponse, IDMappings } from "../../client/src/api/api";
import * as api from '../../client/src/api/api';
import asJson from "../lib/asJson";
import { createArtist } from "./Artist";
@ -60,7 +60,7 @@ export async function exportDB(userId: number, knex: Knex): Promise<api.DBDataFo
}
}));
let tagsPromise: Promise<(Tag & Id & Name & TagRefs)[]> =
let tagsPromise: Promise<(Tag & Id & Name & TagParentId)[]> =
knex.select('name', 'parentId', 'id')
.from('tags')
.where({ 'user': userId })

@ -1,7 +1,7 @@
import Knex from "knex";
import { isConstructorDeclaration } from "typescript";
import * as api from '../../client/src/api/api';
import { Tag, TagRefs, TagDetails, Id, Name } from "../../client/src/api/api";
import { Tag, TagParentId, TagDetails, Id, Name } from "../../client/src/api/api";
import { DBError, DBErrorKind } from "../endpoints/types";
import { makeNotFoundError } from "./common";
@ -35,7 +35,7 @@ export async function getTagChildrenRecursive(id: number,
}
// Returns the id of the created tag.
export async function createTag(userId: number, tag: (Tag & Name & TagRefs), knex: Knex): Promise<number> {
export async function createTag(userId: number, tag: (Tag & Name & TagParentId), knex: Knex): Promise<number> {
return await knex.transaction(async (trx) => {
// If applicable, retrieve the parent tag.
const maybeMatches: any[] | null =
@ -124,13 +124,13 @@ export async function deleteTag(userId: number, tagId: number, knex: Knex) {
}
export async function getTag(userId: number, tagId: number, knex: Knex): Promise<(Tag & TagDetails & Name)> {
const tagPromise: Promise<(Tag & Id & Name & TagRefs) | null> =
const tagPromise: Promise<(Tag & Id & Name & TagParentId) | null> =
knex.select(['id', 'name', 'parentId'])
.from('tags')
.where({ 'user': userId })
.where({ 'id': tagId })
.then((r: (Id & Name & TagRefs)[] | undefined) => r ? r[0] : null)
.then((r: (Id & Name & TagRefs) | null) => {
.then((r: (Id & Name & TagParentId)[] | undefined) => r ? r[0] : null)
.then((r: (Id & Name & TagParentId) | null) => {
if (r) {
return { ...r, mbApi_typename: 'tag'};
}
@ -139,7 +139,7 @@ export async function getTag(userId: number, tagId: number, knex: Knex): Promise
const parentPromise: Promise<(Tag & Id & Name & TagDetails) | null> =
tagPromise
.then((r: (Tag & Id & Name & TagRefs) | null) =>
.then((r: (Tag & Id & Name & TagParentId) | null) =>
(r && r.parentId) ? (
getTag(userId, r.parentId, knex)
.then((rr: (Tag & Name & TagDetails) | null) =>

@ -1,5 +1,5 @@
import Knex from "knex";
import { Track, TrackRefs, TrackDetails, Id, Name, StoreLinks, Tag, Album, Artist, TagRefs } from "../../client/src/api/api";
import { Track, TrackRefs, TrackDetails, Id, Name, StoreLinks, Tag, Album, Artist, TagParentId } from "../../client/src/api/api";
import * as api from '../../client/src/api/api';
import asJson from "../lib/asJson";
import { DBError, DBErrorKind } from "../endpoints/types";
@ -11,7 +11,7 @@ export async function getTrack(id: number, userId: number, knex: Knex):
Promise<Track & Name & StoreLinks & TrackDetails> {
// Start transfers for tracks, tags and artists.
// Also request the track itself.
const tagsPromise: Promise<(Tag & Id & Name & TagRefs)[]> =
const tagsPromise: Promise<(Tag & Id & Name & TagParentId)[]> =
knex.select('tagId')
.from('tracks_tags')
.where({ 'trackId': id })
@ -20,8 +20,8 @@ export async function getTrack(id: number, userId: number, knex: Knex):
knex.select(['id', 'name', 'parentId'])
.from('tags')
.whereIn('id', ids)
.then((tags: (Id & Name & TagRefs)[]) =>
tags.map((tag : (Id & Name & TagRefs)) =>
.then((tags: (Id & Name & TagParentId)[]) =>
tags.map((tag : (Id & Name & TagParentId)) =>
{ return {...tag, mbApi_typename: "tag"}}
))
);

Loading…
Cancel
Save