Files
jacob-mathison 47567589e4
ci/woodpecker/push/woodpecker Pipeline was successful
Lint and Build Checks / Lint, Test, and Build (push) Failing after 368h6m42s
feat(toolkit): add structured media authoring
2026-06-24 11:30:26 -05:00

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,
};