diff --git a/src/index.ts b/src/index.ts index 69a901a..8268241 100644 --- a/src/index.ts +++ b/src/index.ts @@ -25,6 +25,7 @@ export * from './nodes/Product' export * from './nodes/Question' export * from './nodes/Rating' export * from './nodes/Recipe' +export * from './nodes/SoftwareApp' export * from './nodes/Review' export * from './nodes/Video' export * from './nodes/WebPage' diff --git a/src/nodes/SoftwareApp/index.test.ts b/src/nodes/SoftwareApp/index.test.ts new file mode 100644 index 0000000..545fd8b --- /dev/null +++ b/src/nodes/SoftwareApp/index.test.ts @@ -0,0 +1,50 @@ +import { expect } from 'vitest' +import { injectSchemaOrg, useSchemaOrg, useSetup } from '../../../.test' +import { defineSoftwareApp } from '#provider' + +describe('defineSoftwareApp', () => { + it('can be defined', () => { + useSetup(() => { + useSchemaOrg([ + defineSoftwareApp({ + name: 'Angry Birds', + operatingSystem: 'ANDROID', + applicationCategory: 'GameApplication', + aggregateRating: { + ratingValue: '4.6', + ratingCount: 8864, + }, + offers: { + price: '1.00', + priceCurrency: 'USD', + }, + }), + ]) + + const { graphNodes } = injectSchemaOrg() + + expect(graphNodes).toMatchInlineSnapshot(` + [ + { + "@type": "SoftwareApplication", + "aggregateRating": { + "@type": "AggregateRating", + "ratingCount": 8864, + "ratingValue": "4.6", + }, + "applicationCategory": "GameApplication", + "name": "Angry Birds", + "offers": { + "@type": "Offer", + "availability": "https://schema.org/InStock", + "price": "1.00", + "priceCurrency": "USD", + "priceValidUntil": "2023-12-30T00:00:00.000Z", + }, + "operatingSystem": "ANDROID", + }, + ] + `) + }) + }) +}) diff --git a/src/nodes/SoftwareApp/index.ts b/src/nodes/SoftwareApp/index.ts new file mode 100644 index 0000000..3ecb623 --- /dev/null +++ b/src/nodes/SoftwareApp/index.ts @@ -0,0 +1,79 @@ +import type { Arrayable, NodeRelation, NodeRelations, Thing } from '../../types' +import { defineSchemaOrgResolver, resolveRelation } from '../../core' +import type { Offer } from '../Offer' +import { offerResolver } from '../Offer' +import type { AggregateRating } from '../AggregateRating' +import { aggregateRatingResolver } from '../AggregateRating' +import type { Review } from '../Review' +import { reviewResolver } from '../Review' +import { resolveDefaultType } from '../../utils' + +type ApplicationCategory = +'GameApplication' | +'SocialNetworkingApplication' | +'TravelApplication' | +'ShoppingApplication' | +'SportsApplication' | +'LifestyleApplication' | +'BusinessApplication' | +'DesignApplication' | +'DeveloperApplication' | +'DriverApplication' | +'EducationalApplication' | +'HealthApplication' | +'FinanceApplication' | +'SecurityApplication' | +'BrowserApplication' | +'CommunicationApplication' | +'DesktopEnhancementApplication' | +'EntertainmentApplication' | +'MultimediaApplication' | +'HomeApplication' | +'UtilitiesApplication' | +'ReferenceApplication' + +export interface SoftwareAppLite extends Thing { + '@type'?: Arrayable<'SoftwareApplication' | 'MobileApplication' | 'VideoGame' | 'WebApplication'> + /** + * The name of the app. + */ + name?: string + /** + * An offer to sell the app. + * For developers, offers can indicate the marketplaces that carry the application. + * For marketplaces, use offers to indicate the price of the app for a specific app instance. + */ + offers: NodeRelations + /** + * The average review score of the app. + */ + aggregateRating?: NodeRelation + /** + * A single review of the app. + */ + review?: NodeRelation + /** + * The type of app (for example, BusinessApplication or GameApplication). The value must be a supported app type. + */ + applicationCategory?: ApplicationCategory + /** + * The operating system(s) required to use the app (for example, Windows 7, OSX 10.6, Android 1.6) + */ + operatingSystem?: string +} + +export interface SoftwareApp extends SoftwareAppLite {} + +export const softwareAppResolver = defineSchemaOrgResolver({ + defaults: { + '@type': 'SoftwareApplication', + }, + resolve(node, ctx) { + resolveDefaultType(node, 'SoftwareApplication') + node.offers = resolveRelation(node.offers, ctx, offerResolver) + node.aggregateRating = resolveRelation(node.aggregateRating, ctx, aggregateRatingResolver) + node.review = resolveRelation(node.review, ctx, reviewResolver) + return node + }, +}) + diff --git a/src/runtime/base.ts b/src/runtime/base.ts index edb8ccb..d4db635 100644 --- a/src/runtime/base.ts +++ b/src/runtime/base.ts @@ -22,7 +22,9 @@ import type { Question, ReadAction, Recipe, - Review, SchemaOrgNodeDefinition, + Review, + SoftwareApp, + SchemaOrgNodeDefinition, SearchAction, VideoObject, VirtualLocation, @@ -56,7 +58,7 @@ import { readActionResolver, recipeResolver, resolveOpeningHours, reviewResolver, - searchActionResolver, + searchActionResolver, softwareAppResolver, videoResolver, virtualLocationResolver, webPageResolver, @@ -88,6 +90,7 @@ export const definePerson = (input?: T) => provideResolver(inp export const defineProduct = (input?: T) => provideResolver(input, productResolver as SchemaOrgNodeDefinition) export const defineQuestion = (input?: T) => provideResolver(input, questionResolver as SchemaOrgNodeDefinition) export const defineRecipe = (input?: T) => provideResolver(input, recipeResolver as SchemaOrgNodeDefinition) +export const defineSoftwareApp = (input?: T) => provideResolver(input, softwareAppResolver as SchemaOrgNodeDefinition) export const defineReview = (input?: T) => provideResolver(input, reviewResolver as SchemaOrgNodeDefinition) export const defineVideo = (input?: T) => provideResolver(input, videoResolver as SchemaOrgNodeDefinition) export const defineWebPage = (input?: T) => provideResolver(input, webPageResolver as SchemaOrgNodeDefinition)