Skip to content
This repository has been archived by the owner on Sep 3, 2023. It is now read-only.

Commit

Permalink
fix: simplify id resolving and aggressive const ids
Browse files Browse the repository at this point in the history
  • Loading branch information
harlan-zw committed Aug 15, 2022
1 parent 739ac42 commit 08ecd61
Show file tree
Hide file tree
Showing 32 changed files with 256 additions and 273 deletions.
4 changes: 2 additions & 2 deletions src/core/build.ts
Expand Up @@ -27,7 +27,7 @@ export const dedupeAndFlattenNodes = (nodes: RegisteredThing[]) => {
const dedupedNodes: Record<Id, RegisteredThing> = {}
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(
Expand Down Expand Up @@ -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
})
Expand Down
38 changes: 30 additions & 8 deletions src/core/resolve.ts
Expand Up @@ -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 = <T extends Thing>(node: T, ctx: SchemaOrgContext, resolver: SchemaOrgNodeDefinition<T>) => {
Expand All @@ -26,19 +26,41 @@ export const executeResolverOnNode = <T extends Thing>(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 = <T extends Thing>(node: T, ctx: SchemaOrgContext, resolver: SchemaOrgNodeDefinition<T>) => {
export const resolveNodeId = <T extends Thing>(node: T, ctx: SchemaOrgContext, resolver: SchemaOrgNodeDefinition<T>, 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) {
Expand All @@ -51,12 +73,12 @@ export const resolveNodeId = <T extends Thing>(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<any>, ctx: any,
export function resolveRelation(input: Arrayable<any>, ctx: SchemaOrgContext,
resolver: SchemaOrgNodeDefinition<any>,
options: ResolverOptions = {},
) {
Expand All @@ -78,12 +100,12 @@ export function resolveRelation(input: Arrayable<any>, 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'])
}

Expand Down
4 changes: 1 addition & 3 deletions src/nodes/AggregateOffer/index.ts
Expand Up @@ -38,9 +38,7 @@ export const aggregateOfferResolver = defineSchemaOrgResolver<AggregateOffer>({
'@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
},
Expand Down
16 changes: 8 additions & 8 deletions src/nodes/Article/index.test.ts
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
60 changes: 25 additions & 35 deletions src/nodes/Article/index.ts
Expand Up @@ -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'
Expand Down Expand Up @@ -132,57 +129,50 @@ export const articleResolver = defineSchemaOrgResolver<Article>({
'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<ValidArticleSubTypes>
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<WebPage>(PrimaryWebPageId)
const identity = findNode<Organization | Person>(IdentityId)
const identity = findNode<Identity>(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<Image>(firstImage['@id'])?.url)
setIfEmpty(node, 'thumbnailUrl', findNode<Image>(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',
'target': [meta.url],
},
])
// 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
},
})
23 changes: 23 additions & 0 deletions src/nodes/Breadcrumb/index.test.ts
Expand Up @@ -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",
Expand Down
7 changes: 3 additions & 4 deletions src/nodes/Breadcrumb/index.ts
Expand Up @@ -46,9 +46,8 @@ export const breadcrumbResolver = defineSchemaOrgResolver<Breadcrumb>({
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

Expand All @@ -61,10 +60,10 @@ export const breadcrumbResolver = defineSchemaOrgResolver<Breadcrumb>({
}
return breadcrumb
},
rootNodeResolve(breadcrumb, { findNode }) {
rootNodeResolve(node, { findNode }) {
// merge breadcrumbs reference into the webpage
const webPage = findNode<WebPage>(PrimaryWebPageId)
if (webPage)
setIfEmpty(webPage, 'breadcrumb', idReference(breadcrumb))
setIfEmpty(webPage, 'breadcrumb', idReference(node))
},
})
7 changes: 3 additions & 4 deletions src/nodes/Comment/index.test.ts
Expand Up @@ -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/",
},
]
`)
Expand Down
10 changes: 4 additions & 6 deletions src/nodes/Comment/index.ts
Expand Up @@ -33,13 +33,11 @@ export const commentResolver = defineSchemaOrgResolver<Comment>({
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 }) {
Expand Down
4 changes: 1 addition & 3 deletions src/nodes/Event/Place/index.ts
Expand Up @@ -19,9 +19,7 @@ export const placeResolver = defineSchemaOrgResolver<Place>({
'@type': 'Place',
},
resolve(node, ctx) {
if (node.address)
node.address = resolveRelation(node.address, ctx, addressResolver)

node.address = resolveRelation(node.address, ctx, addressResolver)
return node
},
})

0 comments on commit 08ecd61

Please sign in to comment.