/* eslint-env node */ const fs = require('node:fs/promises'); const path = require('node:path'); const defaultResourcePaths = { audio: path.resolve(__dirname, '..', 'src', 'data', 'toolkit', 'audio.json'), credits: path.resolve(__dirname, '..', 'src', 'data', 'toolkit', 'credits.json'), endings: path.resolve(__dirname, '..', 'src', 'data', 'toolkit', 'endings.json'), factions: path.resolve(__dirname, '..', 'src', 'data', 'toolkit', 'factions.json'), flags: path.resolve(__dirname, '..', 'src', 'data', 'toolkit', 'flags.json'), images: path.resolve(__dirname, '..', 'src', 'data', 'toolkit', 'images.json'), lore: path.resolve(__dirname, '..', 'src', 'data', 'toolkit', 'lore.json'), map: path.resolve(__dirname, '..', 'src', 'data', 'toolkit', 'map.json'), opening: path.resolve(__dirname, '..', 'src', 'data', 'toolkit', 'opening.json'), petitioners: path.resolve( __dirname, '..', 'src', 'data', 'toolkit', 'petitioners.json', ), references: path.resolve( __dirname, '..', 'src', 'data', 'toolkit', 'references.json', ), resources: path.resolve( __dirname, '..', 'src', 'data', 'toolkit', 'resources.json', ), rulings: path.resolve(__dirname, '..', 'src', 'data', 'toolkit', 'rulings.json'), seed: path.resolve(__dirname, '..', 'src', 'data', 'toolkit', 'seed.json'), story: path.resolve(__dirname, '..', 'src', 'data', 'toolkit', 'story.json'), }; const singletonObjectResources = ['map', 'story']; const singletonResources = ['credits', ...singletonObjectResources]; const recordArrayResources = [ 'audio', 'endings', 'factions', 'flags', 'images', 'lore', 'opening', 'petitioners', 'references', 'resources', 'rulings', 'seed', ]; function isRecord(value) { return value != null && typeof value === 'object' && !Array.isArray(value); } function isOptionalString(value) { return value == null || typeof value === 'string'; } function isCreditsLogo(value) { return ( isRecord(value) && typeof value.title === 'string' && typeof value.subtitle === 'string' && isOptionalString(value.image) ); } function isCreditsContributor(value) { return ( isRecord(value) && typeof value.name === 'string' && typeof value.role === 'string' && isOptionalString(value.credit) && isOptionalString(value.url) ); } function isCreditsTechnology(value) { return ( isRecord(value) && typeof value.name === 'string' && isOptionalString(value.role) && isOptionalString(value.url) ); } function isCreditsContent(value) { return ( isRecord(value) && isCreditsLogo(value.mainLogo) && Array.isArray(value.contributors) && value.contributors.every(isCreditsContributor) && Array.isArray(value.technologies) && value.technologies.every(isCreditsTechnology) && Array.isArray(value.specialThanks) && value.specialThanks.every(item => typeof item === 'string') ); } function isToolkitRecord(value) { return isRecord(value) && typeof value.id === 'string'; } function isToolkitRecordArray(value) { return Array.isArray(value) && value.every(isToolkitRecord); } function getResourcePath(resourcePaths, resource) { const resourcePath = resourcePaths[resource]; if (resourcePath == null) { throw new Error(`Unsupported toolkit content resource: ${String(resource)}`); } return resourcePath; } function validateResourceContent(resource, content) { if (resource === 'credits' && isCreditsContent(content)) { return content; } if (recordArrayResources.includes(resource) && isToolkitRecordArray(content)) { return content; } if (singletonObjectResources.includes(resource) && isRecord(content)) { return content; } throw new Error(`Invalid toolkit content for resource: ${resource}`); } function validateRecordResource(resourcePaths, resource) { if (singletonResources.includes(resource)) { throw new Error( `${resource} is a singleton resource. Save the full JSON content.`, ); } getResourcePath(resourcePaths, resource); } function createJsonContentStore(resourcePaths = defaultResourcePaths) { async function readJsonResource(resource) { const resourcePath = getResourcePath(resourcePaths, resource); const source = await fs.readFile(resourcePath, 'utf8'); const content = validateResourceContent(resource, JSON.parse(source)); const stats = await fs.stat(resourcePath); return { content, resource, revision: String(Math.round(stats.mtimeMs)), }; } async function writeJsonResource(resource, content) { const resourcePath = getResourcePath(resourcePaths, resource); const validatedContent = validateResourceContent(resource, content); const temporaryPath = `${resourcePath}.${process.pid}.${Date.now()}.tmp`; const formattedContent = `${JSON.stringify(validatedContent, null, 2)}\n`; await fs.writeFile(temporaryPath, formattedContent, 'utf8'); await fs.rename(temporaryPath, resourcePath); return readJsonResource(resource); } async function createJsonRecord(resource, record) { validateRecordResource(resourcePaths, resource); if (!isToolkitRecord(record)) { throw new Error('Toolkit content record requires a string id.'); } const { content } = await readJsonResource(resource); if (content.some(existingRecord => existingRecord.id === record.id)) { throw new Error(`Toolkit content record already exists: ${record.id}`); } return writeJsonResource(resource, [...content, record]); } async function updateJsonRecord(resource, record) { validateRecordResource(resourcePaths, resource); if (!isToolkitRecord(record)) { throw new Error('Toolkit content record requires a string id.'); } const { content } = await readJsonResource(resource); const existingIndex = content.findIndex( existingRecord => existingRecord.id === record.id, ); if (existingIndex < 0) { throw new Error(`Toolkit content record was not found: ${record.id}`); } const nextContent = [...content]; nextContent[existingIndex] = record; return writeJsonResource(resource, nextContent); } async function deleteJsonRecord(resource, id) { validateRecordResource(resourcePaths, resource); if (typeof id !== 'string' || id.length === 0) { throw new Error('Toolkit content delete requires a record id.'); } const { content } = await readJsonResource(resource); const nextContent = content.filter(record => record.id !== id); if (nextContent.length === content.length) { throw new Error(`Toolkit content record was not found: ${id}`); } return writeJsonResource(resource, nextContent); } return { createJsonRecord, deleteJsonRecord, readJsonResource, resourcePaths, updateJsonRecord, writeJsonResource, }; } const defaultStore = createJsonContentStore(); module.exports = { createJsonContentStore, createJsonRecord: defaultStore.createJsonRecord, deleteJsonRecord: defaultStore.deleteJsonRecord, defaultResourcePaths, isCreditsContent, isToolkitRecord, isToolkitRecordArray, readJsonResource: defaultStore.readJsonResource, updateJsonRecord: defaultStore.updateJsonRecord, writeJsonResource: defaultStore.writeJsonResource, };