From 08ecd61d2ad8252ccb0ea8fbee089f8b6c49b137 Mon Sep 17 00:00:00 2001 From: Harlan Wilton Date: Mon, 15 Aug 2022 10:22:39 +1000 Subject: [PATCH] fix: simplify id resolving and aggressive const ids --- src/core/build.ts | 4 +- src/core/resolve.ts | 38 ++++++++++++---- src/nodes/AggregateOffer/index.ts | 4 +- src/nodes/Article/index.test.ts | 16 +++---- src/nodes/Article/index.ts | 60 +++++++++++-------------- src/nodes/Breadcrumb/index.test.ts | 23 ++++++++++ src/nodes/Breadcrumb/index.ts | 7 ++- src/nodes/Comment/index.test.ts | 7 ++- src/nodes/Comment/index.ts | 10 ++--- src/nodes/Event/Place/index.ts | 4 +- src/nodes/Event/index.test.ts | 44 ++++++++++++++++-- src/nodes/Event/index.ts | 27 ++++------- src/nodes/HowTo/HowToStep/index.ts | 2 +- src/nodes/HowTo/index.ts | 13 ++---- src/nodes/Image/index.ts | 18 +++----- src/nodes/ListItem/index.ts | 8 ++-- src/nodes/LocalBusiness/index.test.ts | 2 +- src/nodes/LocalBusiness/index.ts | 32 +++++-------- src/nodes/Organization/index.ts | 33 ++++++-------- src/nodes/Person/index.ts | 16 +------ src/nodes/Product/index.test.ts | 1 - src/nodes/Product/index.ts | 15 +++---- src/nodes/Question/index.ts | 3 +- src/nodes/Recipe/index.ts | 13 +----- src/nodes/Review/index.ts | 6 +-- src/nodes/Video/index.ts | 9 ++-- src/nodes/WebPage/index.test.ts | 24 +++++----- src/nodes/WebPage/index.ts | 18 +++----- src/nodes/WebSite/SearchAction/index.ts | 6 +-- src/nodes/WebSite/index.ts | 21 +++------ src/types.ts | 2 +- src/utils/index.ts | 43 +++++++++--------- 32 files changed, 256 insertions(+), 273 deletions(-) diff --git a/src/core/build.ts b/src/core/build.ts index bea7cf6..c5f02cd 100644 --- a/src/core/build.ts +++ b/src/core/build.ts @@ -27,7 +27,7 @@ export const dedupeAndFlattenNodes = (nodes: RegisteredThing[]) => { const dedupedNodes: Record = {} for (const key of keys) { const n = nodes[key] - const nodeKey = resolveAsGraphKey(n['@id'] || hash(n)) + const nodeKey = resolveAsGraphKey(n['@id'] || hash(n)) as Id dedupedNodes[nodeKey] = Object.keys(n) .sort() .reduce( @@ -85,7 +85,7 @@ export const buildResolvedGraphCtx = (nodes: Thing[], meta: MetaInput) => { const resolver = node._resolver if (resolver) { node = executeResolverOnNode(node, ctx, resolver) - node = resolveNodeId(node, ctx, resolver) + node = resolveNodeId(node, ctx, resolver, true) } ctx.nodes[key] = node }) diff --git a/src/core/resolve.ts b/src/core/resolve.ts index 2e91dfe..16500c4 100644 --- a/src/core/resolve.ts +++ b/src/core/resolve.ts @@ -6,7 +6,7 @@ import type { Thing, } from '../types' import type { ResolverOptions } from '../utils' -import { asArray, idReference, prefixId, setIfEmpty } from '../utils' +import { asArray, idReference, prefixId, setIfEmpty, stripEmptyProperties } from '../utils' import type { SchemaOrgContext } from './graph' export const executeResolverOnNode = (node: T, ctx: SchemaOrgContext, resolver: SchemaOrgNodeDefinition) => { @@ -26,19 +26,41 @@ export const executeResolverOnNode = (node: T, ctx: SchemaOrgCo // handle meta inherits resolver.inheritMeta?.forEach((entry) => { if (typeof entry === 'string') - setIfEmpty(node, entry, ctx.meta?.[entry]) + setIfEmpty(node, entry, ctx.meta[entry]) else - setIfEmpty(node, entry.key, ctx.meta?.[entry.meta]) + setIfEmpty(node, entry.key, ctx.meta[entry.meta]) }) // handle resolve if (resolver?.resolve) node = resolver.resolve(node, ctx) + stripEmptyProperties(node) return node } -export const resolveNodeId = (node: T, ctx: SchemaOrgContext, resolver: SchemaOrgNodeDefinition) => { +export const resolveNodeId = (node: T, ctx: SchemaOrgContext, resolver: SchemaOrgNodeDefinition, resolveAsRoot = false) => { + const prefix = Array.isArray(resolver.idPrefix) ? resolver.idPrefix[0] : resolver.idPrefix + + // may not need an @id + if (!prefix) + return node + + // transform #my-id into https://host.com/#my-id + if (node['@id'] && !(node['@id'] as string).startsWith(ctx.meta.host)) { + node['@id'] = prefixId(ctx.meta[prefix], node['@id']) + return node + } + + const rootId = Array.isArray(resolver.idPrefix) ? resolver.idPrefix?.[1] : undefined + // transform ['host', PrimaryWebPageId] to https://host.com/#webpage + if (resolveAsRoot && rootId) { + // make sure it doesn't exist + const existingNode = ctx.findNode(rootId) + if (!existingNode) + node['@id'] = prefixId(ctx.meta[prefix], rootId) + } + // transform 'host' to https://host.com/#schema/webpage/gj5g59gg if (!node['@id']) { let alias = resolver?.alias if (!alias) { @@ -51,12 +73,12 @@ export const resolveNodeId = (node: T, ctx: SchemaOrgContext, r if (!key.startsWith('_')) hashNodeData[key] = val }) - node['@id'] = prefixId(resolver.root ? ctx.meta.host : ctx.meta.url, `#/schema/${alias}/${hash(hashNodeData)}`) + node['@id'] = prefixId(ctx.meta[prefix], `#/schema/${alias}/${hash(hashNodeData)}`) } return node } -export function resolveRelation(input: Arrayable, ctx: any, +export function resolveRelation(input: Arrayable, ctx: SchemaOrgContext, resolver: SchemaOrgNodeDefinition, options: ResolverOptions = {}, ) { @@ -78,12 +100,12 @@ export function resolveRelation(input: Arrayable, ctx: any, // root nodes need ids if (options.generateId || options.root) - node = resolveNodeId(node, ctx, resolver) + node = resolveNodeId(node, ctx, resolver, false) if (options.root) { if (resolver.rootNodeResolve) resolver.rootNodeResolve(node, ctx) - ctx.addNode(node, ctx) + ctx.addNode(node) return idReference(node['@id']) } diff --git a/src/nodes/AggregateOffer/index.ts b/src/nodes/AggregateOffer/index.ts index a3fcb8f..df3d962 100644 --- a/src/nodes/AggregateOffer/index.ts +++ b/src/nodes/AggregateOffer/index.ts @@ -38,9 +38,7 @@ export const aggregateOfferResolver = defineSchemaOrgResolver({ '@type': 'AggregateOffer', }, resolve(node, ctx) { - if (node.offers) - node.offers = resolveRelation(node.offers, ctx, offerResolver) - + node.offers = resolveRelation(node.offers, ctx, offerResolver) setIfEmpty(node, 'offerCount', asArray(node.offers).length) return node }, diff --git a/src/nodes/Article/index.test.ts b/src/nodes/Article/index.test.ts index 386f748..2339637 100644 --- a/src/nodes/Article/index.test.ts +++ b/src/nodes/Article/index.test.ts @@ -362,14 +362,6 @@ describe('defineArticle', () => { }, "thumbnailUrl": "https://example.com/my-image.png", }, - { - "@id": "https://example.com/#logo", - "@type": "ImageObject", - "caption": "Identity", - "contentUrl": "https://example.com/test.png", - "inLanguage": "en-AU", - "url": "https://example.com/test.png", - }, { "@id": "https://example.com/#/schema/person/xRdko3dItW", "@type": "Person", @@ -382,6 +374,14 @@ describe('defineArticle', () => { "name": "Jane doe", "url": "https://harlanzw.com", }, + { + "@id": "https://example.com/#logo", + "@type": "ImageObject", + "caption": "Identity", + "contentUrl": "https://example.com/test.png", + "inLanguage": "en-AU", + "url": "https://example.com/test.png", + }, { "@id": "https://example.com/#/schema/image/riaRi7jPJC", "@type": "ImageObject", diff --git a/src/nodes/Article/index.ts b/src/nodes/Article/index.ts index 9505995..44e6dc9 100644 --- a/src/nodes/Article/index.ts +++ b/src/nodes/Article/index.ts @@ -11,15 +11,12 @@ import { asArray, idReference, resolvableDateToIso, - resolveId, - resolveType, - resolveWithBaseUrl, - setIfEmpty, - trimLength, + resolveDefaultType, + resolveWithBase, + setIfEmpty, trimLength, } from '../../utils' import type { WebPage } from '../WebPage' import { PrimaryWebPageId } from '../WebPage' -import type { Organization } from '../Organization' import type { Person } from '../Person' import type { Image } from '../Image' import type { Video } from '../Video' @@ -132,46 +129,39 @@ export const articleResolver = defineSchemaOrgResolver
({ 'datePublished', { meta: 'title', key: 'headline' }, ], + idPrefix: ['url', PrimaryArticleId], resolve(node, ctx) { - // @todo check it doesn't exist - setIfEmpty(node, '@id', prefixId(ctx.meta.url, PrimaryArticleId)) - resolveId(node, ctx.meta.url) - if (node.author) { - node.author = resolveRelation(node.author, ctx, personResolver, { - root: true, - }) - } - if (node.dateModified) - node.dateModified = resolvableDateToIso(node.dateModified) - if (node.datePublished) - node.datePublished = resolvableDateToIso(node.datePublished) - if (node['@type']) - node['@type'] = resolveType(node['@type'], 'Article') as Arrayable + node.author = resolveRelation(node.author, ctx, personResolver, { + root: true, + }) + node.dateModified = resolvableDateToIso(node.dateModified) + node.datePublished = resolvableDateToIso(node.datePublished) + resolveDefaultType(node, 'Article') + // Headlines should not exceed 110 characters. - if (node.headline) - node.headline = trimLength(node.headline, 110) + node.headline = trimLength(node.headline, 110) return node }, - rootNodeResolve(article, { findNode, meta }) { + rootNodeResolve(node, { findNode, meta }) { const webPage = findNode(PrimaryWebPageId) - const identity = findNode(IdentityId) + const identity = findNode(IdentityId) - if (article.image && !article.thumbnailUrl) { - const firstImage = asArray(article.image)[0] as Image + if (node.image && !node.thumbnailUrl) { + const firstImage = asArray(node.image)[0] as Image if (typeof firstImage === 'string') - setIfEmpty(article, 'thumbnailUrl', resolveWithBaseUrl(meta.host, firstImage)) + setIfEmpty(node, 'thumbnailUrl', resolveWithBase(meta.host, firstImage)) else if (firstImage?.['@id']) - setIfEmpty(article, 'thumbnailUrl', findNode(firstImage['@id'])?.url) + setIfEmpty(node, 'thumbnailUrl', findNode(firstImage['@id'])?.url) } if (identity) { - setIfEmpty(article, 'publisher', idReference(identity)) - setIfEmpty(article, 'author', idReference(identity)) + setIfEmpty(node, 'publisher', idReference(identity)) + setIfEmpty(node, 'author', idReference(identity)) } if (webPage) { - setIfEmpty(article, 'isPartOf', idReference(webPage)) - setIfEmpty(article, 'mainEntityOfPage', idReference(webPage)) + setIfEmpty(node, 'isPartOf', idReference(webPage)) + setIfEmpty(node, 'mainEntityOfPage', idReference(webPage)) setIfEmpty(webPage, 'potentialAction', [ { '@type': 'ReadAction', @@ -179,10 +169,10 @@ export const articleResolver = defineSchemaOrgResolver
({ }, ]) // clone the dates to the webpage - setIfEmpty(webPage, 'dateModified', article.dateModified) - setIfEmpty(webPage, 'datePublished', article.datePublished) + setIfEmpty(webPage, 'dateModified', node.dateModified) + setIfEmpty(webPage, 'datePublished', node.datePublished) // setIfEmpty(webPage, 'author', article.author) } - return article + return node }, }) diff --git a/src/nodes/Breadcrumb/index.test.ts b/src/nodes/Breadcrumb/index.test.ts index 6386e50..2633935 100644 --- a/src/nodes/Breadcrumb/index.test.ts +++ b/src/nodes/Breadcrumb/index.test.ts @@ -89,6 +89,29 @@ describe('defineBreadcrumb', () => { { "@id": "https://example.com/#breadcrumb", "@type": "BreadcrumbList", + "itemListElement": [ + { + "@type": "ListItem", + "item": "https://example.com", + "name": "Home", + "position": 1, + }, + { + "@type": "ListItem", + "item": "https://example.com/blog", + "name": "Blog", + "position": 2, + }, + { + "@type": "ListItem", + "name": "My Article", + "position": 4, + }, + ], + }, + { + "@id": "https://example.com/#/schema/breadcrumblist/XYJRR9nagB", + "@type": "BreadcrumbList", "itemListElement": [ { "@type": "ListItem", diff --git a/src/nodes/Breadcrumb/index.ts b/src/nodes/Breadcrumb/index.ts index 6dae11a..73d4190 100644 --- a/src/nodes/Breadcrumb/index.ts +++ b/src/nodes/Breadcrumb/index.ts @@ -46,9 +46,8 @@ export const breadcrumbResolver = defineSchemaOrgResolver({ defaults: { '@type': 'BreadcrumbList', }, + idPrefix: ['url', PrimaryBreadcrumbId], resolve(breadcrumb, ctx) { - setIfEmpty(breadcrumb, '@id', prefixId(ctx.meta.url, PrimaryBreadcrumbId)) - resolveId(breadcrumb, ctx.meta.url) if (breadcrumb.itemListElement) { let index = 1 @@ -61,10 +60,10 @@ export const breadcrumbResolver = defineSchemaOrgResolver({ } return breadcrumb }, - rootNodeResolve(breadcrumb, { findNode }) { + rootNodeResolve(node, { findNode }) { // merge breadcrumbs reference into the webpage const webPage = findNode(PrimaryWebPageId) if (webPage) - setIfEmpty(webPage, 'breadcrumb', idReference(breadcrumb)) + setIfEmpty(webPage, 'breadcrumb', idReference(node)) }, }) diff --git a/src/nodes/Comment/index.test.ts b/src/nodes/Comment/index.test.ts index 3c98ac1..e51bfad 100644 --- a/src/nodes/Comment/index.test.ts +++ b/src/nodes/Comment/index.test.ts @@ -18,18 +18,17 @@ describe('defineComment', () => { expect(graphNodes).toMatchInlineSnapshot(` [ { - "@id": "https://example.com/#/schema/comment/f0AuDJRYrl", + "@id": "https://example.com/#/schema/comment/V3foSKHFC7", "@type": "Comment", "author": { - "@id": "https://example.com/#identity", + "@id": "https://example.com/#/schema/person/W64wIB7mRH", }, "text": "This is a comment", }, { - "@id": "https://example.com/#identity", + "@id": "https://example.com/#/schema/person/W64wIB7mRH", "@type": "Person", "name": "Harlan Wilton", - "url": "https://example.com/", }, ] `) diff --git a/src/nodes/Comment/index.ts b/src/nodes/Comment/index.ts index 356fb17..fe7e500 100644 --- a/src/nodes/Comment/index.ts +++ b/src/nodes/Comment/index.ts @@ -33,13 +33,11 @@ export const commentResolver = defineSchemaOrgResolver({ defaults: { '@type': 'Comment', }, + idPrefix: 'url', resolve(node, ctx) { - resolveId(node, ctx.meta.url) - if (node.author) { - node.author = resolveRelation(node.author, ctx, personResolver, { - root: true, - }) - } + node.author = resolveRelation(node.author, ctx, personResolver, { + root: true, + }) return node }, rootNodeResolve(node, { findNode }) { diff --git a/src/nodes/Event/Place/index.ts b/src/nodes/Event/Place/index.ts index 7e1b7e2..027fe68 100644 --- a/src/nodes/Event/Place/index.ts +++ b/src/nodes/Event/Place/index.ts @@ -19,9 +19,7 @@ export const placeResolver = defineSchemaOrgResolver({ '@type': 'Place', }, resolve(node, ctx) { - if (node.address) - node.address = resolveRelation(node.address, ctx, addressResolver) - + node.address = resolveRelation(node.address, ctx, addressResolver) return node }, }) diff --git a/src/nodes/Event/index.test.ts b/src/nodes/Event/index.test.ts index 1e4d3dc..954a7ad 100644 --- a/src/nodes/Event/index.test.ts +++ b/src/nodes/Event/index.test.ts @@ -26,6 +26,45 @@ describe('defineEvent', () => { }) }) + it('handles performing group', () => { + useSetup(() => { + useSchemaOrg([ + defineEvent({ + name: 'test', + organizer: defineOrganization({ + '@type': 'PerformingGroup', + 'name': 'My Organisation', + }), + }), + ]) + + const { graphNodes } = injectSchemaOrg() + + expect(graphNodes).toMatchInlineSnapshot(` + [ + { + "@id": "https://example.com/#event", + "@type": "Event", + "inLanguage": "en-AU", + "name": "test", + "organizer": { + "@id": "https://example.com/#/schema/organization/PqBFgu8CTD", + }, + }, + { + "@id": "https://example.com/#/schema/organization/PqBFgu8CTD", + "@type": [ + "Organization", + "PerformingGroup", + ], + "name": "My Organisation", + "url": "https://example.com/", + }, + ] + `) + }) + }) + it('handles startDate with time', () => { useSetup(() => { useSchemaOrg([ @@ -204,15 +243,14 @@ describe('defineEvent', () => { "@id": "https://example.com/#/schema/organization/klOKg4ARc8", }, "performer": { - "@id": "https://example.com/#identity", + "@id": "https://example.com/#/schema/performinggroup/tkm1kzXzg3", }, "startDate": "2025-07-21T19:00-05:00", }, { - "@id": "https://example.com/#identity", + "@id": "https://example.com/#/schema/performinggroup/tkm1kzXzg3", "@type": "PerformingGroup", "name": "Kira and Morrison", - "url": "https://example.com/", }, { "@id": "https://example.com/#/schema/organization/klOKg4ARc8", diff --git a/src/nodes/Event/index.ts b/src/nodes/Event/index.ts index e556263..716ad34 100644 --- a/src/nodes/Event/index.ts +++ b/src/nodes/Event/index.ts @@ -11,7 +11,6 @@ import { idReference, resolvableDateToDate, resolvableDateToIso, - resolveId, setIfEmpty, } from '../../utils' import type { Organization } from '../Organization' @@ -109,29 +108,21 @@ export const eventResolver = defineSchemaOrgResolver({ 'image', { meta: 'title', key: 'name' }, ], + idPrefix: ['url', PrimaryEventId], resolve(node, ctx) { - // @todo check it doesn't exist - setIfEmpty(node, '@id', prefixId(ctx.meta.url, PrimaryEventId)) - resolveId(node, ctx.meta.url) - if (node.location) { // @ts-expect-error untyped const isVirtual = node.location === 'string' || node.location?.url !== 'undefined' node.location = resolveRelation(node.location, ctx, isVirtual ? virtualLocationResolver : placeResolver) } - if (node.performer) { - node.performer = resolveRelation(node.performer, ctx, personResolver, { - root: true, - }) - } - if (node.organizer) { - node.organizer = resolveRelation(node.organizer, ctx, organizationResolver, { - root: true, - }) - } - if (node.offers) - node.offers = resolveRelation(node.offers, ctx, offerResolver) + node.performer = resolveRelation(node.performer, ctx, personResolver, { + root: true, + }) + node.organizer = resolveRelation(node.organizer, ctx, organizationResolver, { + root: true, + }) + node.offers = resolveRelation(node.offers, ctx, offerResolver) if (node.eventAttendanceMode) node.eventAttendanceMode = withBase(node.eventAttendanceMode, 'https://schema.org/') as EventAttendanceModeTypes @@ -153,13 +144,11 @@ export const eventResolver = defineSchemaOrgResolver({ node[date] = resolvableDateToIso(node[date]) } }) - setIfEmpty(node, 'endDate', node.startDate) return node }, rootNodeResolve(node, { findNode }) { const identity = findNode(IdentityId) - if (identity) setIfEmpty(node, 'organizer', idReference(identity)) }, diff --git a/src/nodes/HowTo/HowToStep/index.ts b/src/nodes/HowTo/HowToStep/index.ts index b2f8cfd..dd117bc 100644 --- a/src/nodes/HowTo/HowToStep/index.ts +++ b/src/nodes/HowTo/HowToStep/index.ts @@ -58,7 +58,7 @@ export const howToStepResolver = defineSchemaOrgResolver({ }, resolve(step, ctx) { if (step.url) - step.url = resolveWithBaseUrl(ctx.meta.url, step.url) + step.url = resolveWithBase(ctx.meta.url, step.url) if (step.image) { step.image = resolveRelation(step.image, ctx, imageResolver, { root: true, diff --git a/src/nodes/HowTo/index.ts b/src/nodes/HowTo/index.ts index 5847b45..5eeb8b8 100644 --- a/src/nodes/HowTo/index.ts +++ b/src/nodes/HowTo/index.ts @@ -1,4 +1,4 @@ -import type { IdReference, NodeRelations, Thing } from '../../types' +import type { NodeRelations, Thing } from '../../types' import { idReference, setIfEmpty, } from '../../utils' @@ -21,10 +21,6 @@ export interface HowToLite extends Thing { * An array of howToStep objects */ step: NodeRelations[] - /** - * Referencing the WebPage by ID. - */ - mainEntityOfPage?: IdReference /** * The total time required to perform all instructions or directions (including time to prepare the supplies), * in ISO 8601 duration format. @@ -78,12 +74,9 @@ export const howToResolver = defineSchemaOrgResolver({ 'inLanguage', { meta: 'title', key: 'name' }, ], + idPrefix: ['url', HowToId], resolve(node, ctx) { - setIfEmpty(node, '@id', prefixId(ctx.meta.url, HowToId)) - resolveId(node, ctx.meta.url) - if (node.step) - node.step = resolveRelation(node.step, ctx, howToStepResolver) - + node.step = resolveRelation(node.step, ctx, howToStepResolver) return node }, rootNodeResolve(node, { findNode }) { diff --git a/src/nodes/Image/index.ts b/src/nodes/Image/index.ts index f8216e5..974b1fa 100644 --- a/src/nodes/Image/index.ts +++ b/src/nodes/Image/index.ts @@ -1,13 +1,7 @@ import type { Thing } from '../../types' import { - idReference, - prefixId, - provideResolver, - resolveId, - resolveWithBaseUrl, setIfEmpty, + resolveWithBase, setIfEmpty, } from '../../utils' -import type { WebPage } from '../WebPage' -import { PrimaryWebPageId } from '../WebPage' import { defineSchemaOrgResolver } from '../../core' export interface ImageLite extends Thing { @@ -49,7 +43,6 @@ export interface Image extends ImageLite {} * Describes an individual image (usually in the context of an embedded media object). */ export const imageResolver = defineSchemaOrgResolver({ - root: true, alias: 'image', cast(input) { if (typeof input === 'string') { @@ -66,9 +59,9 @@ export const imageResolver = defineSchemaOrgResolver({ // @todo possibly only do if there's a caption 'inLanguage', ], + idPrefix: 'host', resolve(image, { meta }) { - image.url = resolveWithBaseUrl(meta.host, image.url) - resolveId(image, meta.host) + image.url = resolveWithBase(meta.host, image.url) setIfEmpty(image, 'contentUrl', image.url) // image height and width are required to render if (image.height && !image.width) @@ -77,7 +70,8 @@ export const imageResolver = defineSchemaOrgResolver({ delete image.width return image }, - rootNodeResolve(image, { findNode, meta }) { + // @todo re-implement #primaryimage + /* rootNodeResolve(image, { findNode, meta }) { const hasPrimaryImage = !!findNode('#primaryimage') if (image['@id']?.endsWith('#logo') && !hasPrimaryImage) { const webPage = findNode(PrimaryWebPageId) @@ -86,5 +80,5 @@ export const imageResolver = defineSchemaOrgResolver({ setIfEmpty(webPage, 'primaryImageOfPage', idReference(image)) } } - }, + }, */ }) diff --git a/src/nodes/ListItem/index.ts b/src/nodes/ListItem/index.ts index 0083a5e..55287c2 100644 --- a/src/nodes/ListItem/index.ts +++ b/src/nodes/ListItem/index.ts @@ -1,8 +1,6 @@ import type { Thing } from '../../types' -import { - resolveUrl, -} from '../../utils' import { defineSchemaOrgResolver } from '../../core' +import { resolveWithBase } from '../../utils' /** * A list item, e.g. a step in a checklist or how-to description. @@ -40,7 +38,9 @@ export const resolveListItem = defineSchemaOrgResolver({ '@type': 'ListItem', }, resolve(node, ctx) { - resolveUrl(node, 'item', ctx.meta.host) + if (typeof node.item === 'string') + node.item = resolveWithBase(ctx.meta.host, node.item as string) + return node }, }) diff --git a/src/nodes/LocalBusiness/index.test.ts b/src/nodes/LocalBusiness/index.test.ts index e264b59..7c6a9ab 100644 --- a/src/nodes/LocalBusiness/index.test.ts +++ b/src/nodes/LocalBusiness/index.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' import { injectSchemaOrg, useSchemaOrg, useSetup } from '../../../.test' -import { defineLocalBusiness } from './index' +import { defineLocalBusiness } from '#provider' describe('defineLocalBusiness', () => { it('can be registered', () => { diff --git a/src/nodes/LocalBusiness/index.ts b/src/nodes/LocalBusiness/index.ts index fbfc2f6..f0fe693 100644 --- a/src/nodes/LocalBusiness/index.ts +++ b/src/nodes/LocalBusiness/index.ts @@ -100,29 +100,21 @@ export const localBusinessResolver = defineSchemaOrgResolver({ { key: 'url', meta: 'host' }, { key: 'currenciesAccepted', meta: 'currency' }, ], + idPrefix: ['host', IdentityId], resolve(node, ctx) { - setIfEmpty(node, '@id', prefixId(ctx.meta.host, IdentityId)) + resolveDefaultType(node, ['Organization', 'LocalBusiness']) - if (node['@type']) - node['@type'] = resolveType(node['@type'], ['Organization', 'LocalBusiness']) as ['Organization', 'LocalBusiness', ValidLocalBusinessSubTypes] + node.address = resolveRelation(node.address, ctx, addressResolver) + node.openingHoursSpecification = resolveRelation(node.openingHoursSpecification, ctx, resolveOpeningHours) + node.logo = resolveRelation(node.logo, ctx, imageResolver, { + afterResolve(logo) { + const hasLogo = !!ctx.findNode('#logo') + if (!hasLogo) + logo['@id'] = prefixId(ctx.meta.host, '#logo') - if (node.address) - node.address = resolveRelation(node.address, ctx, addressResolver) - if (node.openingHoursSpecification) - node.openingHoursSpecification = resolveRelation(node.openingHoursSpecification, ctx, resolveOpeningHours) - - if (node.logo) { - node.logo = resolveRelation(node.logo, ctx, imageResolver, { - afterResolve(logo) { - const hasLogo = !!ctx.findNode('#logo') - if (!hasLogo) - logo['@id'] = prefixId(ctx.meta.host, '#logo') - - setIfEmpty(logo, 'caption', node.name) - }, - }) - } - resolveId(node, ctx.meta.host) + setIfEmpty(logo, 'caption', node.name) + }, + }) return node }, }) diff --git a/src/nodes/Organization/index.ts b/src/nodes/Organization/index.ts index fe2d5fe..03fc8ca 100644 --- a/src/nodes/Organization/index.ts +++ b/src/nodes/Organization/index.ts @@ -1,4 +1,3 @@ -import { hash } from 'ohash' import type { NodeRelation, NodeRelations, Thing } from '../../types' import { IdentityId, @@ -27,7 +26,7 @@ export interface OrganizationLite extends Thing { * (for example, if the logo is mostly white or gray, * it may not look how you want it to look when displayed on a white background). */ - logo: NodeRelation + logo?: NodeRelation /** * The site's home URL. */ @@ -65,25 +64,19 @@ export const organizationResolver defaults: { '@type': 'Organization', }, + idPrefix: ['host', IdentityId], + inheritMeta: [ + { meta: 'host', key: 'url' }, + ], resolve(node, ctx) { - setIfEmpty(node, 'url', ctx.meta.host) - resolveId(node, ctx.meta.host) - // create id if not set - if (!node['@id']) { - // may be re-registering the primary website - const identity = ctx.findNode(IdentityId) - if (!identity || hash(identity?.name) === hash(node.name)) - node['@id'] = prefixId(ctx.meta.host, IdentityId) - } - - if (node['@type']) - node['@type'] = resolveType(node['@type'], 'Organization') - if (node.address) - node.address = resolveRelation(node.address, ctx, addressResolver) - - const isIdentity = resolveAsGraphKey(node['@id'] || '') === IdentityId + resolveDefaultType(node, 'Organization') + node.address = resolveRelation(node.address, ctx, addressResolver) + return node + }, + rootNodeResolve(node, ctx) { + const isIdentity = resolveAsGraphKey(node['@id']) === IdentityId + // logo const webPage = ctx.findNode(PrimaryWebPageId) - if (node.logo) { node.logo = resolveRelation(node.logo, ctx, imageResolver, { root: true, @@ -97,12 +90,12 @@ export const organizationResolver if (webPage) setIfEmpty(webPage, 'primaryImageOfPage', idReference(node.logo as Image)) } + if (isIdentity && webPage) setIfEmpty(webPage, 'about', idReference(node as Organization)) const webSite = ctx.findNode(PrimaryWebSiteId) if (webSite) setIfEmpty(webSite, 'publisher', idReference(node as Organization)) - return node }, }) diff --git a/src/nodes/Person/index.ts b/src/nodes/Person/index.ts index 0b5adcb..0a342e5 100644 --- a/src/nodes/Person/index.ts +++ b/src/nodes/Person/index.ts @@ -10,7 +10,6 @@ import type { WebSite } from '../WebSite' import { PrimaryWebSiteId } from '../WebSite' import type { Article } from '../Article' import { PrimaryArticleId } from '../Article' -import type { Organization } from '../Organization' import { defineSchemaOrgResolver } from '../../core' import type { Image } from '../Image' @@ -48,7 +47,6 @@ export interface Person extends PersonLite {} * Describes an individual person. Most commonly used to identify the author of a piece of content (such as an Article or Comment). */ export const personResolver = defineSchemaOrgResolver({ - root: true, cast(node) { if (typeof node === 'string') { return { @@ -60,20 +58,10 @@ export const personResolver = defineSchemaOrgResolver({ defaults: { '@type': 'Person', }, - resolve(node, { meta, findNode }) { - resolveId(node, meta.host) - // create id if not set - if (!node['@id']) { - // may be re-registering the primary person - const identity = findNode(IdentityId) - if (!identity) - node['@id'] = prefixId(meta.host, IdentityId) - } - return node as Person - }, + idPrefix: ['host', IdentityId], rootNodeResolve(node, { findNode, meta }) { // if this person is the identity - if (resolveAsGraphKey(node['@id'] || '') === IdentityId) { + if (resolveAsGraphKey(node['@id']) === IdentityId) { setIfEmpty(node, 'url', meta.host) const webPage = findNode(PrimaryWebPageId) diff --git a/src/nodes/Product/index.test.ts b/src/nodes/Product/index.test.ts index cb6f1f6..3982a34 100644 --- a/src/nodes/Product/index.test.ts +++ b/src/nodes/Product/index.test.ts @@ -61,7 +61,6 @@ describe('defineProduct', () => { "review": { "@type": "Review", "author": { - "@id": "https://example.com/#identity", "@type": "Person", "name": "Harlan Wilton", }, diff --git a/src/nodes/Product/index.ts b/src/nodes/Product/index.ts index 67448c4..cdae888 100644 --- a/src/nodes/Product/index.ts +++ b/src/nodes/Product/index.ts @@ -86,19 +86,14 @@ export const productResolver = defineSchemaOrgResolver({ 'image', { meta: 'title', key: 'name' }, ], + idPrefix: ['url', ProductId], resolve(node, ctx) { - setIfEmpty(node, '@id', prefixId(ctx.meta.url, ProductId)) - resolveId(node, ctx.meta.url) // provide a default sku setIfEmpty(node, 'sku', hash(node.name)) - if (node.aggregateOffer) - node.aggregateOffer = resolveRelation(node.aggregateOffer, ctx, aggregateOfferResolver) - if (node.aggregateRating) - node.aggregateRating = resolveRelation(node.aggregateRating, ctx, aggregateRatingResolver) - if (node.offers) - node.offers = resolveRelation(node.offers, ctx, offerResolver) - if (node.review) - node.review = resolveRelation(node.review, ctx, reviewResolver) + node.aggregateOffer = resolveRelation(node.aggregateOffer, ctx, aggregateOfferResolver) + node.aggregateRating = resolveRelation(node.aggregateRating, ctx, aggregateRatingResolver) + node.offers = resolveRelation(node.offers, ctx, offerResolver) + node.review = resolveRelation(node.review, ctx, reviewResolver) return node }, rootNodeResolve(product, { findNode }) { diff --git a/src/nodes/Question/index.ts b/src/nodes/Question/index.ts index 7fb99da..d01bc2e 100644 --- a/src/nodes/Question/index.ts +++ b/src/nodes/Question/index.ts @@ -47,13 +47,12 @@ export const questionResolver = defineSchemaOrgResolver({ inheritMeta: [ 'inLanguage', ], + idPrefix: 'url', resolve(question, ctx) { if (question.question) question.name = question.question if (question.answer) question.acceptedAnswer = question.answer - // generate dynamic id if none has been set - resolveId(question, ctx.meta.url) // resolve string answer to Answer question.acceptedAnswer = resolveRelation(question.acceptedAnswer, ctx, answerResolver) return question diff --git a/src/nodes/Recipe/index.ts b/src/nodes/Recipe/index.ts index 67330a7..9036adc 100644 --- a/src/nodes/Recipe/index.ts +++ b/src/nodes/Recipe/index.ts @@ -21,11 +21,6 @@ import type { Image } from '../Image' import type { Person } from '../Person' export interface RecipeLite extends Thing { - '@type'?: 'Recipe' - /** - * Referencing the WebPage or Article by ID. - */ - mainEntityOfPage?: IdReference /** * A string describing the recipe. */ @@ -123,13 +118,9 @@ export const recipeResolver = defineSchemaOrgResolver({ 'image', 'datePublished', ], + idPrefix: ['url', RecipeId], resolve(node, ctx) { - setIfEmpty(node, '@id', prefixId(ctx.meta.url, RecipeId)) - - resolveId(node, ctx.meta.url) - // @todo fix types - if (node.recipeInstructions) - node.recipeInstructions = resolveRelation(node.recipeInstructions, ctx, howToStepResolver) + node.recipeInstructions = resolveRelation(node.recipeInstructions, ctx, howToStepResolver) return node }, rootNodeResolve(node, { findNode }) { diff --git a/src/nodes/Review/index.ts b/src/nodes/Review/index.ts index 4a6081c..8748402 100644 --- a/src/nodes/Review/index.ts +++ b/src/nodes/Review/index.ts @@ -43,10 +43,8 @@ export const reviewResolver = defineSchemaOrgResolver({ 'inLanguage', ], resolve(review, ctx) { - if (review.reviewRating) - review.reviewRating = resolveRelation(review.reviewRating, ctx, ratingResolver) - if (review.author) - review.author = resolveRelation(review.author, ctx, personResolver) + review.reviewRating = resolveRelation(review.reviewRating, ctx, ratingResolver) + review.author = resolveRelation(review.author, ctx, personResolver) return review }, }) diff --git a/src/nodes/Video/index.ts b/src/nodes/Video/index.ts index 825fc0f..9b2e661 100644 --- a/src/nodes/Video/index.ts +++ b/src/nodes/Video/index.ts @@ -2,8 +2,7 @@ import type { Id, NodeRelation, ResolvableDate, Thing } from '../../types' import { asArray, resolvableDateToIso, - resolveId, - resolveWithBaseUrl, setIfEmpty, + resolveWithBase, setIfEmpty, } from '../../utils' import type { Image } from '../Image' import { defineSchemaOrgResolver, resolveRelation } from '../../core' @@ -26,7 +25,7 @@ export interface VideoLite extends Thing { /** * The date the video was published, in ISO 8601 format (e.g., 2020-01-20). */ - uploadDate: ResolvableDate + uploadDate?: ResolvableDate /** * Whether the video should be considered 'family friendly' */ @@ -95,11 +94,11 @@ export const videoResolver = defineSchemaOrgResolver