Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions packages/schema-org/src/core/graph.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Arrayable, Id, MetaInput, ResolvedMeta, SchemaOrgNode, Thing } from '../types'
import { imageResolver } from '../nodes'
import { asArray, resolveAsGraphKey } from '../utils'
import { asArray, resolveAsGraphKey, stripNullProperties } from '../utils'
import { resolveMeta, resolveNode, resolveNodeId, resolveRelation } from './resolve'
import { merge } from './util'

Expand Down Expand Up @@ -124,10 +124,17 @@ export function createSchemaOrgGraph(): SchemaOrgGraph {

if (node._resolver?.resolveRootNode)
node._resolver.resolveRootNode(node, ctx)

delete node._resolver
}

// Delete _resolver before stripping so stripNullProperties does not traverse resolver objects
for (let i = 0; i < ctx.nodes.length; i++)
delete ctx.nodes[i]._resolver

// Strip null opt-out sentinels after ALL resolveRootNode calls complete
// so that later resolvers can still observe null sentinels on earlier nodes
for (let i = 0; i < ctx.nodes.length; i++)
stripNullProperties(ctx.nodes[i])

// Final normalization: sort keys and dedupe only if new nodes were added
const needsDedupe = ctx.nodes.length > countBeforeRelations
const normalizedNodes: Record<Id, SchemaOrgNode> = needsDedupe ? Object.create(null) : null!
Expand Down
58 changes: 58 additions & 0 deletions packages/schema-org/src/nodes/Product/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,64 @@ describe('defineProduct', () => {
})
})

it('null opts out of setIfEmpty defaults (brand)', async () => {
await useSetup(async (head) => {
useSchemaOrg(head, [
definePerson({
name: 'Harlan Wilton',
image: '/image/me.png',
}),
defineProduct({
name: 'test',
image: '/product.png',
brand: null as any,
offers: [{ price: 50 }],
}),
])

const graphNodes = await injectSchemaOrg(head)
const product = graphNodes.find((n: any) => n['@type'] === 'Product')
expect(product).not.toHaveProperty('brand')
})
})

it('null opts out of inheritMeta defaults (description)', async () => {
await useSetup(async (head) => {
useSchemaOrg(head, [
defineProduct({
name: 'test',
image: '/product.png',
description: null as any,
}),
])

const graphNodes = await injectSchemaOrg(head)
const product = graphNodes.find((n: any) => n['@type'] === 'Product')
expect(product).not.toHaveProperty('description')
}, { description: 'page description' })
})

it('null opts out of offer defaults (priceValidUntil, priceCurrency)', async () => {
await useSetup(async (head) => {
useSchemaOrg(head, [
defineProduct({
name: 'test',
image: '/product.png',
offers: [{
price: 50,
priceValidUntil: null as any,
priceCurrency: null as any,
}],
}),
])

const graphNodes = await injectSchemaOrg(head)
const product = graphNodes.find((n: any) => n['@type'] === 'Product')
expect(product!.offers).not.toHaveProperty('priceValidUntil')
expect(product!.offers).not.toHaveProperty('priceCurrency')
})
})

it('merchant listing experience', async () => {
await useSetup(async (head) => {
useSchemaOrg(head, [
Expand Down
41 changes: 38 additions & 3 deletions packages/schema-org/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export function resolvableDateToIso(val: Date | string | undefined) {
export const IdentityId = '#identity'

export function setIfEmpty<T extends Thing>(node: T, field: keyof T, value: any) {
if (!node?.[field] && value)
if (node?.[field] === undefined && value != null)
node[field] = value
}

Expand Down Expand Up @@ -134,15 +134,16 @@ export function resolveAsGraphKey(key?: Id | string) {
}

/**
* Removes attributes which have a null or undefined value
* Removes attributes which have an empty string or undefined value.
* null values are preserved as opt-out sentinels until finalStripNullProperties.
*/
export function stripEmptyProperties(obj: any) {
for (const k in obj) {
if (!Object.hasOwn(obj, k))
continue

const v = obj[k]
if (v === '' || v === null || v === undefined) {
if (v === '' || v === undefined) {
delete obj[k]
}
else if (typeof v === 'object' && v !== null) {
Expand All @@ -154,3 +155,37 @@ export function stripEmptyProperties(obj: any) {
}
return obj
}

/**
* Removes attributes which have a null value.
* Called after all resolvers (including resolveRootNode) have run,
* so that null can act as an explicit opt-out sentinel.
*/
export function stripNullProperties(obj: any) {
if (Array.isArray(obj)) {
for (let i = obj.length - 1; i >= 0; i--) {
const v = obj[i]
if (v === null)
obj.splice(i, 1)
else if (typeof v === 'object' && v !== null)
stripNullProperties(v)
}
return obj
}

for (const k in obj) {
if (!Object.hasOwn(obj, k))
continue

const v = obj[k]
if (v === null) {
delete obj[k]
}
else if (typeof v === 'object') {
if (v.__v_isReadonly || v.__v_isRef)
continue
stripNullProperties(v)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
return obj
}