diff options
-rw-r--r-- | src/data/cacheable-object.js (renamed from src/thing/cacheable-object.js) | 4 | ||||
-rw-r--r-- | src/data/things.js | 783 | ||||
-rw-r--r-- | src/data/validators.js (renamed from src/thing/validators.js) | 0 | ||||
-rw-r--r-- | src/thing/album.js | 270 | ||||
-rw-r--r-- | src/thing/art-tag.js | 37 | ||||
-rw-r--r-- | src/thing/artist.js | 48 | ||||
-rw-r--r-- | src/thing/flash.js | 129 | ||||
-rw-r--r-- | src/thing/group.js | 73 | ||||
-rw-r--r-- | src/thing/homepage-layout.js | 99 | ||||
-rw-r--r-- | src/thing/news-entry.js | 49 | ||||
-rw-r--r-- | src/thing/static-page.js | 52 | ||||
-rw-r--r-- | src/thing/structures.js | 1 | ||||
-rw-r--r-- | src/thing/thing.js | 62 | ||||
-rw-r--r-- | src/thing/track.js | 173 | ||||
-rw-r--r-- | src/thing/wiki-info.js | 90 | ||||
-rwxr-xr-x | src/upd8.js | 36 | ||||
-rw-r--r-- | src/util/sugar.js | 3 | ||||
-rw-r--r-- | test/cacheable-object.js | 2 | ||||
-rw-r--r-- | test/data-validators.js | 2 |
19 files changed, 812 insertions, 1101 deletions
diff --git a/src/thing/cacheable-object.js b/src/data/cacheable-object.js index 9af41603..99280956 100644 --- a/src/thing/cacheable-object.js +++ b/src/data/cacheable-object.js @@ -134,6 +134,10 @@ export default class CacheableObject { } #defineProperties() { + if (!this.constructor.propertyDescriptors) { + throw new Error(`Expected constructor ${this.constructor.name} to define propertyDescriptors`); + } + for (const [ property, descriptor ] of Object.entries(this.constructor.propertyDescriptors)) { const { flags } = descriptor; diff --git a/src/data/things.js b/src/data/things.js new file mode 100644 index 00000000..66176013 --- /dev/null +++ b/src/data/things.js @@ -0,0 +1,783 @@ +// things.js: class definitions for various object types used across the wiki, +// most of which correspond to an output page, such as Track, Album, Artist + +import CacheableObject from './cacheable-object.js'; + +import { + isBoolean, + isColor, + isCommentary, + isCountingNumber, + isContributionList, + isDate, + isDimensions, + isDirectory, + isDuration, + isInstance, + isFileExtension, + isLanguageCode, + isName, + isNumber, + isURL, + isString, + oneOf, + validateArrayItems, + validateInstanceOf, + validateReference, + validateReferenceList, +} from './validators.js'; + +import { + getKebabCase, +} from '../util/wiki-data.js'; + +import find from '../util/find.js'; + +// Stub classes (and their exports) at the top of the file - these are +// referenced later when we actually define static class fields. We deliberately +// define the classes and set their static fields in two separate steps so that +// every class coexists from the outset, and can be directly referenced in field +// definitions later. + +// This list also acts as a quick table of contents for this JS file - use +// ctrl+F or similar to skip to a section. + +// -> Thing +export class Thing extends CacheableObject {} + +// -> Album +export class Album extends Thing {} +export class TrackGroup extends CacheableObject {} + +// -> Track +export class Track extends Thing {} + +// -> Artist +export class Artist extends Thing {} + +// -> Group +export class Group extends Thing {} +export class GroupCategory extends CacheableObject {} + +// -> ArtTag +export class ArtTag extends Thing {} + +// -> NewsEntry +export class NewsEntry extends Thing {} + +// -> StaticPage +export class StaticPage extends Thing {} + +// -> HomepageLayout +export class HomepageLayout extends CacheableObject {} +export class HomepageLayoutRow extends CacheableObject {} +export class HomepageLayoutAlbumsRow extends HomepageLayoutRow {} + +// -> Flash +export class Flash extends Thing {} +export class FlashAct extends CacheableObject {} + +// -> WikiInfo +export class WikiInfo extends CacheableObject {} + +// Before initializing property descriptors, set additional independent +// constants on the classes (which are referenced later). + +Thing.referenceType = Symbol('Thing.referenceType'); + +Album[Thing.referenceType] = 'album'; +Track[Thing.referenceType] = 'track'; +Artist[Thing.referenceType] = 'artist'; +Group[Thing.referenceType] = 'group'; +ArtTag[Thing.referenceType] = 'tag'; +NewsEntry[Thing.referenceType] = 'news-entry'; +StaticPage[Thing.referenceType] = 'static'; +Flash[Thing.referenceType] = 'flash'; + +// -> Thing: base class for wiki data types, providing wiki-specific utility +// functions on top of essential CacheableObject behavior. + +// Regularly reused property descriptors, for ease of access and generally +// duplicating less code across wiki data types. These are specialized utility +// functions, so check each for how its own arguments behave! +Thing.common = { + name: (defaultName) => ({ + flags: {update: true, expose: true}, + update: {validate: isName, default: defaultName} + }), + + color: () => ({ + flags: {update: true, expose: true}, + update: {validate: isColor} + }), + + directory: () => ({ + flags: {update: true, expose: true}, + update: {validate: isDirectory}, + expose: { + dependencies: ['name'], + transform(directory, { name }) { + if (directory === null && name === null) + return null; + else if (directory === null) + return getKebabCase(name); + else + return directory; + } + } + }), + + urls: () => ({ + flags: {update: true, expose: true}, + update: {validate: validateArrayItems(isURL)} + }), + + // Straightforward flag descriptor for a variety of property purposes. + // Provide a default value, true or false! + flag: (defaultValue = false) => { + if (typeof defaultValue !== 'boolean') { + throw new TypeError(`Always set explicit defaults for flags!`); + } + + return { + flags: {update: true, expose: true}, + update: {validate: isBoolean, default: defaultValue} + }; + }, + + // General date type, used as the descriptor for a bunch of properties. + // This isn't dynamic though - it won't inherit from a date stored on + // another object, for example. + simpleDate: () => ({ + flags: {update: true, expose: true}, + update: {validate: isDate} + }), + + // General string type. This should probably generally be avoided in favor + // of more specific validation, but using it makes it easy to find where we + // might want to improve later, and it's a useful shorthand meanwhile. + simpleString: () => ({ + flags: {update: true, expose: true}, + update: {validate: isString} + }), + + // Super simple "contributions by reference" list, used for a variety of + // properties (Artists, Cover Artists, etc). This is the property which is + // externally provided, in the form: + // + // [ + // {who: 'Artist Name', what: 'Viola'}, + // {who: 'artist:john-cena', what: null}, + // ... + // ] + // + // ...processed from YAML, spreadsheet, or any other kind of input. + contribsByRef: () => ({ + flags: {update: true, expose: true}, + update: {validate: isContributionList} + }), + + // A reference list! Keep in mind this is for general references to wiki + // objects of (usually) other Thing subclasses, not specifically leitmotif + // references in tracks (although that property uses referenceList too!). + // + // The underlying function validateReferenceList expects a string like + // 'artist' or 'track', but this utility keeps from having to hard-code the + // string in multiple places by referencing the value saved on the class + // instead. + referenceList: thingClass => { + const { [Thing.referenceType]: referenceType } = thingClass; + if (!referenceType) { + throw new Error(`The passed constructor ${thingClass.name} doesn't define Thing.referenceType!`); + } + + return { + flags: {update: true, expose: true}, + update: {validate: validateReferenceList(referenceType)} + }; + }, + + // Corresponding dynamic property to contribsByRef, which takes the values + // in the provided property and searches the object's artistData for + // matching actual Artist objects. The computed structure has the same form + // as contribsByRef, but with Artist objects instead of string references: + // + // [ + // {who: (an Artist), what: 'Viola'}, + // {who: (an Artist), what: null}, + // ... + // ] + // + // Contributions whose "who" values don't match anything in artistData are + // filtered out. (So if the list is all empty, chances are that either the + // reference list is somehow messed up, or artistData isn't being provided + // properly.) + dynamicContribs: (contribsByRefProperty) => ({ + flags: {expose: true}, + expose: { + dependencies: ['artistData', contribsByRefProperty], + compute: ({ artistData, [contribsByRefProperty]: contribsByRef }) => ( + (contribsByRef && artistData + ? (contribsByRef + .map(({ who: ref, what }) => ({ + who: find.artist(ref, {wikiData: {artistData}}), + what + })) + .filter(({ who }) => who)) + : []) + ) + } + }), + + // General purpose wiki data constructor, for properties like artistData, + // trackData, etc. + wikiData: (thingClass) => ({ + flags: {update: true}, + update: { + validate: validateArrayItems(validateInstanceOf(thingClass)) + } + }) +}; + +Thing.getReference = function(thing) { + if (!thing.constructor[Thing.referenceType]) + throw TypeError(`Passed Thing is ${thing.constructor.name}, which provides no [Thing.referenceType]`); + + if (!thing.directory) + throw TypeError(`Passed ${thing.constructor.name} is missing its directory`); + + return `${thing.constructor[Thing.referenceType]}:${thing.directory}`; +}; + +// -> Album + +Album.propertyDescriptors = { + // Update & expose + + name: Thing.common.name('Unnamed Album'), + color: Thing.common.color(), + directory: Thing.common.directory(), + urls: Thing.common.urls(), + + date: Thing.common.simpleDate(), + coverArtDate: Thing.common.simpleDate(), + trackArtDate: Thing.common.simpleDate(), + dateAddedToWiki: Thing.common.simpleDate(), + + artistContribsByRef: Thing.common.contribsByRef(), + coverArtistContribsByRef: Thing.common.contribsByRef(), + trackCoverArtistContribsByRef: Thing.common.contribsByRef(), + wallpaperArtistContribsByRef: Thing.common.contribsByRef(), + bannerArtistContribsByRef: Thing.common.contribsByRef(), + + groupsByRef: { + flags: {update: true, expose: true}, + + update: { + validate: validateReferenceList('group') + } + }, + + artTagsByRef: { + flags: {update: true, expose: true}, + + update: { + validate: validateReferenceList('tag') + } + }, + + trackGroups: { + flags: {update: true, expose: true}, + + update: { + validate: validateArrayItems(validateInstanceOf(TrackGroup)) + } + }, + + wallpaperStyle: Thing.common.simpleString(), + + wallpaperFileExtension: { + flags: {update: true, expose: true}, + update: {validate: isFileExtension} + }, + + bannerStyle: Thing.common.simpleString(), + + bannerFileExtension: { + flags: {update: true, expose: true}, + update: {validate: isFileExtension} + }, + + bannerDimensions: { + flags: {update: true, expose: true}, + update: {validate: isDimensions} + }, + + hasTrackArt: Thing.common.flag(true), + isMajorRelease: Thing.common.flag(false), + isListedOnHomepage: Thing.common.flag(true), + + commentary: { + flags: {update: true, expose: true}, + update: {validate: isCommentary} + }, + + // Update only + + artistData: Thing.common.wikiData(Artist), + trackData: Thing.common.wikiData(Track), + + // Expose only + + // Previously known as: (album).artists + artistContribs: Thing.common.dynamicContribs('artistContribsByRef'), + + tracks: { + flags: {expose: true}, + + expose: { + dependencies: ['trackGroups', 'trackData'], + compute: ({ trackGroups, trackData }) => ( + (trackGroups && trackData + ? (trackGroups + .flatMap(group => group.tracksByRef ?? []) + .map(ref => find.track(ref, {wikiData: {trackData}})) + .filter(Boolean)) + : []) + ) + } + }, +}; + +TrackGroup.propertyDescriptors = { + // Update & expose + + name: Thing.common.name('Unnamed Track Group'), + color: Thing.common.color(), + + dateOriginallyReleased: Thing.common.simpleDate(), + + tracksByRef: { + flags: {update: true, expose: true}, + update: {validate: validateReferenceList('track')} + }, + + isDefaultTrackGroup: Thing.common.flag(false), + + // Update only + + trackData: Thing.common.wikiData(Track), + + // Expose only + + tracks: { + flags: {expose: true}, + + expose: { + dependencies: ['tracksByRef', 'trackData'], + compute: ({ tracksByRef, trackData }) => ( + (tracksByRef && trackData + ? (tracksByRef + .map(ref => find.track(ref, {wikiData: {trackData}})) + .filter(Boolean)) + : []) + ) + } + }, +}; + +// -> Track + +Track.propertyDescriptors = { + // Update & expose + + name: Thing.common.name('Unnamed Track'), + directory: Thing.common.directory(), + + duration: { + flags: {update: true, expose: true}, + update: {validate: isDuration} + }, + + urls: Thing.common.urls(), + dateFirstReleased: Thing.common.simpleDate(), + coverArtDate: Thing.common.simpleDate(), + + hasCoverArt: Thing.common.flag(true), + hasURLs: Thing.common.flag(true), + + referencedTracksByRef: { + flags: {update: true, expose: true}, + update: {validate: validateReferenceList('track')} + }, + + artistContribsByRef: Thing.common.contribsByRef(), + contributorContribsByRef: Thing.common.contribsByRef(), + coverArtistContribsByRef: Thing.common.contribsByRef(), + + artTagsByRef: { + flags: {update: true, expose: true}, + update: {validate: validateReferenceList('tag')} + }, + + // Previously known as: (track).aka + originalReleaseTrackByRef: { + flags: {update: true, expose: true}, + update: {validate: validateReference('track')} + }, + + commentary: { + flags: {update: true, expose: true}, + update: {validate: isCommentary} + }, + + lyrics: Thing.common.simpleString(), + + // Update only + + albumData: Thing.common.wikiData(Album), + artistData: Thing.common.wikiData(Artist), + artTagData: Thing.common.wikiData(ArtTag), + + // Expose only + + album: { + flags: {expose: true}, + + expose: { + dependencies: ['albumData'], + compute: ({ [Track.instance]: track, albumData }) => ( + albumData?.find(album => album.tracks.includes(track)) ?? null) + } + }, + + date: { + flags: {expose: true}, + + expose: { + dependencies: ['albumData', 'dateFirstReleased'], + compute: ({ albumData, dateFirstReleased, [Track.instance]: track }) => ( + dateFirstReleased ?? + albumData?.find(album => album.tracks.includes(track))?.date ?? + null + ) + } + }, + + // Previously known as: (track).artists + artistContribs: Thing.common.dynamicContribs('artistContribsByRef'), + + artTags: { + flags: {expose: true}, + + expose: { + dependencies: ['artTagsByRef', 'artTagData'], + + compute: ({ artTagsByRef, artTagData }) => ( + (artTagsByRef && artTagData + ? (artTagsByRef + .map(ref => find.tag(ref, {wikiData: {tagData: artTagData}})) + .filter(Boolean)) + : []) + ) + } + } +}; + +// -> Artist + +Artist.propertyDescriptors = { + // Update & expose + + name: Thing.common.name('Unnamed Artist'), + directory: Thing.common.directory(), + urls: Thing.common.urls(), + + aliasRefs: { + flags: {update: true, expose: true}, + update: {validate: validateReferenceList('artist')} + }, + + contextNotes: Thing.common.simpleString(), +}; + +// -> Group + +Group.propertyDescriptors = { + // Update & expose + + name: Thing.common.name('Unnamed Group'), + directory: Thing.common.directory(), + + description: Thing.common.simpleString(), + + urls: Thing.common.urls(), + + // Expose only + + descriptionShort: { + flags: {expose: true}, + + expose: { + dependencies: ['description'], + compute: ({ description }) => description.split('<hr class="split">')[0] + } + } +}; + +GroupCategory.propertyDescriptors = { + // Update & expose + + name: Thing.common.name('Unnamed Group Category'), + color: Thing.common.color(), + + groupsByRef: { + flags: {update: true, expose: true}, + update: {validate: validateReferenceList('group')} + }, +}; + +// -> ArtTag + +ArtTag.propertyDescriptors = { + // Update & expose + + name: Thing.common.name('Unnamed Art Tag'), + directory: Thing.common.directory(), + color: Thing.common.color(), + isContentWarning: Thing.common.flag(false), +}; + +// -> NewsEntry + +NewsEntry.propertyDescriptors = { + // Update & expose + + name: Thing.common.name('Unnamed News Entry'), + directory: Thing.common.directory(), + date: Thing.common.simpleDate(), + + content: Thing.common.simpleString(), + + // Expose only + + contentShort: { + flags: {expose: true}, + + expose: { + dependencies: ['content'], + + compute({ content }) { + return body.split('<hr class="split">')[0]; + } + } + }, +}; + +// -> StaticPage + +StaticPage.propertyDescriptors = { + // Update & expose + + name: Thing.common.name('Unnamed Static Page'), + + nameShort: { + flags: {update: true, expose: true}, + update: {validate: isName}, + + expose: { + dependencies: ['name'], + transform: (value, { name }) => value ?? name + } + }, + + directory: Thing.common.directory(), + content: Thing.common.simpleString(), + stylesheet: Thing.common.simpleString(), + showInNavigationBar: Thing.common.flag(true), +}; + +// -> HomepageLayout + +HomepageLayout.propertyDescriptors = { + // Update & expose + + sidebarContent: Thing.common.simpleString(), + + rows: { + flags: {update: true, expose: true}, + + update: { + validate: validateArrayItems(validateInstanceOf(HomepageLayoutRow)) + } + }, +}; + +HomepageLayoutRow.propertyDescriptors = { + // Update & expose + + name: Thing.common.name('Unnamed Homepage Row'), + + type: { + flags: {update: true, expose: true}, + + update: { + validate(value) { + throw new Error(`'type' property validator must be overridden`); + } + } + }, + + color: Thing.common.color(), +}; + +HomepageLayoutAlbumsRow.propertyDescriptors = { + ...HomepageLayoutRow.propertyDescriptors, + + // Update & expose + + type: { + flags: {update: true, expose: true}, + update: { + validate(value) { + if (value !== 'albums') { + throw new TypeError(`Expected 'albums'`); + } + + return true; + } + } + }, + + sourceGroupByRef: { + flags: {update: true, expose: true}, + update: {validate: validateReference('group')} + }, + + sourceAlbumsByRef: { + flags: {update: true, expose: true}, + update: {validate: validateReferenceList('album')} + }, + + countAlbumsFromGroup: { + flags: {update: true, expose: true}, + update: {validate: isCountingNumber} + }, + + actionLinks: { + flags: {update: true, expose: true}, + update: {validate: validateArrayItems(isString)} + }, +}; + +// -> Flash + +Flash.propertyDescriptors = { + // Update & expose + + name: Thing.common.name('Unnamed Flash'), + + directory: { + flags: {update: true, expose: true}, + update: {validate: isDirectory}, + + // Flashes expose directory differently from other Things! Their + // default directory is dependent on the page number (or ID), not + // the name. + expose: { + dependencies: ['page'], + transform(directory, { page }) { + if (directory === null && page === null) + return null; + else if (directory === null) + return page; + else + return directory; + } + } + }, + + page: { + flags: {update: true, expose: true}, + update: {validate: oneOf(isString, isNumber)}, + + expose: { + transform: value => value.toString() + } + }, + + date: Thing.common.simpleDate(), + + coverArtFileExtension: { + flags: {update: true, expose: true}, + update: {validate: isFileExtension} + }, + + featuredTracksByRef: { + flags: {update: true, expose: true}, + update: {validate: validateReferenceList('track')} + }, + + contributorContribsByRef: Thing.common.contribsByRef(), + urls: Thing.common.urls(), +}; + +FlashAct.propertyDescriptors = { + // Update & expose + + name: Thing.common.name('Unnamed Flash Act'), + color: Thing.common.color(), + anchor: Thing.common.simpleString(), + jump: Thing.common.simpleString(), + jumpColor: Thing.common.color(), + + flashesByRef: { + flags: {update: true, expose: true}, + update: {validate: validateReferenceList('flash')} + }, +}; + +// WikiInfo + +WikiInfo.propertyDescriptors = { + // Update & expose + + name: Thing.common.name('Unnamed Wiki'), + + // Displayed in nav bar. + shortName: { + flags: {update: true, expose: true}, + update: {validate: isName}, + + expose: { + dependencies: ['name'], + transform: (value, { name }) => value ?? name + } + }, + + color: Thing.common.color(), + + // One-line description used for <meta rel="description"> tag. + description: Thing.common.simpleString(), + + footerContent: Thing.common.simpleString(), + + defaultLanguage: { + flags: {update: true, expose: true}, + update: {validate: isLanguageCode} + }, + + canonicalBase: { + flags: {update: true, expose: true}, + update: {validate: isURL} + }, + + // Feature toggles + + enableArtistAvatars: Thing.common.flag(false), + enableFlashesAndGames: Thing.common.flag(false), + enableListings: Thing.common.flag(false), + enableNews: Thing.common.flag(false), + enableArtTagUI: Thing.common.flag(false), + enableGroupUI: Thing.common.flag(false), +}; diff --git a/src/thing/validators.js b/src/data/validators.js index 83922229..83922229 100644 --- a/src/thing/validators.js +++ b/src/data/validators.js diff --git a/src/thing/album.js b/src/thing/album.js deleted file mode 100644 index ba75352d..00000000 --- a/src/thing/album.js +++ /dev/null @@ -1,270 +0,0 @@ -import CacheableObject from './cacheable-object.js'; -import Thing from './thing.js'; - -import { - isBoolean, - isColor, - isCommentary, - isContributionList, - isDate, - isDimensions, - isDirectory, - isInstance, - isFileExtension, - isName, - isURL, - isString, - validateArrayItems, - validateInstanceOf, - validateReference, - validateReferenceList, -} from './validators.js'; - -import Artist from './artist.js'; -import ArtTag from './art-tag.js'; -import Track from './track.js'; - -import find from '../util/find.js'; - -export class TrackGroup extends CacheableObject { - static propertyDescriptors = { - // Update & expose - - name: { - flags: {update: true, expose: true}, - update: {default: 'Unnamed Track Group', validate: isName} - }, - - color: { - flags: {update: true, expose: true}, - update: {validate: isColor} - }, - - dateOriginallyReleased: { - flags: {update: true, expose: true}, - update: {validate: isDate} - }, - - tracksByRef: { - flags: {update: true, expose: true}, - update: {validate: validateReferenceList('track')} - }, - - isDefaultTrackGroup: { - flags: {update: true, expose: true}, - update: {validate: isBoolean} - }, - - // Update only - - trackData: { - flags: {update: true}, - update: {validate: validateArrayItems(item => isInstance(item, Track))} - }, - - // Expose only - - tracks: { - flags: {expose: true}, - - expose: { - dependencies: ['tracksByRef', 'trackData'], - compute: ({ tracksByRef, trackData }) => ( - (tracksByRef && trackData - ? (tracksByRef - .map(ref => find.track(ref, {wikiData: {trackData}})) - .filter(Boolean)) - : []) - ) - } - }, - }; -} - -export default class Album extends Thing { - static [Thing.referenceType] = 'album'; - - static propertyDescriptors = { - // Update & expose - - name: { - flags: {update: true, expose: true}, - update: {default: 'Unnamed Album', validate: isName} - }, - - color: { - flags: {update: true, expose: true}, - update: {validate: isColor} - }, - - directory: { - flags: {update: true, expose: true}, - update: {validate: isDirectory}, - expose: Thing.directoryExpose - }, - - urls: { - flags: {update: true, expose: true}, - - update: { - validate: validateArrayItems(isURL) - } - }, - - date: { - flags: {update: true, expose: true}, - update: {validate: isDate} - }, - - coverArtDate: { - flags: {update: true, expose: true}, - update: {validate: isDate} - }, - - trackArtDate: { - flags: {update: true, expose: true}, - update: {validate: isDate} - }, - - dateAddedToWiki: { - flags: {update: true, expose: true}, - - update: {validate: isDate} - }, - - artistContribsByRef: { - flags: {update: true, expose: true}, - update: {validate: isContributionList} - }, - - coverArtistContribsByRef: { - flags: {update: true, expose: true}, - update: {validate: isContributionList} - }, - - trackCoverArtistContribsByRef: { - flags: {update: true, expose: true}, - update: {validate: isContributionList} - }, - - wallpaperArtistContribsByRef: { - flags: {update: true, expose: true}, - update: {validate: isContributionList} - }, - - bannerArtistContribsByRef: { - flags: {update: true, expose: true}, - update: {validate: isContributionList} - }, - - groupsByRef: { - flags: {update: true, expose: true}, - - update: { - validate: validateReferenceList('group') - } - }, - - artTagsByRef: { - flags: {update: true, expose: true}, - - update: { - validate: validateReferenceList('tag') - } - }, - - trackGroups: { - flags: {update: true, expose: true}, - - update: { - validate: validateArrayItems(validateInstanceOf(TrackGroup)) - } - }, - - wallpaperStyle: { - flags: {update: true, expose: true}, - update: {validate: isString} - }, - - wallpaperFileExtension: { - flags: {update: true, expose: true}, - update: {validate: isFileExtension} - }, - - bannerStyle: { - flags: {update: true, expose: true}, - update: {validate: isString} - }, - - bannerFileExtension: { - flags: {update: true, expose: true}, - update: {validate: isFileExtension} - }, - - bannerDimensions: { - flags: {update: true, expose: true}, - update: {validate: isDimensions} - }, - - hasTrackArt: { - flags: {update: true, expose: true}, - - update: { - default: true, - validate: isBoolean - } - }, - - isMajorRelease: { - flags: {update: true, expose: true}, - - update: { - default: false, - validate: isBoolean - } - }, - - isListedOnHomepage: { - flags: {update: true, expose: true}, - - update: { - default: true, - validate: isBoolean - } - }, - - commentary: { - flags: {update: true, expose: true}, - update: {validate: isCommentary} - }, - - // Update only - - artistData: Thing.genWikiDataProperty(Artist), - trackData: Thing.genWikiDataProperty(Track), - - // Expose only - - // Previously known as: (album).artists - artistContribs: { - flags: {expose: true}, - expose: Thing.genContribsExpose('artistContribsByRef') - }, - - tracks: { - flags: {expose: true}, - - expose: { - dependencies: ['trackGroups', 'trackData'], - compute: ({ trackGroups, trackData }) => ( - (trackGroups && trackData - ? (trackGroups - .flatMap(group => group.tracksByRef ?? []) - .map(ref => find.track(ref, {wikiData: {trackData}})) - .filter(Boolean)) - : []) - ) - } - }, - }; -} diff --git a/src/thing/art-tag.js b/src/thing/art-tag.js deleted file mode 100644 index 4b09d885..00000000 --- a/src/thing/art-tag.js +++ /dev/null @@ -1,37 +0,0 @@ -import Thing from './thing.js'; - -import { - isBoolean, - isColor, - isDirectory, - isName, -} from './validators.js'; - -export default class ArtTag extends Thing { - static [Thing.referenceType] = 'tag'; - - static propertyDescriptors = { - // Update & expose - - name: { - flags: {update: true, expose: true}, - update: {validate: isName} - }, - - directory: { - flags: {update: true, expose: true}, - update: {validate: isDirectory}, - expose: Thing.directoryExpose - }, - - color: { - flags: {update: true, expose: true}, - update: {validate: isColor} - }, - - isContentWarning: { - flags: {update: true, expose: true}, - update: {validate: isBoolean, default: false} - }, - }; -} diff --git a/src/thing/artist.js b/src/thing/artist.js deleted file mode 100644 index bbb2a935..00000000 --- a/src/thing/artist.js +++ /dev/null @@ -1,48 +0,0 @@ -import Thing from './thing.js'; - -import { - isDirectory, - isName, - isString, - isURL, - validateArrayItems, - validateReferenceList, -} from './validators.js'; - -export default class Artist extends Thing { - static [Thing.referenceType] = 'artist'; - - static propertyDescriptors = { - // Update & expose - - name: { - flags: {update: true, expose: true}, - - update: { - default: 'Unnamed Artist', - validate: isName - } - }, - - directory: { - flags: {update: true, expose: true}, - update: {validate: isDirectory}, - expose: Thing.directoryExpose - }, - - urls: { - flags: {update: true, expose: true}, - update: {validate: validateArrayItems(isURL)} - }, - - aliasRefs: { - flags: {update: true, expose: true}, - update: {validate: validateReferenceList('artist')} - }, - - contextNotes: { - flags: {update: true, expose: true}, - update: {validate: isString} - }, - }; -} diff --git a/src/thing/flash.js b/src/thing/flash.js deleted file mode 100644 index 4eac65ad..00000000 --- a/src/thing/flash.js +++ /dev/null @@ -1,129 +0,0 @@ -import Thing from './thing.js'; - -import { - isColor, - isContributionList, - isDate, - isDirectory, - isFileExtension, - isName, - isNumber, - isString, - isURL, - oneOf, - validateArrayItems, - validateReferenceList, -} from './validators.js'; - -export default class Flash extends Thing { - static [Thing.referenceType] = 'flash'; - - static propertyDescriptors = { - // Update & expose - - name: { - flags: {update: true, expose: true}, - - update: { - default: 'Unnamed Flash', - validate: isName - } - }, - - directory: { - flags: {update: true, expose: true}, - update: {validate: isDirectory}, - - // Flashes expose directory differently from other Things! Their - // default directory is dependent on the page number (or ID), not - // the name. - expose: { - dependencies: ['page'], - transform(directory, { page }) { - if (directory === null && page === null) - return null; - else if (directory === null) - return page; - else - return directory; - } - } - }, - - page: { - flags: {update: true, expose: true}, - update: {validate: oneOf(isString, isNumber)}, - - expose: { - transform: value => value.toString() - } - }, - - date: { - flags: {update: true, expose: true}, - update: {validate: isDate} - }, - - coverArtFileExtension: { - flags: {update: true, expose: true}, - update: {validate: isFileExtension} - }, - - featuredTracksByRef: { - flags: {update: true, expose: true}, - update: {validate: validateReferenceList('track')} - }, - - contributorContribsByRef: { - flags: {update: true, expose: true}, - update: {validate: isContributionList} - }, - - urls: { - flags: {update: true, expose: true}, - update: {validate: validateArrayItems(isURL)} - }, - }; -} - -export class FlashAct extends Thing { - static [Thing.referenceType] = 'flash-act'; - - static propertyDescriptors = { - // Update & expose - - name: { - flags: {update: true, expose: true}, - - update: { - default: 'Unnamed Flash Act', - validate: isName - } - }, - - color: { - flags: {update: true, expose: true}, - update: {validate: isColor} - }, - - anchor: { - flags: {update: true, expose: true}, - update: {validate: isString} - }, - - jump: { - flags: {update: true, expose: true}, - update: {validate: isString} - }, - - jumpColor: { - flags: {update: true, expose: true}, - update: {validate: isColor} - }, - - flashesByRef: { - flags: {update: true, expose: true}, - update: {validate: validateReferenceList('flash')} - }, - }; -} diff --git a/src/thing/group.js b/src/thing/group.js deleted file mode 100644 index 3b92e957..00000000 --- a/src/thing/group.js +++ /dev/null @@ -1,73 +0,0 @@ -import CacheableObject from './cacheable-object.js'; -import Thing from './thing.js'; - -import { - isColor, - isDirectory, - isName, - isString, - isURL, - validateArrayItems, - validateReferenceList, -} from './validators.js'; - -export class GroupCategory extends CacheableObject { - static propertyDescriptors = { - // Update & expose - - name: { - flags: {update: true, expose: true}, - update: {default: 'Unnamed Group Category', validate: isName} - }, - - color: { - flags: {update: true, expose: true}, - update: {validate: isColor} - }, - - groupsByRef: { - flags: {update: true, expose: true}, - update: {validate: validateReferenceList('group')} - }, - }; -} - -export default class Group extends Thing { - static [Thing.referenceType] = 'group'; - - static propertyDescriptors = { - // Update & expose - - name: { - flags: {update: true, expose: true}, - update: {default: 'Unnamed Group', validate: isName} - }, - - directory: { - flags: {update: true, expose: true}, - update: {validate: isDirectory}, - expose: Thing.directoryExpose - }, - - description: { - flags: {update: true, expose: true}, - update: {validate: isString} - }, - - urls: { - flags: {update: true, expose: true}, - update: {validate: validateArrayItems(isURL)} - }, - - // Expose only - - descriptionShort: { - flags: {expose: true}, - - expose: { - dependencies: ['description'], - compute: ({ description }) => description.split('<hr class="split">')[0] - } - } - }; -} diff --git a/src/thing/homepage-layout.js b/src/thing/homepage-layout.js deleted file mode 100644 index 47173917..00000000 --- a/src/thing/homepage-layout.js +++ /dev/null @@ -1,99 +0,0 @@ -import CacheableObject from './cacheable-object.js'; - -import { - isColor, - isCountingNumber, - isName, - isString, - oneOf, - validateArrayItems, - validateInstanceOf, - validateReference, - validateReferenceList, -} from './validators.js'; - -export class HomepageLayoutRow extends CacheableObject { - static propertyDescriptors = { - // Update & expose - - name: { - flags: {update: true, expose: true}, - update: {validate: isName} - }, - - type: { - flags: {update: true, expose: true}, - - update: { - validate(value) { - throw new Error(`'type' property validator must be overridden`); - } - } - }, - - color: { - flags: {update: true, expose: true}, - update: {validate: isColor} - }, - }; -} - -export class HomepageLayoutAlbumsRow extends HomepageLayoutRow { - static propertyDescriptors = { - ...HomepageLayoutRow.propertyDescriptors, - - // Update & expose - - type: { - flags: {update: true, expose: true}, - update: { - validate(value) { - if (value !== 'albums') { - throw new TypeError(`Expected 'albums'`); - } - - return true; - } - } - }, - - sourceGroupByRef: { - flags: {update: true, expose: true}, - update: {validate: validateReference('group')} - }, - - sourceAlbumsByRef: { - flags: {update: true, expose: true}, - update: {validate: validateReferenceList('album')} - }, - - countAlbumsFromGroup: { - flags: {update: true, expose: true}, - update: {validate: isCountingNumber} - }, - - actionLinks: { - flags: {update: true, expose: true}, - update: {validate: validateArrayItems(isString)} - }, - } -} - -export default class HomepageLayout extends CacheableObject { - static propertyDescriptors = { - // Update & expose - - sidebarContent: { - flags: {update: true, expose: true}, - update: {validate: isString} - }, - - rows: { - flags: {update: true, expose: true}, - - update: { - validate: validateArrayItems(validateInstanceOf(HomepageLayoutRow)) - } - }, - }; -} diff --git a/src/thing/news-entry.js b/src/thing/news-entry.js deleted file mode 100644 index 2db2f37c..00000000 --- a/src/thing/news-entry.js +++ /dev/null @@ -1,49 +0,0 @@ -import Thing from './thing.js'; - -import { - isDate, - isDirectory, - isName, -} from './validators.js'; - -export default class NewsEntry extends Thing { - static [Thing.referenceType] = 'news-entry'; - - static propertyDescriptors = { - // Update & expose - - name: { - flags: {update: true, expose: true}, - update: {validate: isName} - }, - - directory: { - flags: {update: true, expose: true}, - update: {validate: isDirectory}, - expose: Thing.directoryExpose - }, - - date: { - flags: {update: true, expose: true}, - update: {validate: isDate} - }, - - content: { - flags: {update: true, expose: true}, - }, - - // Expose only - - contentShort: { - flags: {expose: true}, - - expose: { - dependencies: ['content'], - - compute({ content }) { - return body.split('<hr class="split">')[0]; - } - } - }, - }; -} diff --git a/src/thing/static-page.js b/src/thing/static-page.js deleted file mode 100644 index e2b51507..00000000 --- a/src/thing/static-page.js +++ /dev/null @@ -1,52 +0,0 @@ -import Thing from './thing.js'; - -import { - isBoolean, - isDirectory, - isName, - isString, -} from './validators.js'; - -export default class StaticPage extends Thing { - static [Thing.referenceType] = 'static'; - - static propertyDescriptors = { - // Update & expose - - name: { - flags: {update: true, expose: true}, - update: {validate: isName, default: 'Unnamed Static Page'} - }, - - nameShort: { - flags: {update: true, expose: true}, - update: {validate: isName}, - - expose: { - dependencies: ['name'], - transform: (value, { name }) => value ?? name - } - }, - - directory: { - flags: {update: true, expose: true}, - update: {validate: isDirectory}, - expose: Thing.directoryExpose - }, - - content: { - flags: {update: true, expose: true}, - update: {validate: isString} - }, - - stylesheet: { - flags: {update: true, expose: true}, - update: {validate: isString} - }, - - showInNavigationBar: { - flags: {update: true, expose: true}, - update: {validate: isBoolean, default: true} - }, - }; -} diff --git a/src/thing/structures.js b/src/thing/structures.js deleted file mode 100644 index 364ba149..00000000 --- a/src/thing/structures.js +++ /dev/null @@ -1 +0,0 @@ -// Generic structure utilities common across various Thing types. diff --git a/src/thing/thing.js b/src/thing/thing.js deleted file mode 100644 index 2d6def62..00000000 --- a/src/thing/thing.js +++ /dev/null @@ -1,62 +0,0 @@ -// Base class for Things. No, we will not come up with a better name. -// Sorry not sorry! :) - -import CacheableObject from './cacheable-object.js'; - -import { - validateArrayItems, -} from './validators.js'; - -import { getKebabCase } from '../util/wiki-data.js'; -import find from '../util/find.js'; - -export default class Thing extends CacheableObject { - static referenceType = Symbol('Thing.referenceType'); - - static directoryExpose = { - dependencies: ['name'], - transform(directory, { name }) { - if (directory === null && name === null) - return null; - else if (directory === null) - return getKebabCase(name); - else - return directory; - } - }; - - static genContribsExpose(contribsByRefProperty) { - return { - dependencies: ['artistData', contribsByRefProperty], - compute: ({ artistData, [contribsByRefProperty]: contribsByRef }) => ( - (contribsByRef && artistData - ? (contribsByRef - .map(({ who: ref, what }) => ({ - who: find.artist(ref, {wikiData: {artistData}}), - what - })) - .filter(({ who }) => who)) - : []) - ) - }; - } - - static genWikiDataProperty(thingClass) { - return { - flags: {update: true}, - update: { - validate: validateArrayItems(x => x instanceof thingClass) - } - }; - } - - static getReference(thing) { - if (!thing.constructor[Thing.referenceType]) - throw TypeError(`Passed Thing is ${thing.constructor.name}, which provides no [Thing.referenceType]`); - - if (!thing.directory) - throw TypeError(`Passed ${thing.constructor.name} is missing its directory`); - - return `${thing.constructor[Thing.referenceType]}:${thing.directory}`; - } -} diff --git a/src/thing/track.js b/src/thing/track.js deleted file mode 100644 index 3edabc92..00000000 --- a/src/thing/track.js +++ /dev/null @@ -1,173 +0,0 @@ -import Thing from './thing.js'; - -import { - isBoolean, - isColor, - isCommentary, - isContributionList, - isDate, - isDirectory, - isDuration, - isName, - isURL, - isString, - validateArrayItems, - validateReference, - validateReferenceList, -} from './validators.js'; - -import Album from './album.js'; -import Artist from './artist.js'; -import ArtTag from './art-tag.js'; - -import find from '../util/find.js'; - -export default class Track extends Thing { - static [Thing.referenceType] = 'track'; - - static propertyDescriptors = { - // Update & expose - - name: { - flags: {update: true, expose: true}, - - update: { - default: 'Unnamed Track', - validate: isName - } - }, - - directory: { - flags: {update: true, expose: true}, - update: {validate: isDirectory}, - expose: Thing.directoryExpose - }, - - duration: { - flags: {update: true, expose: true}, - update: {validate: isDuration} - }, - - urls: { - flags: {update: true, expose: true}, - - update: { - validate: validateArrayItems(isURL) - } - }, - - dateFirstReleased: { - flags: {update: true, expose: true}, - update: {validate: isDate} - }, - - coverArtDate: { - flags: {update: true, expose: true}, - update: {validate: isDate} - }, - - hasCoverArt: { - flags: {update: true, expose: true}, - update: {default: true, validate: isBoolean} - }, - - hasURLs: { - flags: {update: true, expose: true}, - update: {default: true, validate: isBoolean} - }, - - referencedTracksByRef: { - flags: {update: true, expose: true}, - update: {validate: validateReferenceList('track')} - }, - - artistContribsByRef: { - flags: {update: true, expose: true}, - update: {validate: isContributionList} - }, - - contributorContribsByRef: { - flags: {update: true, expose: true}, - update: {validate: isContributionList} - }, - - coverArtistContribsByRef: { - flags: {update: true, expose: true}, - update: {validate: isContributionList} - }, - - artTagsByRef: { - flags: {update: true, expose: true}, - update: {validate: validateReferenceList('tag')} - }, - - // Previously known as: (track).aka - originalReleaseTrackByRef: { - flags: {update: true, expose: true}, - update: {validate: validateReference('track')} - }, - - commentary: { - flags: {update: true, expose: true}, - update: {validate: isCommentary} - }, - - lyrics: { - flags: {update: true, expose: true}, - update: {validate: isString} - }, - - // Update only - - albumData: Thing.genWikiDataProperty(Album), - artistData: Thing.genWikiDataProperty(Artist), - artTagData: Thing.genWikiDataProperty(ArtTag), - - // Expose only - - album: { - flags: {expose: true}, - - expose: { - dependencies: ['albumData'], - compute: ({ [this.instance]: track, albumData }) => ( - albumData?.find(album => album.tracks.includes(track)) ?? null) - } - }, - - date: { - flags: {expose: true}, - - expose: { - dependencies: ['albumData', 'dateFirstReleased'], - compute: ({ albumData, dateFirstReleased, [this.instance]: track }) => ( - dateFirstReleased ?? - albumData?.find(album => album.tracks.includes(track))?.date ?? - null - ) - } - }, - - // Previously known as: (track).artists - artistContribs: { - flags: {expose: true}, - expose: Thing.genContribsExpose('artistContribsByRef') - }, - - artTags: { - flags: {expose: true}, - - expose: { - dependencies: ['artTagsByRef', 'artTagData'], - - compute: ({ artTagsByRef, artTagData }) => ( - (artTagsByRef && artTagData - ? (artTagsByRef - .map(ref => find.tag(ref, {wikiData: {tagData: artTagData}})) - .filter(Boolean)) - : []) - ) - } - } - }; -} diff --git a/src/thing/wiki-info.js b/src/thing/wiki-info.js deleted file mode 100644 index b805bf76..00000000 --- a/src/thing/wiki-info.js +++ /dev/null @@ -1,90 +0,0 @@ -import CacheableObject from './cacheable-object.js'; - -import { - isBoolean, - isColor, - isLanguageCode, - isName, - isString, - isURL, -} from './validators.js'; - -export default class WikiInfo extends CacheableObject { - static propertyDescriptors = { - // Update & expose - - name: { - flags: {update: true, expose: true}, - update: {validate: isName, default: 'Unnamed Wiki'} - }, - - // Displayed in nav bar. - shortName: { - flags: {update: true, expose: true}, - update: {validate: isName}, - - expose: { - dependencies: ['name'], - transform: (value, { name }) => value ?? name - } - }, - - color: { - flags: {update: true, expose: true}, - update: {validate: isColor} - }, - - // One-line description used for <meta rel="description"> tag. - description: { - flags: {update: true, expose: true}, - update: {validate: isString} - }, - - footerContent: { - flags: {update: true, expose: true}, - update: {validate: isString} - }, - - defaultLanguage: { - flags: {update: true, expose: true}, - update: {validate: isLanguageCode} - }, - - canonicalBase: { - flags: {update: true, expose: true}, - update: {validate: isURL} - }, - - // Feature toggles - - enableArtistAvatars: { - flags: {update: true, expose: true}, - update: {validate: isBoolean, default: false} - }, - - enableFlashesAndGames: { - flags: {update: true, expose: true}, - update: {validate: isBoolean, default: false} - }, - - enableListings: { - flags: {update: true, expose: true}, - update: {validate: isBoolean, default: false} - }, - - enableNews: { - flags: {update: true, expose: true}, - update: {validate: isBoolean, default: false} - }, - - enableArtTagUI: { - flags: {update: true, expose: true}, - update: {validate: isBoolean, default: false} - }, - - enableGroupUI: { - flags: {update: true, expose: true}, - update: {validate: isBoolean, default: false} - }, - }; -} diff --git a/src/upd8.js b/src/upd8.js index 2aa4eb29..de79a0fb 100755 --- a/src/upd8.js +++ b/src/upd8.js @@ -91,20 +91,26 @@ import find from './util/find.js'; import * as html from './util/html.js'; import unbound_link, {getLinkThemeString} from './util/link.js'; -import Album, { TrackGroup } from './thing/album.js'; -import Artist from './thing/artist.js'; -import ArtTag from './thing/art-tag.js'; -import CacheableObject from './thing/cacheable-object.js'; -import Flash, { FlashAct } from './thing/flash.js'; -import Group, { GroupCategory } from './thing/group.js'; -import HomepageLayout, { +import CacheableObject from './data/cacheable-object.js'; + +import { + Album, + Artist, + ArtTag, + Flash, + FlashAct, + Group, + GroupCategory, + HomepageLayout, HomepageLayoutAlbumsRow, -} from './thing/homepage-layout.js'; -import NewsEntry from './thing/news-entry.js'; -import StaticPage from './thing/static-page.js'; -import Thing from './thing/thing.js'; -import Track from './thing/track.js'; -import WikiInfo from './thing/wiki-info.js'; + HomepageLayoutRow, + NewsEntry, + StaticPage, + Thing, + Track, + TrackGroup, + WikiInfo, +} from './data/things.js'; import { fancifyFlashURL, @@ -2612,7 +2618,7 @@ async function main() { call(processAggregate.close); - dataStep.save(processResults); + call(dataStep.save, processResults); return; } @@ -2696,7 +2702,7 @@ async function main() { }); } - dataStep.save(processResults); + call(dataStep.save, processResults); }); } diff --git a/src/util/sugar.js b/src/util/sugar.js index 219c3eec..d6bc3df6 100644 --- a/src/util/sugar.js +++ b/src/util/sugar.js @@ -356,7 +356,8 @@ export function showAggregate(topError, {pathToFile = p => p} = {}) { const stackLine = stackLines?.find(line => line.trim().startsWith('at') && !line.includes('sugar') - && !line.includes('node:internal')); + && !line.includes('node:internal') + && !line.includes('<anonymous>')); const tracePart = (stackLine ? '- ' + stackLine.trim().replace(/file:\/\/(.*\.js)/, (match, pathname) => pathToFile(pathname)) : '(no stack trace)'); diff --git a/test/cacheable-object.js b/test/cacheable-object.js index 203d2af0..dd93343f 100644 --- a/test/cacheable-object.js +++ b/test/cacheable-object.js @@ -1,6 +1,6 @@ import test from 'tape'; -import CacheableObject from '../src/thing/cacheable-object.js'; +import CacheableObject from '../src/data/cacheable-object.js'; // Utility diff --git a/test/data-validators.js b/test/data-validators.js index 739333a3..a7b9b48d 100644 --- a/test/data-validators.js +++ b/test/data-validators.js @@ -24,7 +24,7 @@ import { // Compositional utilities oneOf, -} from '../src/thing/validators.js'; +} from '../src/data/validators.js'; function test(msg, fn) { _test(msg, t => { |