260 lines
7.2 KiB
JavaScript
260 lines
7.2 KiB
JavaScript
/* 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,
|
|
};
|