diff --git a/app/common/jsonpath.mjs b/app/common/jsonpath.mjs new file mode 100644 index 0000000..1d6a3ed --- /dev/null +++ b/app/common/jsonpath.mjs @@ -0,0 +1,26 @@ +import { JSONPath } from 'jsonpath-plus'; + +function query(data, expression) { + return JSONPath({ + json: data, + path: expression, + eval: false, + }); +} + +function apply(data, expression, transform) { + const matches = JSONPath({ + json: data, + path: expression, + resultType: 'all', + eval: false, + }); + + for (const { parent, parentProperty, value } of matches) { + parent[parentProperty] = transform(value); + } + + return data; +} + +export default { apply, query }; diff --git a/app/data/LocalizationProcessor.mjs b/app/data/LocalizationProcessor.mjs index 91fedbe..e133179 100644 --- a/app/data/LocalizationProcessor.mjs +++ b/app/data/LocalizationProcessor.mjs @@ -1,7 +1,7 @@ import fs from 'fs/promises'; import path from 'path'; import { mkdirp } from '../common/fs.mjs'; -import jsonpath from 'jsonpath'; +import jsonpath from '../common/jsonpath.mjs'; import get from 'lodash/get.js'; import set from 'lodash/set.js'; import pLimit from 'p-limit'; diff --git a/app/data/LocalizationProcessor.test.mjs b/app/data/LocalizationProcessor.test.mjs index 0236166..59dee3f 100644 --- a/app/data/LocalizationProcessor.test.mjs +++ b/app/data/LocalizationProcessor.test.mjs @@ -83,6 +83,24 @@ describe('LocalizationProcessor', () => { }); describe('dataIterations', () => { + it('finds matching nodes at any depth', () => { + const processor = makeProcessor([ + { key: 'stages', nodes: '$..stages.*', id: 'id', values: 'name' }, + ]); + + const data = { + schedules: [ + { stages: [{ id: 'stage-1', name: 'Scorch Gorge' }] }, + { nested: { stages: [{ id: 'stage-2', name: 'Eeltail Alley' }] } }, + ], + }; + + expect([...processor.dataIterations(data)].map(({ id, value }) => ({ id, value }))).toEqual([ + { id: 'stage-1', value: 'Scorch Gorge' }, + { id: 'stage-2', value: 'Eeltail Alley' }, + ]); + }); + it('yields entries with correct id, value, and path', () => { const processor = makeProcessor([ { key: 'stages', nodes: '$.stages[*]', id: 'id', values: 'name' }, diff --git a/app/data/updaters/DataUpdater.mjs b/app/data/updaters/DataUpdater.mjs index 9afb683..f64bb55 100644 --- a/app/data/updaters/DataUpdater.mjs +++ b/app/data/updaters/DataUpdater.mjs @@ -10,7 +10,7 @@ import ImageProcessor from '../ImageProcessor.mjs'; import NsoClient from '../../splatnet/NsoClient.mjs'; import { locales, regionalLocales, defaultLocale } from '../../../src/common/i18n.mjs'; import { LocalizationProcessor } from '../LocalizationProcessor.mjs'; -import jsonpath from 'jsonpath'; +import jsonpath from '../../common/jsonpath.mjs'; import { deriveId, getDateParts, getTopOfCurrentHour } from '../../common/util.mjs'; export default class DataUpdater { diff --git a/app/data/updaters/DataUpdater.test.mjs b/app/data/updaters/DataUpdater.test.mjs new file mode 100644 index 0000000..604c13a --- /dev/null +++ b/app/data/updaters/DataUpdater.test.mjs @@ -0,0 +1,42 @@ +import { describe, expect, it } from 'vitest'; +import DataUpdater from './DataUpdater.mjs'; + +describe('DataUpdater', () => { + describe('deriveIds', () => { + it('replaces every recursively matched node while preserving its data', () => { + const updater = Object.create(DataUpdater.prototype); + updater.derivedIds = ['$..weapons.*']; + const splattershot = { + name: 'Splattershot', + image: { url: 'https://example.com/assets/weapon-a.png' }, + }; + const splatRoller = { + name: 'Splat Roller', + image: { url: 'https://example.com/assets/weapon-b.png' }, + }; + const scheduleWeapons = [splattershot]; + const nestedWeapons = [splatRoller]; + const data = { + schedules: [ + { + weapons: scheduleWeapons, + }, + { + nested: { + weapons: nestedWeapons, + }, + }, + ], + }; + + updater.deriveIds(data); + + const weapons = [...scheduleWeapons, ...nestedWeapons]; + expect(weapons.map(({ name }) => name)).toEqual(['Splattershot', 'Splat Roller']); + expect(weapons.map(({ __splatoon3ink_id: id }) => id)).toEqual([ + '1bfe00916f1f6b9a', + '027798dcfb87596b', + ]); + }); + }); +}); diff --git a/app/data/updaters/FestivalUpdater.mjs b/app/data/updaters/FestivalUpdater.mjs index 8d22ecf..622cde0 100644 --- a/app/data/updaters/FestivalUpdater.mjs +++ b/app/data/updaters/FestivalUpdater.mjs @@ -1,6 +1,6 @@ import fs from 'fs/promises'; import pLimit from 'p-limit'; -import jsonpath from 'jsonpath'; +import jsonpath from '../../common/jsonpath.mjs'; import { getFestId } from '../../common/util.mjs'; import ValueCache from '../../common/ValueCache.mjs'; import { regionTokens } from '../../splatnet/NsoClient.mjs'; diff --git a/package-lock.json b/package-lock.json index 0386c8a..ff5bdb0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,7 @@ "cron": "^4.4.0", "dotenv": "^17.3.1", "ical-generator": "^11.1.1", - "jsonpath": "^1.2.1", + "jsonpath-plus": "^10.4.0", "lodash": "^4.17.21", "masto": "^8.0.0", "nxapi": "^1.6.1-next.248", @@ -3136,6 +3136,30 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@jsep-plugin/assignment": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@jsep-plugin/assignment/-/assignment-1.3.0.tgz", + "integrity": "sha512-VVgV+CXrhbMI3aSusQyclHkenWSAm95WaiKrMxRFam3JSUiIaQjoMIw2sEs/OX4XifnqeQUN4DYbJjlA8EfktQ==", + "license": "MIT", + "engines": { + "node": ">= 10.16.0" + }, + "peerDependencies": { + "jsep": "^0.4.0||^1.0.0" + } + }, + "node_modules/@jsep-plugin/regex": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@jsep-plugin/regex/-/regex-1.0.4.tgz", + "integrity": "sha512-q7qL4Mgjs1vByCaTnDFcBnV9HS7GVPJX5vyVoCgZHNSC9rjwIlmbXG5sUuorR5ndfHAIlJ8pVStxvjXHbNvtUg==", + "license": "MIT", + "engines": { + "node": ">= 10.16.0" + }, + "peerDependencies": { + "jsep": "^0.4.0||^1.0.0" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -5800,6 +5824,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "dev": true, "license": "BSD-2-Clause", "dependencies": { "esprima": "^4.0.1", @@ -6043,6 +6068,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, "license": "BSD-2-Clause", "bin": { "esparse": "bin/esparse.js", @@ -6082,6 +6108,7 @@ "version": "5.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=4.0" @@ -6097,6 +6124,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=0.10.0" @@ -6994,6 +7022,15 @@ "node": ">=20.0.0" } }, + "node_modules/jsep": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/jsep/-/jsep-1.4.0.tgz", + "integrity": "sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw==", + "license": "MIT", + "engines": { + "node": ">= 10.16.0" + } + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -7091,27 +7128,22 @@ "node": ">=10" } }, - "node_modules/jsonpath": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/jsonpath/-/jsonpath-1.3.0.tgz", - "integrity": "sha512-0kjkYHJBkAy50Z5QzArZ7udmvxrJzkpKYW27fiF//BrMY7TQibYLl+FYIXN2BiYmwMIVzSfD8aDRj6IzgBX2/w==", + "node_modules/jsonpath-plus": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-10.4.0.tgz", + "integrity": "sha512-T92WWatJXmhBbKsgH/0hl+jxjdXrifi5IKeMY02DWggRxX0UElcbVzPlmgLTbvsPeW1PasQ6xE2Q75stkhGbsA==", "license": "MIT", "dependencies": { - "esprima": "1.2.5", - "static-eval": "2.1.1", - "underscore": "1.13.6" - } - }, - "node_modules/jsonpath/node_modules/esprima": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-1.2.5.tgz", - "integrity": "sha512-S9VbPDU0adFErpDai3qDkjq8+G05ONtKzcyNrPKg/ZKa+tf879nX2KexNU95b31UoTJjRLInNBHHHjFPoCd7lQ==", + "@jsep-plugin/assignment": "^1.3.0", + "@jsep-plugin/regex": "^1.0.4", + "jsep": "^1.4.0" + }, "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" + "jsonpath": "bin/jsonpath-cli.js", + "jsonpath-plus": "bin/jsonpath-cli.js" }, "engines": { - "node": ">=0.4.0" + "node": ">=18.0.0" } }, "node_modules/keyv": { @@ -8995,6 +9027,7 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, "license": "BSD-3-Clause", "optional": true, "engines": { @@ -9047,15 +9080,6 @@ "dev": true, "license": "MIT" }, - "node_modules/static-eval": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/static-eval/-/static-eval-2.1.1.tgz", - "integrity": "sha512-MgWpQ/ZjGieSVB3eOJVs4OA2LT/q1vx98KPCTTQPzq/aLr0YUXTsgryTXr4SLfR0ZfUUCiedM9n/ABeDIyy4mA==", - "license": "MIT", - "dependencies": { - "escodegen": "^2.1.0" - } - }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", @@ -9406,12 +9430,6 @@ "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==", "license": "MIT" }, - "node_modules/underscore": { - "version": "1.13.6", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz", - "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==", - "license": "MIT" - }, "node_modules/undici-types": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", @@ -9526,13 +9544,16 @@ } }, "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.1.tgz", + "integrity": "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], "license": "MIT", "bin": { - "uuid": "dist/bin/uuid" + "uuid": "dist/esm/bin/uuid" } }, "node_modules/vary": { diff --git a/package.json b/package.json index 4982d9c..49eb1b2 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "cron": "^4.4.0", "dotenv": "^17.3.1", "ical-generator": "^11.1.1", - "jsonpath": "^1.2.1", + "jsonpath-plus": "^10.4.0", "lodash": "^4.17.21", "masto": "^8.0.0", "nxapi": "^1.6.1-next.248", @@ -77,5 +77,13 @@ "vite": "^8.2.2", "vitest": "^4.1.11", "vue-eslint-parser": "^10.4.0" + }, + "overrides": { + "nxapi": { + "uuid": "^11.1.1", + "node-notifier": { + "uuid": "^11.1.1" + } + } } }