diff --git a/.changeset/late-cows-impress.md b/.changeset/late-cows-impress.md new file mode 100644 index 000000000..d328c8702 --- /dev/null +++ b/.changeset/late-cows-impress.md @@ -0,0 +1,6 @@ +--- +"@cube-creator/ui": minor +"@cube-creator/shared-dimensions-api": minor +--- + +Searching and paging Shared Dimensions diff --git a/apis/shared-dimensions/bootstrap/entrypoint.ts b/apis/shared-dimensions/bootstrap/entrypoint.ts index 17574ebfa..0dc6152e3 100644 --- a/apis/shared-dimensions/bootstrap/entrypoint.ts +++ b/apis/shared-dimensions/bootstrap/entrypoint.ts @@ -5,5 +5,5 @@ import type { BootstrappedResourceFactory } from './index' export const entrypoint = (ptr: BootstrappedResourceFactory, ns: NamespaceBuilder) => ptr('').addOut(rdf.type, [hydra.Resource, md.Entrypoint]) - .addOut(md.sharedDimensions, ns('_term-sets?pageSize=1000')) + .addOut(md.sharedDimensions, ns('_term-sets?pageSize=20')) .addOut(md.hierarchies, ns('_hierarchies')) diff --git a/apis/shared-dimensions/bootstrap/shapes.ts b/apis/shared-dimensions/bootstrap/shapes.ts index 3fa87f2dc..5ee823237 100644 --- a/apis/shared-dimensions/bootstrap/shapes.ts +++ b/apis/shared-dimensions/bootstrap/shapes.ts @@ -11,6 +11,10 @@ const SharedDimensionUpdate = clownface({ dataset: $rdf.dataset() }) .namedNode(shape('shape/shared-dimension-update')) .addOut(rdf.type, sh.NodeShape) +const SharedDimensionSearch = clownface({ dataset: $rdf.dataset() }) + .namedNode(shape('shape/shared-dimension-search')) + .addOut(rdf.type, sh.NodeShape) + const SharedDimensionTermCreate = clownface({ dataset: $rdf.dataset() }) .namedNode(shape('shape/shared-dimension-term-create')) .addOut(rdf.type, sh.NodeShape) @@ -30,6 +34,7 @@ const HierarchyCreate = clownface({ dataset: $rdf.dataset() }) export default [ SharedDimensionCreate, SharedDimensionUpdate, + SharedDimensionSearch, SharedDimensionTermCreate, SharedDimensionTermUpdate, Hierarchy, diff --git a/apis/shared-dimensions/hydra/index.ttl b/apis/shared-dimensions/hydra/index.ttl index f8551f198..2c5fa46b4 100644 --- a/apis/shared-dimensions/hydra/index.ttl +++ b/apis/shared-dimensions/hydra/index.ttl @@ -1,3 +1,4 @@ +PREFIX owl: BASE @prefix rdfs: . @prefix schema: . @@ -41,8 +42,10 @@ md:SharedDimensions rdfs:subClassOf hydra:Collection ; hydra:supportedOperation [ - a hydra:Operation ; + a hydra:Operation, schema:DownloadAction ; hydra:method "GET" ; + hydra:title "Search" ; + hydra:expects ; code:implementedBy [ a code:EcmaScript ; @@ -51,7 +54,7 @@ md:SharedDimensions hydra-box:variables [ a hydra:IriTemplate ; - hydra:template "/_term-sets{?q,pageSize,page}" ; + hydra:template "/_term-sets{?q,pageSize,page,includeDeprecated}" ; hydra:mapping [ a hydra:IriTemplateMapping ; @@ -67,6 +70,11 @@ md:SharedDimensions a hydra:IriTemplateMapping ; hydra:property hydra:pageIndex ; hydra:variable "page" ; + ], + [ + a hydra:IriTemplateMapping ; + hydra:property owl:deprecated ; + hydra:variable "includeDeprecated" ; ] ; ] ; ], [ @@ -85,6 +93,7 @@ md:SharedDimensions a sh:NodeShape, sh:Shape . a sh:NodeShape, sh:Shape . + a sh:NodeShape, sh:Shape . md:Hierarchies a hydra:Class ; diff --git a/apis/shared-dimensions/lib/domain/hierarchies.ts b/apis/shared-dimensions/lib/domain/hierarchies.ts index 710cc568b..af73d2126 100644 --- a/apis/shared-dimensions/lib/domain/hierarchies.ts +++ b/apis/shared-dimensions/lib/domain/hierarchies.ts @@ -1,4 +1,4 @@ -import type { NamedNode } from '@rdfjs/types' +import type { NamedNode, Quad } from '@rdfjs/types' import { CONSTRUCT, SELECT } from '@tpluscode/sparql-builder' import { md, meta } from '@cube-creator/core/namespace' import clownface, { GraphPointer } from 'clownface' @@ -7,9 +7,11 @@ import httpError from 'http-errors' import $rdf from 'rdf-ext' import slugify from 'slugify' import { DomainError } from '@cube-creator/api-errors' +import { ParsingClient } from 'sparql-http-client/ParsingClient' import { SharedDimensionsStore } from '../store' import env from '../env' import { textSearch } from '../query' +import { CollectionData } from '../handlers/collection' import { newId, replace } from './resource' interface GetHierarchies { @@ -18,7 +20,7 @@ interface GetHierarchies { offset: number } -export function getHierarchies({ freetextQuery, limit, offset }: GetHierarchies) { +export async function getHierarchies({ freetextQuery, limit, offset }: GetHierarchies, client: ParsingClient): Promise>> { const hierarchy = $rdf.variable('hierarchy') const name = $rdf.variable('name') @@ -35,10 +37,11 @@ export function getHierarchies({ freetextQuery, limit, offset }: GetHierarchies) .ORDER().BY(name) } - return CONSTRUCT` + return { + members: await CONSTRUCT` ?proxyUrl ?p ?o . ` - .WHERE` + .WHERE` { ${select} } @@ -46,7 +49,9 @@ export function getHierarchies({ freetextQuery, limit, offset }: GetHierarchies) ${hierarchy} ?p ?o . BIND(IRI(CONCAT("${env.MANAGED_DIMENSIONS_API_BASE}", "dimension/_hierarchy/proxy?id=", ENCODE_FOR_URI(STR(${hierarchy})))) AS ?proxyUrl) - ` + `.execute(client.query), + totalItems: 0, + } } interface CreateHierarchy { diff --git a/apis/shared-dimensions/lib/domain/shared-dimensions.ts b/apis/shared-dimensions/lib/domain/shared-dimensions.ts index 306afb34b..69dba6b8a 100644 --- a/apis/shared-dimensions/lib/domain/shared-dimensions.ts +++ b/apis/shared-dimensions/lib/domain/shared-dimensions.ts @@ -1,8 +1,8 @@ import path from 'path' -import type { Quad, Stream, Term } from '@rdfjs/types' +import type { Quad, Stream, Term, Literal, NamedNode } from '@rdfjs/types' import { hydra, rdf, schema, sh } from '@tpluscode/rdf-ns-builders' import $rdf from 'rdf-ext' -import { toRdf } from 'rdf-literal' +import { toRdf, fromRdf } from 'rdf-literal' import { fromFile } from 'rdf-utils-fs' import clownface from 'clownface' import { isResource } from 'is-graph-pointer' @@ -11,18 +11,21 @@ import { ParsingClient } from 'sparql-http-client/ParsingClient' import { md } from '@cube-creator/core/namespace' import env from '../env' import shapeToQuery, { rewriteTemplates } from '../shapeToQuery' +import { CollectionData } from '../handlers/collection' import { getDynamicProperties } from './shared-dimension' interface GetSharedDimensions { freetextQuery?: string limit?: number offset?: number + includeDeprecated?: Literal } -export async function getSharedDimensions(client: StreamClient, { freetextQuery = '', limit = 10, offset = 0 }: GetSharedDimensions = {}): Promise { +export async function getSharedDimensions(client: StreamClient, { freetextQuery = '', limit = 10, offset = 0, includeDeprecated }: GetSharedDimensions = {}): Promise>> { const { constructQuery } = await shapeToQuery() - const shape = await loadShape('dimensions-query-shape') + const memberQueryShape = await loadShape('dimensions-query-shape', md.MembersQueryShape) + const totalQueryShape = await loadShape('dimensions-query-shape', md.CountQueryShape) const { MANAGED_DIMENSIONS_BASE } = env const variables = new Map(Object.entries({ @@ -30,11 +33,13 @@ export async function getSharedDimensions(client: StreamClient, { freetextQuery limit, offset, freetextQuery, + includeDeprecated, orderBy: schema.name, })) - await rewriteTemplates(shape, variables) + await rewriteTemplates(memberQueryShape, variables) + await rewriteTemplates(totalQueryShape, variables) - const dataset = await $rdf.dataset().import(await constructQuery(shape).execute(client)) + const dataset = await $rdf.dataset().import(await client.query.construct(constructQuery(memberQueryShape))) clownface({ dataset }) .has(rdf.type, schema.DefinedTermSet) .forEach(termSet => { @@ -42,7 +47,14 @@ export async function getSharedDimensions(client: StreamClient, { freetextQuery termSet.addOut(md.terms, $rdf.namedNode(`${MANAGED_DIMENSIONS_BASE}dimension/_terms?dimension=${termSet.value}`)) }) - return dataset.toArray() + const totalItems = clownface({ + dataset: await $rdf.dataset().import(await client.query.construct(constructQuery(totalQueryShape))), + }).has(hydra.totalItems).out(hydra.totalItems).term as Literal + + return { + members: dataset, + totalItems: fromRdf(totalItems), + } } interface GetSharedTerms { @@ -53,7 +65,7 @@ interface GetSharedTerms { validThrough?: Date } -export async function getSharedTerms({ sharedDimensions, freetextQuery, validThrough, limit = 10, offset = 0 }: GetSharedTerms, client: C): Promise { +export async function getSharedTerms({ sharedDimensions, freetextQuery, validThrough, limit = 10, offset = 0 }: GetSharedTerms, client: C): Promise> { const shape = await loadShape('terms-query-shape') shape.addOut(sh.targetNode, sharedDimensions) @@ -86,17 +98,19 @@ export async function getSharedTerms({ s } const { constructQuery } = await shapeToQuery() - return constructQuery(shape).execute(client, { - operation: 'postDirect', - }) as any + return { + members: await client.query.construct(constructQuery(shape), { + operation: 'postDirect', + }) as any, + } } -async function loadShape(shape: string) { +async function loadShape(shape: string, shapeType: NamedNode = sh.NodeShape) { const dataset = await $rdf.dataset().import(fromFile(path.resolve(__dirname, `../shapes/${shape}.ttl`))) const ptr = clownface({ dataset, - }).has(rdf.type, sh.NodeShape) + }).has(rdf.type, shapeType) if (!isResource(ptr)) { throw new Error('Expected a single blank node or named node') diff --git a/apis/shared-dimensions/lib/handlers/collection.ts b/apis/shared-dimensions/lib/handlers/collection.ts index f729e2941..a9028e0d2 100644 --- a/apis/shared-dimensions/lib/handlers/collection.ts +++ b/apis/shared-dimensions/lib/handlers/collection.ts @@ -1,18 +1,23 @@ -import type { NamedNode, Quad } from '@rdfjs/types' +import type { NamedNode, Quad, Stream } from '@rdfjs/types' import $rdf from 'rdf-ext' import clownface, { GraphPointer } from 'clownface' import { hydra, rdf } from '@tpluscode/rdf-ns-builders' +export interface CollectionData = Stream | Iterable> { + members: M + totalItems?: number +} + interface CollectionHandler { memberType: NamedNode collectionType: NamedNode view?: NamedNode - memberQuads: Quad[] + data: CollectionData> collection: NamedNode } -export function getCollection({ collection, view, memberQuads, memberType, collectionType }: CollectionHandler): GraphPointer { - const dataset = $rdf.dataset(memberQuads) +export function getCollection({ collection, view, data: { members: memberQuads, totalItems }, memberType, collectionType }: CollectionHandler): GraphPointer { + const dataset = $rdf.dataset([...memberQuads]) const graph = clownface({ dataset }) const members = graph.has(rdf.type, memberType) @@ -20,7 +25,12 @@ export function getCollection({ collection, view, memberQuads, memberType, colle graph.node(collection) .addOut(rdf.type, [hydra.Collection, collectionType]) .addOut(hydra.member, members) - .addOut(hydra.totalItems, members.terms.length) + + if (totalItems) { + graph.node(collection).addOut(hydra.totalItems, totalItems) + } else { + graph.node(collection).addOut(hydra.totalItems, members.terms.length) + } if (view) { graph.node(view) diff --git a/apis/shared-dimensions/lib/handlers/hierarchies.ts b/apis/shared-dimensions/lib/handlers/hierarchies.ts index b689c8ad7..ec5ddb539 100644 --- a/apis/shared-dimensions/lib/handlers/hierarchies.ts +++ b/apis/shared-dimensions/lib/handlers/hierarchies.ts @@ -27,7 +27,7 @@ export const get = asyncMiddleware(async (req, res, next) => { offset, } const collection = await getCollection({ - memberQuads: await getHierarchies(queryParams).execute(parsingClient.query), + data: await getHierarchies(queryParams, parsingClient), collectionType: md.Hierarchies, memberType: md.Hierarchy, collection: req.hydra.resource.term, diff --git a/apis/shared-dimensions/lib/handlers/hierarchy.ts b/apis/shared-dimensions/lib/handlers/hierarchy.ts index 9605716d4..3e8d8d555 100644 --- a/apis/shared-dimensions/lib/handlers/hierarchy.ts +++ b/apis/shared-dimensions/lib/handlers/hierarchy.ts @@ -56,7 +56,7 @@ export const getExternal = asyncMiddleware(async (req, res) => { }) const hierarchy = clownface({ - dataset: $rdf.dataset(await query.execute(parsingClient)), + dataset: $rdf.dataset(await parsingClient.query.construct(query)), }).namedNode(url) ensureEndpoint(hierarchy) diff --git a/apis/shared-dimensions/lib/handlers/shared-dimensions.ts b/apis/shared-dimensions/lib/handlers/shared-dimensions.ts index 00047987f..3f591d0df 100644 --- a/apis/shared-dimensions/lib/handlers/shared-dimensions.ts +++ b/apis/shared-dimensions/lib/handlers/shared-dimensions.ts @@ -1,12 +1,12 @@ import type { Term } from '@rdfjs/types' -import { hydra, oa, schema } from '@tpluscode/rdf-ns-builders' +import { hydra, oa, owl, schema, xsd } from '@tpluscode/rdf-ns-builders' import { asyncMiddleware } from 'middleware-async' import { protectedResource } from '@hydrofoil/labyrinth/resource' import { Enrichment } from '@hydrofoil/labyrinth/lib/middleware/preprocessResource' import httpError from 'http-errors' import clownface, { GraphPointer } from 'clownface' import $rdf from 'rdf-ext' -import { md } from '@cube-creator/core/namespace' +import * as ns from '@cube-creator/core/namespace' import conditional from 'express-conditional-middleware' import { isMultipart } from '@cube-creator/express/multipart' import { shaclValidate } from '../middleware/shacl' @@ -29,19 +29,26 @@ export const get = asyncMiddleware(async (req, res, next) => { const offset = (page - 1) * pageSize const queryParams = { freetextQuery: query.has(hydra.freetextQuery).out(hydra.freetextQuery).value, - validThrough: query.has(md.onlyValidTerms, query.literal(true)).terms.length ? new Date() : undefined, + validThrough: query.has(ns.md.onlyValidTerms, query.literal(true)).terms.length ? new Date() : undefined, limit: pageSize, offset, + includeDeprecated: $rdf.literal(query.has(owl.deprecated).out(owl.deprecated).value || 'false', xsd.boolean), } const collection = getCollection({ view: $rdf.namedNode(req.absoluteUrl()), - memberQuads: await getSharedDimensions(streamClient, queryParams), - collectionType: md.SharedDimensions, + data: await getSharedDimensions(streamClient, queryParams), + collectionType: ns.md.SharedDimensions, memberType: schema.DefinedTermSet, collection: req.hydra.resource.term, }) + collection.addOut(ns.query.templateMappings, templateMappings => { + for (const { predicate, object } of query.dataset) { + templateMappings.addOut(predicate, object) + } + }) + return res.dataset(collection.dataset) }) @@ -83,15 +90,15 @@ export const getTerms = asyncMiddleware(async (req, res, next) => { const queryParams = { sharedDimensions: sharedDimensions.map(rewriteTerm), freetextQuery: query.has(hydra.freetextQuery).out(hydra.freetextQuery).value, - validThrough: query.has(md.onlyValidTerms, query.literal(true)).terms.length ? new Date() : undefined, + validThrough: query.has(ns.md.onlyValidTerms, query.literal(true)).terms.length ? new Date() : undefined, limit: pageSize, offset, } const collection = getCollection({ - memberQuads: await getSharedTerms(queryParams, parsingClient), + data: await getSharedTerms(queryParams, parsingClient), memberType: schema.DefinedTerm, - collectionType: md.SharedDimensionTerms, + collectionType: ns.md.SharedDimensionTerms, collection: termsCollectionId(sharedDimensions, queryParams.freetextQuery), }) @@ -119,9 +126,9 @@ const postDirect = protectedResource(shaclValidate, asyncMiddleware(async (req, export const post = conditional(isMultipart, postImportedDimension, postDirect) export const injectTermsLink: Enrichment = async (req, pointer) => { - pointer.deleteOut(md.terms).addOut(md.terms, termsCollectionId([pointer.term])) + pointer.deleteOut(ns.md.terms).addOut(ns.md.terms, termsCollectionId([pointer.term])) } export const injectExportLink: Enrichment = async (req, pointer) => { - pointer.deleteOut(md.export).addOut(md.export, pointer.namedNode(`${env.MANAGED_DIMENSIONS_BASE}dimension/_export?dimension=${pointer.value}`)) + pointer.deleteOut(ns.md.export).addOut(ns.md.export, pointer.namedNode(`${env.MANAGED_DIMENSIONS_BASE}dimension/_export?dimension=${pointer.value}`)) } diff --git a/apis/shared-dimensions/lib/namespace.ts b/apis/shared-dimensions/lib/namespace.ts index f17afaf01..8411e002d 100644 --- a/apis/shared-dimensions/lib/namespace.ts +++ b/apis/shared-dimensions/lib/namespace.ts @@ -3,6 +3,7 @@ import env from './env' type Shapes = 'shape/shared-dimension-create' | 'shape/shared-dimension-update' +| 'shape/shared-dimension-search' | 'shape/shared-dimension-term-create' | 'shape/shared-dimension-term-update' | 'shape/hierarchy-create' diff --git a/apis/shared-dimensions/lib/shapeToQuery.ts b/apis/shared-dimensions/lib/shapeToQuery.ts index 9a8bc7dc1..043be862d 100644 --- a/apis/shared-dimensions/lib/shapeToQuery.ts +++ b/apis/shared-dimensions/lib/shapeToQuery.ts @@ -1,14 +1,14 @@ import onetime from 'onetime' import { md } from '@cube-creator/core/namespace' -import { AnyPointer, GraphPointer } from 'clownface' +import clownface, { AnyPointer, GraphPointer } from 'clownface' import { isGraphPointer } from 'is-graph-pointer' -import { hydra } from '@tpluscode/rdf-ns-builders' +import { hydra, sh } from '@tpluscode/rdf-ns-builders' import { Parameters, PropertyShape } from '@hydrofoil/shape-to-query/model/constraint/ConstraintComponent' import evalTemplateLiteral from 'rdf-loader-code/evalTemplateLiteral.js' import namespace from '@rdfjs/namespace' -import { sparql } from '@tpluscode/sparql-builder' import $rdf from 'rdf-ext' import type { Literal } from '@rdfjs/types' +import type { ServicePattern, GroupPattern } from 'sparqljs' import env from './env' /* @@ -21,7 +21,11 @@ const _importDynamic = new Function('modulePath', 'return import(modulePath)') export default async function shapeToQuery(): Promise> { await setup() - const { constructQuery, deleteQuery, s2q } = await _importDynamic('@hydrofoil/shape-to-query') + const { + constructQuery, + deleteQuery, + s2q, + } = await _importDynamic('@hydrofoil/shape-to-query') as typeof import('@hydrofoil/shape-to-query') return { constructQuery, @@ -58,11 +62,13 @@ export async function rewriteTemplates(shape: AnyPointer, variables: Map { shape.dataset.delete(quad) - shape.dataset.add($rdf.quad(quad.subject, quad.predicate, value, quad.graph)) + if (value) { + shape.dataset.add($rdf.quad(quad.subject, quad.predicate, value, quad.graph)) + } }) templateNode.deleteOut() @@ -74,9 +80,9 @@ const setup = onetime(async () => { }) async function defineConstraintComponents() { - const { default: ConstraintComponent } = await _importDynamic('@hydrofoil/shape-to-query/model/constraint/ConstraintComponent.js') - const { constraintComponents } = await _importDynamic('@hydrofoil/shape-to-query/model/constraint/index.js') - const { PatternConstraintComponent } = await _importDynamic('@hydrofoil/shape-to-query/model/constraint/pattern.js') + const { default: ConstraintComponent } = await _importDynamic('@hydrofoil/shape-to-query/model/constraint/ConstraintComponent.js') as typeof import('@hydrofoil/shape-to-query/model/constraint/ConstraintComponent.js') + const { constraintComponents } = await _importDynamic('@hydrofoil/shape-to-query/model/constraint/index.js') as typeof import('@hydrofoil/shape-to-query/model/constraint/index.js') + const { PatternConstraintComponent } = await _importDynamic('@hydrofoil/shape-to-query/model/constraint/pattern.js') as typeof import('@hydrofoil/shape-to-query/model/constraint/pattern.js') constraintComponents.set(md.FreeTextSearchConstraintComponent, class TextSearch extends ConstraintComponent { static match(pointer: GraphPointer) { @@ -99,7 +105,7 @@ async function defineConstraintComponents() { yield new TextSearch('fuseki', patternElement.pointer.value) break default: - yield new PatternConstraintComponent('^' + patternElement.pointer.value) + yield new PatternConstraintComponent(patternElement.pointer.term as Literal) } } } @@ -108,29 +114,66 @@ async function defineConstraintComponents() { super(md.FreeTextSearchConstraintComponent) } - buildPatterns({ focusNode, valueNode, propertyPath }: Parameters): any { + buildPropertyShapePatterns(args: Parameters) { if (this.vendor === 'stardog') { - const fts = namespace('tag:stardog:api:search:') - return sparql` - service ${fts.textMatch} { - [] ${fts.query} """${this.pattern + '*'}"""; - ${fts.result} ${valueNode} ; - } - ${focusNode} ${propertyPath} ${valueNode} . - ` + return [this.stardogServiceGroup(args)] } if (this.vendor === 'fuseki') { - return sparql` - ${focusNode} (${propertyPath} """${this.pattern + '*'}""") . - - # Second filtering to make sure the word starts with the given query - ${focusNode} ${propertyPath} ${valueNode} . - FILTER (REGEX(${valueNode}, "^${this.pattern}", "i")) - ` + return [this.fusekiPatterns(args)] } throw new Error('Unsupported vendor') } + + stardogServiceGroup({ focusNode, valueNode, propertyPath }: Parameters): ServicePattern { + if (!propertyPath || !('value' in propertyPath)) { + throw new Error('Property path must be a named node') + } + + const fts = namespace('tag:stardog:api:search:') + + const patterns = clownface({ dataset: $rdf.dataset() }) + .blankNode() + .addOut(fts.query, $rdf.literal(this.pattern + '*')) + .addOut(fts.result, valueNode) + .node(focusNode).addOut(propertyPath, valueNode) + + return { + type: 'service', + name: fts.textMatch, + silent: false, + patterns: [{ + type: 'bgp', + triples: [...patterns.dataset], + }], + } + } + + fusekiPatterns({ focusNode, valueNode, propertyPath }: Parameters): GroupPattern { + if (!propertyPath || !('value' in propertyPath)) { + throw new Error('Property path must be a named node') + } + + const patterns = clownface({ dataset: $rdf.dataset() }) + .node(focusNode) + .addList($rdf.namedNode('http://jena.apache.org/text#query'), [propertyPath, $rdf.literal(this.pattern + '*')]) + .addOut(propertyPath, valueNode) // Second filtering to make sure the word starts with the given query + + return { + type: 'group', + patterns: [{ + type: 'bgp', + triples: [...patterns.dataset], + }, { + type: 'filter', + expression: { + type: 'operation', + operator: 'regex', + args: [valueNode, $rdf.literal('^' + this.pattern), $rdf.literal('i')], + }, + }], + } + } }) } diff --git a/apis/shared-dimensions/lib/shapes/dimensions-query-shape.ttl b/apis/shared-dimensions/lib/shapes/dimensions-query-shape.ttl index bdefe3bc9..244bea78b 100644 --- a/apis/shared-dimensions/lib/shapes/dimensions-query-shape.ttl +++ b/apis/shared-dimensions/lib/shapes/dimensions-query-shape.ttl @@ -1,3 +1,4 @@ +PREFIX hydra: PREFIX xsd: @prefix sh: . @prefix rdf: . @@ -8,7 +9,26 @@ PREFIX sparql: prefix md: [ - a sh:NodeShape ; + a sh:NodeShape, md:CountQueryShape ; + sh:rule + [ + sh:subject hydra:Collection ; + sh:predicate hydra:totalItems ; + sh:object + [ + sh:count + [ + sh:distinct + [ + sh:filterShape _:FilterShape ; + ] ; + ] + ] ; + ] ; +] . + +[ + a sh:NodeShape, md:MembersQueryShape ; sh:target [ a s2q:NodeExpressionTarget ; @@ -25,23 +45,7 @@ prefix md: sh:orderBy [ sh:path [ s2q:variable "orderBy" ] ] ; sh:nodes [ - sh:filterShape - [ - sh:property - [ - sh:path schema:name ; - sh:pattern - [ - s2q:template "^${freetextQuery}" ; - ] ; - sh:flags "i" ; - ] ; - sh:property - [ - sh:path rdf:type ; - sh:hasValue schema:DefinedTermSet, meta:SharedDimension ; - ] ; - ] ; + sh:filterShape _:FilterShape ; ] ; ] ; ] ; @@ -101,3 +105,40 @@ prefix md: ] ] ; ] . + +_:validThrough + s2q:optional + [ + sh:path schema:validThrough ; + ] ; +. + +_:FilterShape + sh:property + [ + sh:path schema:name ; + sh:pattern + [ + s2q:template "^${freetextQuery}" ; + ] ; + sh:flags "i" ; + ] ; + sh:property + [ + sh:path rdf:type ; + sh:hasValue schema:DefinedTermSet, meta:SharedDimension ; + ] ; + sh:expression + [ + sh:deactivated + [ + s2q:variable "includeDeprecated" ; + sh:defaultValue false + ] ; + sparql:or + ( + [ sparql:not ( [ sparql:bound ( _:validThrough ) ] ) ] + [ sparql:gt ( _:validThrough [ sparql:now () ] ) ] + ) ; + ] ; +. diff --git a/apis/shared-dimensions/lib/shapes/index.ts b/apis/shared-dimensions/lib/shapes/index.ts index 32e8e6bfc..dc16266ce 100644 --- a/apis/shared-dimensions/lib/shapes/index.ts +++ b/apis/shared-dimensions/lib/shapes/index.ts @@ -45,6 +45,7 @@ function entry(id: NamedNode, init: (graph: AnyPointer) => Initializer([ entry(shape['shape/shared-dimension-create'], sharedDimension.create), entry(shape['shape/shared-dimension-update'], sharedDimension.update), + entry(shape['shape/shared-dimension-search'], sharedDimension.search), entry(shape['shape/shared-dimension-term-create'], dimensionTerm.create), entry(shape['shape/shared-dimension-term-update'], dimensionTerm.update), entry(shape['shape/hierarchy-create'], hierarchy()), diff --git a/apis/shared-dimensions/lib/shapes/shared-dimension.ts b/apis/shared-dimensions/lib/shapes/shared-dimension.ts index 3ae9d9295..2455376d8 100644 --- a/apis/shared-dimensions/lib/shapes/shared-dimension.ts +++ b/apis/shared-dimensions/lib/shapes/shared-dimension.ts @@ -9,7 +9,7 @@ import { editor, iso6391, md, meta, sh1 } from '@cube-creator/core/namespace' import { fromPointer as nodeShape } from '@rdfine/shacl/lib/NodeShape' import { fromPointer as propertyGroup } from '@rdfine/shacl/lib/PropertyGroup' import { fromPointer as resource } from '@rdfine/rdfs/lib/Resource' -import { dcat } from '@tpluscode/rdf-ns-builders' +import { dcat, owl } from '@tpluscode/rdf-ns-builders' const defaultGroup = $rdf.namedNode('#default-group') const i14yGroup = $rdf.namedNode('#i14y-group') @@ -429,3 +429,24 @@ export const update = (): Initializer => ({ }, ], }) + +export const search = (): Initializer => ({ + property: [ + { + path: hydra.freetextQuery, + name: 'Name', + minCount: 1, + maxCount: 1, + order: 1, + }, + { + path: owl.deprecated, + name: 'Show deprecated', + minCount: 1, + maxCount: 1, + defaultValue: false, + datatype: xsd.boolean, + order: 2, + }, + ], +}) diff --git a/apis/shared-dimensions/lib/store/index.ts b/apis/shared-dimensions/lib/store/index.ts index c1b334846..401d07f57 100644 --- a/apis/shared-dimensions/lib/store/index.ts +++ b/apis/shared-dimensions/lib/store/index.ts @@ -30,10 +30,11 @@ export default class implements SharedDimensionsStore { const { constructQuery } = await shapeToQuery() const query = constructQuery(shape, { focusNode: term, - }).FROM(this.graph) + }) - const quads = await query.execute(this.client, { + const quads = await this.client.query.construct(query, { operation: 'postDirect', + defaultGraphUri: [this.graph], }) return clownface({ dataset: $rdf.dataset(quads), @@ -59,7 +60,7 @@ export default class implements SharedDimensionsStore { async delete(id: NamedNode): Promise { const shape = await this.getShape(id) - await (await this.deleteQuery(shape, id)).execute(this.client) + await this.client.query.update(await this.deleteQuery(shape, id)) } private async deleteQuery(shape: GraphPointer, focusNode: NamedNode) { diff --git a/apis/shared-dimensions/package.json b/apis/shared-dimensions/package.json index ab45e02bb..0db7eb461 100644 --- a/apis/shared-dimensions/package.json +++ b/apis/shared-dimensions/package.json @@ -7,7 +7,7 @@ "@cube-creator/core": "1.0.0", "@cube-creator/express": "0.0.0", "@hydrofoil/labyrinth": "^0.4.2", - "@hydrofoil/shape-to-query": "^0.12", + "@hydrofoil/shape-to-query": "^0.13.5", "@rdfine/hydra": "^0.8.2", "@rdfine/rdfs": "^0.6.4", "@rdfine/schema": "^0.6.3", diff --git a/apis/shared-dimensions/test/lib/domain/managed-dimensions.test.ts b/apis/shared-dimensions/test/lib/domain/managed-dimensions.test.ts index d1c9c6b62..7272eccbf 100644 --- a/apis/shared-dimensions/test/lib/domain/managed-dimensions.test.ts +++ b/apis/shared-dimensions/test/lib/domain/managed-dimensions.test.ts @@ -3,7 +3,7 @@ import { expect } from 'chai' import $rdf from 'rdf-ext' import { mdClients } from '@cube-creator/testing/lib' import { insertTestDimensions } from '@cube-creator/testing/lib/seedData' -import { rdf, rdfs, schema } from '@tpluscode/rdf-ns-builders' +import { rdf, rdfs, schema, xsd } from '@tpluscode/rdf-ns-builders' import { getSharedDimensions, getSharedTerms } from '../../../lib/domain/shared-dimensions' describe('@cube-creator/shared-dimensions-api/lib/domain/shared-dimensions @SPARQL', () => { @@ -13,26 +13,52 @@ describe('@cube-creator/shared-dimensions-api/lib/domain/shared-dimensions @SPAR describe('getSharedDimensions', () => { it('returns from all graphs', async () => { + // given + const collectionData = await getSharedDimensions(mdClients.streamClient) + // when const dataset = $rdf.dataset([ - ...await getSharedDimensions(mdClients.streamClient), + ...collectionData.members, ]) // then const termSets = [...dataset.match(null, rdf.type, schema.DefinedTermSet)].map(({ subject }) => subject) expect(termSets).to.deep.contain.members([ - $rdf.namedNode('http://example.com/dimension/colors'), + $rdf.namedNode('https://ld.admin.ch/cube/dimension/technologies'), $rdf.namedNode('http://example.com/dimension/countries'), $rdf.namedNode('http://example.com/dimension/chemicals'), ]) }) it('returns filtered by name', async () => { + // given + const collectionData = await getSharedDimensions(mdClients.streamClient, { + freetextQuery: 'techno', + }) + + // when + const dataset = $rdf.dataset([ + ...collectionData.members, + ]) + + // then + const termSets = [...dataset.match(null, rdf.type, schema.DefinedTermSet)].map(({ subject }) => subject) + expect(termSets).to.have.length(1) + expect(termSets).to.deep.contain.members([ + $rdf.namedNode('https://ld.admin.ch/cube/dimension/technologies'), + ]) + }) + + it('returns filtered to include deprecated', async () => { + // given + const collectionData = await getSharedDimensions(mdClients.streamClient, { + freetextQuery: 'colors', + includeDeprecated: $rdf.literal('true', xsd.boolean), + }) + // when const dataset = $rdf.dataset([ - ...await getSharedDimensions(mdClients.streamClient, { - freetextQuery: 'colors', - }), + ...collectionData.members, ]) // then @@ -51,10 +77,11 @@ describe('@cube-creator/shared-dimensions-api/lib/domain/shared-dimensions @SPAR sharedDimensions: [$rdf.namedNode('http://example.com/dimension/colors')], freetextQuery: undefined, } + const collectionData = await getSharedTerms(search, mdClients.streamClient) // when const dataset = await $rdf.dataset() - .import(await getSharedTerms(search, mdClients.streamClient)) + .import(collectionData.members) // then expect(dataset.match(null, rdf.type, schema.DefinedTerm)) @@ -82,10 +109,11 @@ describe('@cube-creator/shared-dimensions-api/lib/domain/shared-dimensions @SPAR sharedDimensions: [$rdf.namedNode('https://ld.admin.ch/cube/dimension/technologies')], freetextQuery: 'sparql', } + const collectionData = await getSharedTerms(search, mdClients.streamClient) // when const dataset = await $rdf.dataset() - .import(await getSharedTerms(search, mdClients.streamClient)) + .import(collectionData.members) // then expect(dataset.match(null, rdf.type, schema.DefinedTerm)) @@ -118,10 +146,11 @@ describe('@cube-creator/shared-dimensions-api/lib/domain/shared-dimensions @SPAR freetextQuery: undefined, limit: 1, } + const collectionData = await getSharedTerms(search, mdClients.streamClient) // when const dataset = await $rdf.dataset() - .import(await getSharedTerms(search, mdClients.streamClient)) + .import(collectionData.members) // then expect(dataset.match(null, rdf.type, schema.DefinedTerm)) @@ -134,9 +163,11 @@ describe('@cube-creator/shared-dimensions-api/lib/domain/shared-dimensions @SPAR sharedDimensions: [$rdf.namedNode('http://example.com/dimension/colors')], freetextQuery: 'r', } + const collectionData = await getSharedTerms(search, mdClients.streamClient) + // when const dataset = await $rdf.dataset() - .import(await getSharedTerms(search, mdClients.streamClient)) + .import(collectionData.members) // then const [term, ...more] = dataset.match(null, rdf.type, schema.DefinedTerm) @@ -151,9 +182,11 @@ describe('@cube-creator/shared-dimensions-api/lib/domain/shared-dimensions @SPAR freetextQuery: undefined, validThrough: new Date(Date.parse('2021-04-15')), } + const collectionData = await getSharedTerms(search, mdClients.streamClient) + // when const dataset = await $rdf.dataset() - .import(await getSharedTerms(search, mdClients.streamClient)) + .import(collectionData.members) // then const terms = [...dataset.match(null, rdf.type, schema.DefinedTerm)].map(({ subject }) => subject) diff --git a/apis/shared-dimensions/test/lib/loader.test.ts b/apis/shared-dimensions/test/lib/loader.test.ts index 864909fbe..5220acedc 100644 --- a/apis/shared-dimensions/test/lib/loader.test.ts +++ b/apis/shared-dimensions/test/lib/loader.test.ts @@ -5,6 +5,7 @@ import { INSERT } from '@tpluscode/sparql-builder' import { mdClients } from '@cube-creator/testing/lib' import namespace from '@rdfjs/namespace' import { hydra, schema, sh } from '@tpluscode/rdf-ns-builders' +import { md } from '@cube-creator/core/namespace' import Loader from '../../lib/loader' const ex = namespace('http://example.com/') @@ -12,7 +13,7 @@ const graph = ex('shared-dimensions') const testResources = INSERT.DATA` graph ${graph} { ${ex.foo} - a ${hydra.Resource} ; + a ${md.SharedDimension} ; ${schema.name} "Yes" ; ${ex.hidden} "foo" ; . @@ -53,7 +54,7 @@ describe('shared-dimensions/lib/loader @SPARQL', () => { // then expect(resource.types).to.have.property('size', 1) expect([...resource.types]).to.deep.contain.members([ - hydra.Resource, + md.SharedDimension, ]) }) diff --git a/fuseki/shared-dimensions.trig b/fuseki/shared-dimensions.trig index 29aacf1c9..ae53e8b30 100644 --- a/fuseki/shared-dimensions.trig +++ b/fuseki/shared-dimensions.trig @@ -145,6 +145,7 @@ graph { a schema:DefinedTermSet, meta:SharedDimension ; schema:validFrom "2021-01-20T23:59:59Z"^^xsd:dateTime ; + schema:validThrough "2021-04-20T23:59:59Z"^^xsd:dateTime ; schema:name "Colors"@en, "Farben"@de, "I colori"@it, "Les couleurs"@fr ; . diff --git a/package.json b/package.json index 1ca6727ca..1e847edd5 100644 --- a/package.json +++ b/package.json @@ -78,7 +78,8 @@ ] }, "resolutions": { - "@types/eslint": "^8" + "@types/eslint": "^8", + "@zazuko/env": "2.2.0" }, "lint-staged": { "*.{js,ts,vue}": [ diff --git a/packages/core/namespace.ts b/packages/core/namespace.ts index 666a116e0..1d97bec33 100644 --- a/packages/core/namespace.ts +++ b/packages/core/namespace.ts @@ -116,7 +116,9 @@ type SharedDimensionsTerms = 'Hierarchy' | 'HierarchyProxy' | 'Entrypoint' | - 'FreeTextSearchConstraintComponent' + 'FreeTextSearchConstraintComponent' | + 'MembersQueryShape' | + 'CountQueryShape' prefixes.view = 'https://cube.link/view/' diff --git a/packages/model/Project.ts b/packages/model/Project.ts index 7906e89ad..f08902027 100644 --- a/packages/model/Project.ts +++ b/packages/model/Project.ts @@ -42,8 +42,8 @@ export interface ImportProject extends Project { export type CubeProject = CsvProject | ImportProject +// eslint-disable-next-line @typescript-eslint/no-empty-interface export interface ProjectsCollection extends Collection { - searchParams: GraphPointer } export const isCsvProject = (project: CsvProject | ImportProject): project is CsvProject => { diff --git a/patches/@types+sparql-http-client+2.2.8.patch b/patches/@types+sparql-http-client+2.2.8.patch new file mode 100644 index 000000000..f9a65a82c --- /dev/null +++ b/patches/@types+sparql-http-client+2.2.8.patch @@ -0,0 +1,12 @@ +diff --git a/node_modules/@types/sparql-http-client/StreamClient.d.ts b/node_modules/@types/sparql-http-client/StreamClient.d.ts +index e4e1e60..9d9e621 100755 +--- a/node_modules/@types/sparql-http-client/StreamClient.d.ts ++++ b/node_modules/@types/sparql-http-client/StreamClient.d.ts +@@ -13,6 +13,7 @@ declare namespace StreamClient { + interface QueryOptions { + headers?: HeadersInit | undefined; + operation?: 'get' | 'postUrlencoded' | 'postDirect' | undefined; ++ defaultGraphUri?: Array | undefined; + } + + interface QueryInit { diff --git a/patches/sparql-http-client+2.4.0.patch b/patches/sparql-http-client+2.4.0.patch new file mode 100644 index 000000000..420ed82b3 --- /dev/null +++ b/patches/sparql-http-client+2.4.0.patch @@ -0,0 +1,188 @@ +diff --git a/node_modules/sparql-http-client/Endpoint.js b/node_modules/sparql-http-client/Endpoint.js +index 766b417..a9ee851 100644 +--- a/node_modules/sparql-http-client/Endpoint.js ++++ b/node_modules/sparql-http-client/Endpoint.js +@@ -36,7 +36,7 @@ class Endpoint { + * @param {boolean} [options.update=false] if true, performs a SPARQL Update + * @return {Promise} + */ +- async get (query, { headers, update = false } = {}) { ++ async get (query, { headers, update = false, defaultGraphUri = [], namedGraphUri = [] } = {}) { + let url = null + + if (!update) { +@@ -47,6 +47,13 @@ class Endpoint { + url.searchParams.append('update', query) + } + ++ for (const uri of defaultGraphUri) { ++ url.searchParams.append('default-graph-uri', uri) ++ } ++ for (const uri of namedGraphUri) { ++ url.searchParams.append('named-graph-uri', uri) ++ } ++ + return this.fetch(url.toString().replace(/\+/g, '%20'), { + method: 'GET', + headers: this.mergeHeaders(headers) +@@ -61,7 +68,7 @@ class Endpoint { + * @param {boolean} [options.update=false] if true, performs a SPARQL Update + * @return {Promise} + */ +- async postDirect (query, { headers, update = false } = {}) { ++ async postDirect (query, { headers, update = false, defaultGraphUri = [], namedGraphUri = [] } = {}) { + let url = null + + if (!update) { +@@ -76,6 +83,13 @@ class Endpoint { + headers.set('content-type', 'application/sparql-query; charset=utf-8') + } + ++ for (const uri of defaultGraphUri) { ++ url.searchParams.append('default-graph-uri', uri) ++ } ++ for (const uri of namedGraphUri) { ++ url.searchParams.append('named-graph-uri', uri) ++ } ++ + return this.fetch(url, { + method: 'POST', + headers, +@@ -91,7 +105,7 @@ class Endpoint { + * @param {boolean} [options.update=false] if true, performs a SPARQL Update + * @return {Promise} + */ +- async postUrlencoded (query, { headers, update = false } = {}) { ++ async postUrlencoded (query, { headers, update = false, defaultGraphUri = [], namedGraphUri = [] } = {}) { + let url = null + let body = null + +@@ -103,6 +117,13 @@ class Endpoint { + body = 'update=' + encodeURIComponent(query) + } + ++ for (const uri of defaultGraphUri) { ++ url.searchParams.append('default-graph-uri', uri) ++ } ++ for (const uri of namedGraphUri) { ++ url.searchParams.append('named-graph-uri', uri) ++ } ++ + headers = this.mergeHeaders(headers) + + if (!headers.has('content-type')) { +diff --git a/node_modules/sparql-http-client/RawQuery.js b/node_modules/sparql-http-client/RawQuery.js +index 5aa6bfc..1538933 100644 +--- a/node_modules/sparql-http-client/RawQuery.js ++++ b/node_modules/sparql-http-client/RawQuery.js +@@ -21,14 +21,14 @@ class RawQuery { + * @param {'get'|'postUrlencoded'|'postDirect'} [init.operation='get'] + * @return {Promise} + */ +- async ask (query, { headers, operation = 'get' } = {}) { ++ async ask (query, { headers, operation = 'get', ...options } = {}) { + headers = this.endpoint.mergeHeaders(headers) + + if (!headers.has('accept')) { + headers.set('accept', 'application/sparql-results+json') + } + +- return this.endpoint[operation](query, { headers }) ++ return this.endpoint[operation](query, { headers, ...options }) + } + + /** +@@ -41,14 +41,14 @@ class RawQuery { + * @param {'get'|'postUrlencoded'|'postDirect'} [init.operation='get'] + * @return {Promise} + */ +- async construct (query, { headers, operation = 'get' } = {}) { ++ async construct (query, { headers, operation = 'get', ...options } = {}) { + headers = new this.endpoint.fetch.Headers(headers) + + if (!headers.has('accept')) { + headers.set('accept', 'application/n-triples') + } + +- return this.endpoint[operation](query, { headers }) ++ return this.endpoint[operation](query, { headers, ...options }) + } + + /** +@@ -61,14 +61,14 @@ class RawQuery { + * @param {'get'|'postUrlencoded'|'postDirect'} [init.operation='get'] + * @return {Promise} + */ +- async select (query, { headers, operation = 'get' } = {}) { ++ async select (query, { headers, operation = 'get', ...options } = {}) { + headers = this.endpoint.mergeHeaders(headers) + + if (!headers.has('accept')) { + headers.set('accept', 'application/sparql-results+json') + } + +- return this.endpoint[operation](query, { headers }) ++ return this.endpoint[operation](query, { headers, ...options }) + } + + /** +@@ -81,14 +81,14 @@ class RawQuery { + * @param {'get'|'postUrlencoded'|'postDirect'} [init.operation='postUrlencoded'] + * @return {Promise} + */ +- async update (query, { headers, operation = 'postUrlencoded' } = {}) { ++ async update (query, { headers, operation = 'postUrlencoded', ...options } = {}) { + headers = new this.endpoint.fetch.Headers(headers) + + if (!headers.has('accept')) { + headers.set('accept', '*/*') + } + +- return this.endpoint[operation](query, { headers, update: true }) ++ return this.endpoint[operation](query, { headers, update: true, ...options }) + } + } + +diff --git a/node_modules/sparql-http-client/StreamQuery.js b/node_modules/sparql-http-client/StreamQuery.js +index 163b325..bbbe20d 100644 +--- a/node_modules/sparql-http-client/StreamQuery.js ++++ b/node_modules/sparql-http-client/StreamQuery.js +@@ -27,8 +27,8 @@ class StreamQuery extends RawQuery { + * @param {'get'|'postUrlencoded'|'postDirect'} [init.operation='get'] + * @return {Promise} + */ +- async ask (query, { headers, operation } = {}) { +- const res = await super.ask(query, { headers, operation }) ++ async ask (query, { headers, operation, ...options } = {}) { ++ const res = await super.ask(query, { headers, operation, ...options }) + + await checkResponse(res) + +@@ -44,14 +44,14 @@ class StreamQuery extends RawQuery { + * @param {'get'|'postUrlencoded'|'postDirect'} [init.operation='get'] + * @return {Promise} + */ +- async construct (query, { headers, operation } = {}) { ++ async construct (query, { headers, operation, ...options } = {}) { + headers = new this.endpoint.fetch.Headers(headers) + + if (!headers.has('accept')) { + headers.set('accept', 'application/n-triples, text/turtle') + } + +- const res = await super.construct(query, { headers, operation }) ++ const res = await super.construct(query, { headers, operation, ...options }) + + await checkResponse(res) + +@@ -67,8 +67,8 @@ class StreamQuery extends RawQuery { + * @param {'get'|'postUrlencoded'|'postDirect'} [init.operation='get'] + * @return {Promise} + */ +- async select (query, { headers, operation } = {}) { +- const res = await super.select(query, { headers, operation }) ++ async select (query, { headers, operation, ...options } = {}) { ++ const res = await super.select(query, { headers, operation, ...options }) + + await checkResponse(res) + diff --git a/ui/src/api/mixins/SharedDimension.ts b/ui/src/api/mixins/SharedDimension.ts index 7e443d0fd..d059988b5 100644 --- a/ui/src/api/mixins/SharedDimension.ts +++ b/ui/src/api/mixins/SharedDimension.ts @@ -22,10 +22,6 @@ export default function mixin (base: Base) { @property.literal({ path: schema.validThrough, type: Date }) validThrough?: Date - - get deprecated () { - return this.validThrough && this.validThrough <= new Date() - } } return Impl diff --git a/ui/src/forms/editors/index.ts b/ui/src/forms/editors/index.ts index 36840d0c6..593fda640 100644 --- a/ui/src/forms/editors/index.ts +++ b/ui/src/forms/editors/index.ts @@ -90,7 +90,7 @@ export const checkBox: Lazy = { await import('./CheckboxEditor.vue').then(createCustomElement('cc-checkbox')) return ({ value }, { update }) => { - const booleanValue = trueTerm.equals(value.object?.term) + const booleanValue = value.object?.value === 'true' return html`` } }, diff --git a/ui/src/store/modules/projects.ts b/ui/src/store/modules/projects.ts index 70bbd56c1..8e44d5979 100644 --- a/ui/src/store/modules/projects.ts +++ b/ui/src/store/modules/projects.ts @@ -1,9 +1,9 @@ import { ActionTree, MutationTree, GetterTree } from 'vuex' -import { api, rootURL } from '@/api' +import { api } from '@/api' import { RootState } from '../types' import * as ns from '@cube-creator/core/namespace' -import { Project, ProjectsCollection } from '@cube-creator/model' -import { serializeProjectDetails, serializeProjectsCollection } from '../serializers' +import { CubeProject, Project, ProjectsCollection } from '@cube-creator/model' +import { serializeProjectDetails, serializeCollection } from '../serializers' import { RdfResource } from 'alcaeus' export interface ProjectsState { @@ -53,7 +53,7 @@ const actions: ActionTree = { const mutations: MutationTree = { storeCollection (state, collection) { - state.collection = collection ? serializeProjectsCollection(collection) : null + state.collection = collection ? serializeCollection(collection, sortProject) : null }, storeProjectDetails (state, { project, details }) { @@ -64,6 +64,20 @@ const mutations: MutationTree = { }, } +function sortProject (a: CubeProject, b: CubeProject) { + const aPlannedUpdate = a.plannedNextUpdate?.toISOString() + if (!aPlannedUpdate) { + return 1 + } + + const bPlannedUpdate = b.plannedNextUpdate?.toISOString() + if (!bPlannedUpdate) { + return -1 + } + + return aPlannedUpdate.localeCompare(bPlannedUpdate) || a.label.localeCompare(b.label) +} + export default { namespaced: true, state: initialState, diff --git a/ui/src/store/modules/sharedDimensions.ts b/ui/src/store/modules/sharedDimensions.ts index 5025cb615..260f30a45 100644 --- a/ui/src/store/modules/sharedDimensions.ts +++ b/ui/src/store/modules/sharedDimensions.ts @@ -3,6 +3,8 @@ import { api } from '@/api' import { RootState } from '../types' import { cc, md } from '@cube-creator/core/namespace' import { Collection, RdfResource } from 'alcaeus' +import { serializeCollection } from '@/store/serializers' +import { schema } from '@tpluscode/rdf-ns-builders' export interface SharedDimensionsState { entrypoint: null | RdfResource @@ -33,13 +35,25 @@ const actions: ActionTree = { return entrypoint }, - async fetchCollection (context) { + async fetchCollection (context, query) { const entrypoint = context.state.entrypoint const collectionURI = entrypoint?.get(md.sharedDimensions)?.id + let params = new URLSearchParams() + if (query) { + params = new URLSearchParams(query) + } else if (location.search) { + params = new URLSearchParams(location.search) + } + if (!collectionURI) throw new Error('Missing shared dimensions collection in entrypoint') - const collection = await api.fetchResource(collectionURI.value) + const collectionUrl = new URL(collectionURI.value) + for (const [key, value] of params) { + collectionUrl.searchParams.set(key, value) + } + + const collection = await api.fetchResource(collectionUrl.toString()) context.commit('storeCollection', collection) }, } @@ -50,10 +64,14 @@ const mutations: MutationTree = { }, storeCollection (state, collection) { - state.collection = collection + state.collection = collection ? serializeCollection(collection, sortByName) : null }, } +function sortByName (l: RdfResource, r: RdfResource) { + return l.pointer.out(schema.name).value?.localeCompare(r.pointer.out(schema.name).value || '') || 0 +} + export default { namespaced: true, state: initialState, diff --git a/ui/src/store/serializers.ts b/ui/src/store/serializers.ts index 6765e9be2..fc534ed10 100644 --- a/ui/src/store/serializers.ts +++ b/ui/src/store/serializers.ts @@ -10,40 +10,38 @@ import { DimensionMetadata, DimensionMetadataCollection, JobCollection, - ProjectsCollection, SourcesCollection, Table, TableCollection, } from '@cube-creator/model' import { IdentifierMapping, LiteralColumnMapping, ReferenceColumnMapping } from '@cube-creator/model/ColumnMapping' import { Link } from '@cube-creator/model/lib/Link' -import { dcterms, oa, rdf, rdfs, schema } from '@tpluscode/rdf-ns-builders' +import { dcterms, hydra, oa, rdf, rdfs, schema } from '@tpluscode/rdf-ns-builders' import { RdfResource, ResourceIdentifier } from '@tpluscode/rdfine/RdfResource' import { ProjectDetails, SharedDimensionTerm } from './types' import { clone } from '@/store/searchParams' +import type { Collection } from '@rdfine/hydra' +import type { GraphPointer } from 'clownface' export const displayLanguage = ['en', 'de', 'fr', ''] -export function serializeProjectsCollection (collection: ProjectsCollection): ProjectsCollection { - return Object.freeze({ - ...serializeResource(collection), - searchParams: clone(collection.pointer), - member: collection.member.sort(sortProject), - }) as ProjectsCollection -} - -function sortProject (a: CubeProject, b: CubeProject) { - const aPlannedUpdate = a.plannedNextUpdate?.toISOString() - if (!aPlannedUpdate) { - return 1 +declare module '@rdfine/hydra' { + interface Collection { + searchParams: GraphPointer + pageSize: number + perPage: number } +} - const bPlannedUpdate = b.plannedNextUpdate?.toISOString() - if (!bPlannedUpdate) { - return -1 - } +export function serializeCollection (collection: Collection, sort: (l: T, r: T) => number): Collection { + const member = sort ? collection.member.sort(sort) : collection.member - return aPlannedUpdate.localeCompare(bPlannedUpdate) || a.label.localeCompare(b.label) + return Object.freeze({ + ...serializeResource(collection), + searchParams: clone(collection.pointer), + member, + totalItems: collection.totalItems, + }) as Collection } export function serializeProjectDetails (details: RdfResource): ProjectDetails { diff --git a/ui/src/views/SharedDimensions.vue b/ui/src/views/SharedDimensions.vue index 1aa894083..caffac839 100644 --- a/ui/src/views/SharedDimensions.vue +++ b/ui/src/views/SharedDimensions.vue @@ -22,13 +22,18 @@
- - show deprecated - +
+

No shared dimension yet @@ -59,7 +71,7 @@ @@ -125,4 +180,8 @@ export default defineComponent({ .abbreviations span { margin-right: 20px } + +cc-hydra-operation-form { + width: 100%; +} diff --git a/ui/tests/e2e/specs/shared-dimensions.spec.ts b/ui/tests/e2e/specs/shared-dimensions.spec.ts index dc3d49ed9..15df69cab 100644 --- a/ui/tests/e2e/specs/shared-dimensions.spec.ts +++ b/ui/tests/e2e/specs/shared-dimensions.spec.ts @@ -16,15 +16,15 @@ describe('Shared dimensions', () => { cy.contains('.button', 'Create shared dimension').click() - cy.contains('form-property', 'Identifier') + cy.get('.quickview').contains('form-property', 'Identifier') .find('input') .type(toIdentifier(dimensionName)) - cy.contains('form-property', 'Name') + cy.get('.quickview').contains('form-property', 'Name') .find('input') .type(dimensionName) - cy.contains('form-property', 'Name') + cy.get('.quickview').contains('form-property', 'Name') .find('select') .select('en') diff --git a/yarn.lock b/yarn.lock index acd89671f..3e758bd0d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1566,22 +1566,25 @@ rdf-loaders-registry "^0.2.0" sparql-http-client "^2.2.2" -"@hydrofoil/shape-to-query@^0.12": - version "0.12.0" - resolved "https://registry.yarnpkg.com/@hydrofoil/shape-to-query/-/shape-to-query-0.12.0.tgz#cc36a95781bc334b48c7836cf17631d9af2a100e" - integrity sha512-2ZljN+03GpRQAnMS7WOMADCkqJtTCEbOblHD0dL+6p968ZZC5eVzECmgpZnVli6/escZS/kWeWbH/7QZBiMglQ== +"@hydrofoil/shape-to-query@^0.13.5": + version "0.13.5" + resolved "https://registry.yarnpkg.com/@hydrofoil/shape-to-query/-/shape-to-query-0.13.5.tgz#246ffcaaf51d1ee3f1f62162310c23a2ad28d7d9" + integrity sha512-bwjUzeOLEV48tldt+0pGH8y21MOTO9cw6A78boM9i+bpKR6UJqAZ6Rv8RmeGSY5GTIILfuelqP/kgOgvM0C6Xw== dependencies: + "@hydrofoil/sparql-processor" "^0.1.3" "@tpluscode/rdf-ns-builders" ">=3.0.2" - "@tpluscode/rdf-string" "^1.3.1" - "@tpluscode/sparql-builder" "^2.0.3" + "@tpluscode/rdf-string" "^1.3.3" + "@types/sparqljs" "^3.1.11" "@vocabulary/dash" "^1.0.4" "@vocabulary/dash-sparql" "^1.0.4" "@vocabulary/sh" "^1.1.5" - "@zazuko/env" "^2.2.0" + "@zazuko/env" "^2.4.2" "@zazuko/prefixes" ">=2" - clownface-shacl-path "^2.1.1" + clownface-shacl-path "^2.2" is-graph-pointer "^2.0.0" rdf-literal "^1.3.2" + sparqljs "^3.6.1" + ts-pattern "^5.2.0" "@hydrofoil/shaperone-core@^0.11", "@hydrofoil/shaperone-core@^0.11.0": version "0.11.0" @@ -1664,6 +1667,15 @@ concat-merge "^1.0.3" lit "^2.0.0" +"@hydrofoil/sparql-processor@^0.1.3": + version "0.1.3" + resolved "https://registry.yarnpkg.com/@hydrofoil/sparql-processor/-/sparql-processor-0.1.3.tgz#076b1a5ef53fc9ee65143afbad390b7247531689" + integrity sha512-QVgIy+H3038t0r2EtPuknn1I9Cn2foVKk36uU3Wk+npceFu995kTkH1T77AN4KRxsSZBUyENN9XUxaCvUwiN+Q== + dependencies: + "@types/sparqljs" "^3.1.11" + "@zazuko/prefixes" "^2.1.0" + ts-pattern "^5.2.0" + "@isaacs/cliui@^8.0.2": version "8.0.2" resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" @@ -2911,7 +2923,7 @@ dependencies: "@rdfjs/to-ntriples" "^2.0.0" -"@rdfjs/term-set@^2", "@rdfjs/term-set@^2.0.1": +"@rdfjs/term-set@^2.0.1": version "2.0.1" resolved "https://registry.yarnpkg.com/@rdfjs/term-set/-/term-set-2.0.1.tgz#bbf406e0477c226e65753da2da0089cbbe847ab5" integrity sha512-ZD8IwSY7tPpevs2iaQEsesAu8c7TO4GKHQHObbehUE4odKa9BuhuimdNuYwBoyVprTtHARaW6VW+0Jsu7ehD+Q== @@ -3249,10 +3261,10 @@ "@tpluscode/rdf-ns-builders" "^2" "@zazuko/rdf-vocabularies" ">=2023.01.17" -"@tpluscode/rdf-string@^1.3.0", "@tpluscode/rdf-string@^1.3.1": - version "1.3.3" - resolved "https://registry.yarnpkg.com/@tpluscode/rdf-string/-/rdf-string-1.3.3.tgz#7bd989b4745240eb0fb8b140dceaf54715151b0d" - integrity sha512-BiBVkX3EVRApo6DNpXuq7Mh87tPX/3aD8m1tjxe4TT6piRrmwbIT/QRrlNdAjt5cAEBRwH8cp+GFTOb/X4Y8Ww== +"@tpluscode/rdf-string@^1.3.1", "@tpluscode/rdf-string@^1.3.3": + version "1.3.4" + resolved "https://registry.yarnpkg.com/@tpluscode/rdf-string/-/rdf-string-1.3.4.tgz#de007ed202c5455d7805dd188feab0b8cc4ead69" + integrity sha512-bWdUgsJC84CFYPQ0GCatA7rZqQme12ubogqm81gOd+m/5pK/Ua5yK8iYGLh0ArtPCmYqXm160UCOCLm17h1AUw== dependencies: "@rdfjs/data-model" "^2.0.0" "@rdfjs/environment" "^1.0.0" @@ -3288,18 +3300,6 @@ "@types/sparql-http-client" "^2" debug "^4.1.1" -"@tpluscode/sparql-builder@^2.0.3": - version "2.0.3" - resolved "https://registry.yarnpkg.com/@tpluscode/sparql-builder/-/sparql-builder-2.0.3.tgz#7a116347126551933b50dd169770d0b5659de8a5" - integrity sha512-VoV2Ifv8QSaj7uXagPkMzcUxEEhJbn3S+etz82O6fTUvrrn3ndwB/LgNfeSwc2NxPj3kBkyCb5EVeaD7YawOjw== - dependencies: - "@rdfjs/data-model" "^2" - "@rdfjs/term-set" "^2" - "@rdfjs/types" "*" - "@tpluscode/rdf-string" "^1.3.0" - "@types/sparql-http-client" "^3.0.0" - anylogger "^1.0.11" - "@transloadit/prettier-bytes@0.0.7": version "0.0.7" resolved "https://registry.yarnpkg.com/@transloadit/prettier-bytes/-/prettier-bytes-0.0.7.tgz#cdb5399f445fdd606ed833872fa0cabdbc51686b" @@ -4008,20 +4008,12 @@ dependencies: rdf-js "^4.0.2" -"@types/sparql-http-client@^3.0.0": - version "3.0.2" - resolved "https://registry.yarnpkg.com/@types/sparql-http-client/-/sparql-http-client-3.0.2.tgz#8f89fccdb6af40e72160ed865ef2d0913149024e" - integrity sha512-sGQ7y+W/fhSM78vBjCuwaZPiM/UAfl5ZRmc6NoxddkqBmb5JEfUmTqts7lESzuG9XxDmLfiIXsrZjeSyDlPMlg== +"@types/sparqljs@^3.1.11", "@types/sparqljs@^3.1.3": + version "3.1.12" + resolved "https://registry.yarnpkg.com/@types/sparqljs/-/sparqljs-3.1.12.tgz#29a030615a3aed6eb05fbd618ecf58ab345506f8" + integrity sha512-zg/sdKKtYI0845wKPSuSgunyU1o/+7tRzMw85lHsf4p/0UbA6+65MXAyEtv1nkaqSqrq/bXm7+bqXas+Xo5dpQ== dependencies: "@rdfjs/types" ">=1.0.0" - "@types/rdfjs__environment" "*" - -"@types/sparqljs@^3.1.3": - version "3.1.3" - resolved "https://registry.yarnpkg.com/@types/sparqljs/-/sparqljs-3.1.3.tgz#e4b9a2511bc2f14f564559ed6cf567835791a7e9" - integrity sha512-nmFgmR6ns4i8sg9fYu+293H+PMLKmDOZy34sgwgAeUEEiIqSs4guj5aCZRt3gq1g0yuKXkqrxLDq/684g7pGtQ== - dependencies: - rdf-js "^4.0.2" "@types/stack-utils@^2.0.0": version "2.0.3" @@ -4964,7 +4956,7 @@ "@zazuko/env" "^2.1.1" "@zazuko/rdf-utils-fs" "^3.3.0" -"@zazuko/env@^2.1.1", "@zazuko/env@^2.2.0": +"@zazuko/env@2.2.0", "@zazuko/env@^2.1.1", "@zazuko/env@^2.4.2": version "2.2.0" resolved "https://registry.yarnpkg.com/@zazuko/env/-/env-2.2.0.tgz#090ca10466113428483bfffeedf981b0b411d8e5" integrity sha512-73KwqrckawQTmoPAizlkHIRpsqFOaR31LA4XEZUnLM6CbGF/DhmbclfhMtg8GBAiZLIVdgXj6GZAW0lUmA4knQ== @@ -6397,11 +6389,12 @@ clownface-shacl-path@^1.0.1, clownface-shacl-path@^1.3.2: "@tpluscode/rdf-ns-builders" "^2.0.0" "@tpluscode/rdf-string" "^0.2.26" -clownface-shacl-path@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/clownface-shacl-path/-/clownface-shacl-path-2.1.1.tgz#ff655221c9e660726d82552ce26fddc03d6517f2" - integrity sha512-UqMETJtsjlHzp0aTBOja3I9NE6mR3E4s19FcTvYo2xJ80mxLNV+zJ5XhhdR1+lxttRfe2SNdPa9hUj1NsjOcOw== +clownface-shacl-path@^2.2: + version "2.4.0" + resolved "https://registry.yarnpkg.com/clownface-shacl-path/-/clownface-shacl-path-2.4.0.tgz#c2873c48f3f07de515d8659e06358f5707a1cfe9" + integrity sha512-0kVoROr51QAYbh3q/IHTXF3lwUgWF2xhZ66/l6Q244VghoZ5JTWON0/1Lp6Jjg2UXIe14MvAaUk2mW0IiwUtKQ== dependencies: + "@rdfjs/term-map" "^2.0.0" "@rdfjs/term-set" "^2.0.1" "@tpluscode/rdf-ns-builders" ">=3.0.2" "@tpluscode/rdf-string" "^1.3.1" @@ -12963,10 +12956,10 @@ rdf-cube-view-query@^1.8.2: rdf-sparql-builder "^0.1.8" sparql-http-client "^2.2.2" -rdf-data-factory@^1.0.4, rdf-data-factory@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/rdf-data-factory/-/rdf-data-factory-1.1.0.tgz#d0510b9f100dd79e94f29559a12d4a5a585054d6" - integrity sha512-g8feOVZ/KL1OK2Pco/jDBDFh4m29QDsOOD+rWloG9qFvIzRFchGy2CviLUX491E0ByewXxMpaq/A3zsWHQA16A== +rdf-data-factory@^1.1.0, rdf-data-factory@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/rdf-data-factory/-/rdf-data-factory-1.1.2.tgz#d47550d2649d0d64f8cae3fcc9efae7a8a895d9a" + integrity sha512-TfQD63Lokabd09ES1jAtKK8AA6rkr9rwyUBGo6olOt1CE0Um36CUQIqytyf0am2ouBPR0l7SaHxCiMcPGHkt1A== dependencies: "@rdfjs/types" "*" @@ -14076,12 +14069,12 @@ sparql-http-client@^3.0.0: readable-stream "^4.5.2" stream-chunks "^1.0.0" -sparqljs@^3.5.1: - version "3.5.1" - resolved "https://registry.yarnpkg.com/sparqljs/-/sparqljs-3.5.1.tgz#631cd4b5bcc3d9ce23c5313b3a158eccf858e28b" - integrity sha512-sHc6z7hNF3ACvXurKe8hT1sD52Fc0fN3uPLS6SQnXRV9CJl33GNAS4w5Dd3X3GgykUt9SlnjhI1QRKhLzun4qQ== +sparqljs@^3.5.1, sparqljs@^3.6.1: + version "3.7.3" + resolved "https://registry.yarnpkg.com/sparqljs/-/sparqljs-3.7.3.tgz#075821d51ef4954284e36569503fe5558cfb71b0" + integrity sha512-FQfHUhfwn5PD9WH6xPU7DhFfXMgqK/XoDrYDVxz/grhw66Il0OjRg3JBgwuEvwHnQt7oSTiKWEiCZCPNaUbqgg== dependencies: - rdf-data-factory "^1.0.4" + rdf-data-factory "^1.1.2" spawndamnit@^2.0.0: version "2.0.0" @@ -14294,7 +14287,7 @@ string-to-stream@^3.0.1: dependencies: readable-stream "^3.4.0" -"string-width-cjs@npm:string-width@^4.2.0": +"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -14311,15 +14304,6 @@ string-width@^2.1.1: is-fullwidth-code-point "^2.0.0" strip-ansi "^4.0.0" -string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - string-width@^5.0.0, string-width@^5.0.1, string-width@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" @@ -14389,7 +14373,7 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -14403,13 +14387,6 @@ strip-ansi@^4.0.0: dependencies: ansi-regex "^3.0.0" -strip-ansi@^6.0.0, strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - strip-ansi@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.0.1.tgz#61740a08ce36b61e50e65653f07060d000975fb2" @@ -14795,6 +14772,11 @@ ts-node@^9.0.0: source-map-support "^0.5.17" yn "3.1.1" +ts-pattern@^5.2.0: + version "5.6.0" + resolved "https://registry.yarnpkg.com/ts-pattern/-/ts-pattern-5.6.0.tgz#831516bbb9041499c5525e8976d8fc6b68ac8bb9" + integrity sha512-SL8u60X5+LoEy9tmQHWCdPc2hhb2pKI6I1tU5Jue3v8+iRqZdcT3mWPwKKJy1fMfky6uha82c8ByHAE8PMhKHw== + tsconfig-paths@^3.14.1: version "3.14.1" resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz#ba0734599e8ea36c862798e920bcf163277b137a" @@ -15579,7 +15561,7 @@ workerpool@6.2.1: resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.1.tgz#46fc150c17d826b86a008e5a4508656777e9c343" integrity sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -15605,15 +15587,6 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"