prevent double-reading of registry, allow menu/frame scripts

This commit is contained in:
dragonwocky 2021-10-10 00:00:59 +11:00
parent 579050e349
commit 9b4b21c297
3 changed files with 206 additions and 254 deletions

View File

@ -258,34 +258,6 @@ export const create = async (
} else { } else {
path.push('content'); path.push('content');
} }
console.log(
{
pointer: {
table: parentTable,
id: parentID,
spaceId: spaceID,
},
path,
command: prepend ? 'listBefore' : 'listAfter',
args: {
...(siblingID ? { after: siblingID } : {}),
id: recordID,
},
},
{
pointer: {
table: recordTable,
id: recordID,
spaceId: spaceID,
},
path: [],
command: 'set',
args: {
...args,
...recordValue,
},
}
);
const json = await fs.getJSON('https://www.notion.so/api/v3/saveTransactions', { const json = await fs.getJSON('https://www.notion.so/api/v3/saveTransactions', {
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',

View File

@ -9,216 +9,192 @@
import { fmt, registry } from './_.mjs'; import { fmt, registry } from './_.mjs';
const check = async ( const check = async (
mod, mod,
key, key,
value, value,
types, types,
{ {
extension = '', extension = '',
error = `invalid ${key} (${extension ? `${extension} ` : ''}${types}): ${JSON.stringify( error = `invalid ${key} (${extension ? `${extension} ` : ''}${types}): ${JSON.stringify(
value value
)}`, )}`,
optional = false, optional = false,
} = {} } = {}
) => { ) => {
let test; let test;
for (const type of Array.isArray(types) ? [types] : types.split('|')) { for (const type of Array.isArray(types) ? [types] : types.split('|')) {
if (type === 'file') { if (type === 'file') {
test = test =
value && !value.startsWith('http') value && !value.startsWith('http')
? await fmt.is(`repo/${mod._dir}/${value}`, type, { extension }) ? await fmt.is(`repo/${mod._dir}/${value}`, type, { extension })
: false; : false;
} else test = await fmt.is(value, type, { extension }); } else test = await fmt.is(value, type, { extension });
if (test) break; if (test) break;
} }
if (!test) { if (!test) {
if (optional && (await fmt.is(value, 'undefined'))) return true; if (optional && (await fmt.is(value, 'undefined'))) return true;
if (error) registry._errors.push({ source: mod._dir, message: error }); if (error) mod._err(error);
return false; return false;
} }
return true; return true;
}, };
validateEnvironments = (mod) => {
return check(mod, 'environments', mod.environments, 'array', { optional: true }).then( const validateEnvironments = async (mod) => {
(passed) => { mod.environments = mod.environments ?? registry.supportedEnvs;
if (!passed) return false; const isArray = await check(mod, 'environments', mod.environments, 'array');
if (!mod.environments) { if (!isArray) return false;
mod.environments = registry.supportedEnvs; return mod.environments.map((tag) =>
return true; check(mod, 'environments.env', tag, registry.supportedEnvs)
}
return mod.environments.map((tag) =>
check(mod, 'environments.env', tag, registry.supportedEnvs)
);
}
); );
}, },
validateTags = (mod) => { validateTags = async (mod) => {
return check(mod, 'tags', mod.tags, 'array').then((passed) => { const isArray = await check(mod, 'tags', mod.tags, 'array');
if (!passed) return false; if (!isArray) return false;
const containsCategory = mod.tags.filter((tag) => const categoryTags = ['core', 'extension', 'theme'],
['core', 'extension', 'theme'].includes(tag) containsCategory = mod.tags.filter((tag) => categoryTags.includes(tag)).length;
).length; if (!containsCategory) {
if (!containsCategory) { mod._err(
registry._errors.push({ `invalid tags (must contain at least one of 'core', 'extension', or 'theme'):
source: mod._dir, ${JSON.stringify(mod.tags)}`
message: `invalid tags (must contain at least one of 'core', 'extension', or 'theme'): ${JSON.stringify(
mod.tags
)}`,
});
return false;
}
if (
(mod.tags.includes('theme') &&
!(mod.tags.includes('light') || mod.tags.includes('dark'))) ||
(mod.tags.includes('light') && mod.tags.includes('dark'))
) {
registry._errors.push({
source: mod._dir,
message: `invalid tags (themes must be either 'light' or 'dark', not neither or both): ${JSON.stringify(
mod.tags
)}`,
});
return false;
}
return mod.tags.map((tag) => check(mod, 'tags.tag', tag, 'string'));
});
},
validateAuthors = (mod) => {
return check(mod, 'authors', mod.authors, 'array').then((passed) => {
if (!passed) return false;
return mod.authors.map((author) => [
check(mod, 'authors.author.name', author.name, 'string'),
check(mod, 'authors.author.email', author.email, 'email', { optional: true }),
check(mod, 'authors.author.homepage', author.homepage, 'url'),
check(mod, 'authors.author.avatar', author.avatar, 'url'),
]);
});
},
validateCSS = (mod) => {
return check(mod, 'css', mod.css, 'object').then((passed) => {
if (!passed) return false;
const tests = [];
for (let dest of ['frame', 'client', 'menu']) {
if (!mod.css[dest]) continue;
let test = check(mod, `css.${dest}`, mod.css[dest], 'array');
test = test.then((passed) => {
if (!passed) return false;
return mod.css[dest].map((file) =>
check(mod, `css.${dest}.file`, file, 'file', { extension: '.css' })
);
});
tests.push(test);
}
return tests;
});
},
validateJS = (mod) => {
return check(mod, 'js', mod.js, 'object').then((passed) => {
if (!passed) return false;
const tests = [];
if (mod.js.client) {
let test = check(mod, 'js.client', mod.js.client, 'array');
test = test.then((passed) => {
if (!passed) return false;
return mod.js.client.map((file) =>
check(mod, 'js.client.file', file, 'file', { extension: '.mjs' })
);
});
tests.push(test);
}
if (mod.js.electron) {
let test = check(mod, 'js.electron', mod.js.electron, 'array');
test = test.then((passed) => {
if (!passed) return false;
return mod.js.electron.map((file) =>
check(mod, 'js.electron.file', file, 'object').then((passed) => {
if (!passed) return false;
return [
check(mod, 'js.electron.file.source', file.source, 'file', {
extension: '.mjs',
}),
// referencing the file within the electron app
// existence can't be validated, so only format is
check(mod, 'js.electron.file.target', file.target, 'string', {
extension: '.js',
}),
];
})
);
});
tests.push(test);
}
return tests;
});
},
validateOptions = (mod) => {
return check(mod, 'options', mod.options, 'array').then((passed) => {
if (!passed) return false;
return mod.options.map((option) =>
check(mod, 'options.option.type', option.type, registry.optionTypes).then((passed) => {
if (!passed) return false;
const tests = [
check(mod, 'options.option.key', option.key, 'alphanumeric'),
check(mod, 'options.option.label', option.label, 'string'),
check(mod, 'options.option.tooltip', option.tooltip, 'string', {
optional: true,
}),
check(mod, 'options.option.environments', option.environments, 'array', {
optional: true,
}).then((passed) => {
if (!passed) return false;
if (!option.environments) {
option.environments = registry.supportedEnvs;
return true;
}
return option.environments.map((environment) =>
check(
mod,
'options.option.environments.env',
environment,
registry.supportedEnvs
)
);
}),
];
switch (option.type) {
case 'toggle':
tests.push(check(mod, 'options.option.value', option.value, 'boolean'));
break;
case 'select':
tests.push(
check(mod, 'options.option.values', option.values, 'array').then((passed) => {
if (!passed) return false;
return option.values.map((value) =>
check(mod, 'options.option.values.value', value, 'string')
);
})
);
break;
case 'text':
case 'hotkey':
tests.push(check(mod, 'options.option.value', option.value, 'string'));
break;
case 'number':
case 'color':
tests.push(check(mod, 'options.option.value', option.value, option.type));
break;
case 'file':
tests.push(
check(mod, 'options.option.extensions', option.extensions, 'array').then(
(passed) => {
if (!passed) return false;
return option.extensions.map((value) =>
check(mod, 'options.option.extensions.extension', value, 'string')
);
}
)
);
}
return tests;
})
); );
}); return false;
}
const isTheme = mod.tags.includes('theme'),
hasThemeMode = mod.tags.includes('light') || mod.tags.includes('dark'),
isBothThemeModes = mod.tags.includes('light') && mod.tags.includes('dark');
if (isTheme && (!hasThemeMode || isBothThemeModes)) {
mod._err(
`invalid tags (themes must be either 'light' or 'dark', not neither or both):
${JSON.stringify(mod.tags)}`
);
return false;
}
return mod.tags.map((tag) => check(mod, 'tags.tag', tag, 'string'));
},
validateAuthors = async (mod) => {
const isArray = await check(mod, 'authors', mod.authors, 'array');
if (!isArray) return false;
return mod.authors.map((author) => [
check(mod, 'authors.author.name', author.name, 'string'),
check(mod, 'authors.author.email', author.email, 'email', { optional: true }),
check(mod, 'authors.author.homepage', author.homepage, 'url'),
check(mod, 'authors.author.avatar', author.avatar, 'url'),
]);
},
validateCSS = async (mod) => {
const isArray = await check(mod, 'css', mod.css, 'object');
if (!isArray) return false;
const tests = [];
for (let dest of ['frame', 'client', 'menu']) {
if (!mod.css[dest]) continue;
let test = await check(mod, `css.${dest}`, mod.css[dest], 'array');
if (test) {
test = mod.css[dest].map((file) =>
check(mod, `css.${dest}.file`, file, 'file', { extension: '.css' })
);
}
tests.push(test);
}
return tests;
},
validateJS = async (mod) => {
const isArray = await check(mod, 'js', mod.js, 'object');
if (!isArray) return false;
const tests = [];
for (let dest of ['frame', 'client', 'menu']) {
if (!mod.js[dest]) continue;
let test = await check(mod, `js.${dest}`, mod.js[dest], 'array');
if (test) {
test = mod.js[dest].map((file) =>
check(mod, `js.${dest}.file`, file, 'file', { extension: '.mjs' })
);
}
tests.push(test);
}
if (mod.js.electron) {
const isArray = await check(mod, 'js.electron', mod.js.electron, 'array');
if (isArray) {
for (const file of mod.js.electron) {
const isObject = await check(mod, 'js.electron.file', file, 'object');
if (!isObject) {
tests.push(false);
continue;
}
tests.push([
check(mod, 'js.electron.file.source', file.source, 'file', {
extension: '.mjs',
}),
// referencing the file within the electron app
// existence can't be validated, so only format is
check(mod, 'js.electron.file.target', file.target, 'string', {
extension: '.js',
}),
]);
}
} else tests.push(false);
}
return tests;
},
validateOptions = async (mod) => {
const isArray = await check(mod, 'options', mod.options, 'array');
if (!isArray) return false;
const tests = [];
for (const option of mod.options) {
const key = 'options.option',
optTypeValid = await check(mod, `${key}.type`, option.type, registry.optionTypes);
if (!optTypeValid) {
tests.push(false);
continue;
}
option.environments = option.environments ?? registry.supportedEnvs;
tests.push([
check(mod, `${key}.key`, option.key, 'alphanumeric'),
check(mod, `${key}.label`, option.label, 'string'),
check(mod, `${key}.tooltip`, option.tooltip, 'string', {
optional: true,
}),
check(mod, `${key}.environments`, option.environments, 'array').then((isArray) => {
if (!isArray) return false;
return option.environments.map((environment) =>
check(mod, `${key}.environments.env`, environment, registry.supportedEnvs)
);
}),
]);
switch (option.type) {
case 'toggle':
tests.push(check(mod, `${key}.value`, option.value, 'boolean'));
break;
case 'select': {
let test = await check(mod, `${key}.values`, option.values, 'array');
if (test) {
test = option.values.map((value) =>
check(mod, `${key}.values.value`, value, 'string')
);
}
tests.push(test);
break;
}
case 'text':
case 'hotkey':
tests.push(check(mod, `${key}.value`, option.value, 'string'));
break;
case 'number':
case 'color':
tests.push(check(mod, `${key}.value`, option.value, option.type));
break;
case 'file': {
let test = await check(mod, `${key}.extensions`, option.extensions, 'array');
if (test) {
test = option.extensions.map((ext) =>
check(mod, `${key}.extensions.extension`, ext, 'string')
);
}
tests.push(test);
break;
}
}
}
return tests;
}; };
/** /**

View File

@ -14,9 +14,6 @@
import { env, fs, storage } from './_.mjs'; import { env, fs, storage } from './_.mjs';
import { validate } from './registry-validation.mjs'; import { validate } from './registry-validation.mjs';
export const _cache = [],
_errors = [];
/** /**
* mod ids whitelisted as part of the enhancer's core, permanently enabled * mod ids whitelisted as part of the enhancer's core, permanently enabled
* @constant * @constant
@ -64,27 +61,35 @@ export const welcomeNotification = {
version: env.version, version: env.version,
}; };
let _list,
_errors = [];
/** /**
* list all available mods in the repo * list all available mods in the repo
* @param {function} filter - a function to filter out mods * @param {function} filter - a function to filter out mods
* @returns {array} a validated list of mod.json objects * @returns {array} a validated list of mod.json objects
*/ */
export const list = async (filter = (mod) => true) => { export const list = async (filter = (mod) => true) => {
if (!_cache.length) { if (!_list) {
for (const dir of await fs.getJSON('repo/registry.json')) { _list = new Promise(async (res, rej) => {
try { const passed = [];
const mod = await fs.getJSON(`repo/${dir}/mod.json`); for (const dir of await fs.getJSON('repo/registry.json')) {
mod._dir = dir; try {
if (await validate(mod)) _cache.push(mod); const mod = {
} catch (e) { ...(await fs.getJSON(`repo/${dir}/mod.json`)),
console.log(e); _dir: dir,
_errors.push({ source: dir, message: 'invalid mod.json' }); _err: (message) => _errors.push({ source: dir, message }),
};
if (await validate(mod)) passed.push(mod);
} catch {
_errors.push({ source: dir, message: 'invalid mod.json' });
}
} }
} res(passed);
});
} }
const list = []; const filtered = [];
for (const mod of _cache) if (await filter(mod)) list.push(mod); for (const mod of await _list) if (await filter(mod)) filtered.push(mod);
return list; return filtered;
}; };
/** /**
@ -92,7 +97,7 @@ export const list = async (filter = (mod) => true) => {
* @returns {array<object>} error objects with an error message and a source directory * @returns {array<object>} error objects with an error message and a source directory
*/ */
export const errors = async () => { export const errors = async () => {
if (!_errors.length) await list(); await list();
return _errors; return _errors;
}; };
@ -102,8 +107,7 @@ export const errors = async () => {
* @returns {object} the mod's mod.json * @returns {object} the mod's mod.json
*/ */
export const get = async (id) => { export const get = async (id) => {
if (!_cache.length) await list(); return (await list((mod) => mod.id === id))[0];
return _cache.find((mod) => mod.id === id);
}; };
/** /**