UNPKG

@curvenote/cli

Version:
578 lines (577 loc) 26.5 kB
import { selectors } from 'myst-cli'; import { v4 as uuid } from 'uuid'; import { confirmOrExit } from '../utils/utils.js'; import chalk from 'chalk'; import { format } from 'date-fns'; import { getFromJournals, getFromUrl, postNewSubmission, postNewWork, postNewWorkVersion, postUpdateSubmissionWorkVersion, } from './utils.js'; import inquirer from 'inquirer'; import { plural } from 'myst-common'; import { getWorkFromKey } from '../works/utils.js'; export function kindQuestions(kinds) { return { name: 'kinds', type: 'list', message: 'What kind of submission are you making?', choices: kinds.map(({ name, id }) => ({ name, value: id })), }; } export function collectionMoniker(collection) { if (collection.name === collection.content.title || !collection.content.title) { return collection.name; } return `${collection.content.title} (${collection.name})`; } export function collectionQuestions(venue, collections, opts) { return { name: 'collections', type: 'list', message: `Venue ${venue} has multiple collections${(opts === null || opts === void 0 ? void 0 : opts.allowClosedCollection) ? '' : ' open for submission'}. Which do you want to select?`, choices: collections.map((item) => ({ name: collectionMoniker(item), value: item, })), }; } export function venueQuestion(session, action = 'submit') { return { name: 'venue', type: 'input', message: `Enter the venue name you want to ${action} to?`, filter: (venue) => venue.toLowerCase(), validate: async (venue) => { if (venue.length < 3) { return 'Venue name must be at least 3 characters'; } try { await getFromJournals(session, `/sites/${venue}`); } catch (err) { return `Venue "${venue}" not found.`; } return true; }, }; } /** * Choose and return a collection and kind based on venue * * By default, this function only looks at open collections; however, if * `opts.allowClosedCollection` is `true`, the "open" requirement is removed. * * First `collection` must be determined; successful cases include: * - `collection` is provided, valid, and "open" * - `venue` has a single, "open" `collection` * - `opts.yes` is `true` and the `venue` has a default, "open" `collection` * - user interactively selects one of the available `collections` on the `venue` * * On failure, this function will `process.exit(1)`. Failure cases include: * - No "open" collections * - Provided `collection` is invalid or not "open" * - `opts.yes` is `true` but there is no default, "open" `collection` * * After determining `collection`, `kind` is chosen using `determineKindFromCollection`. */ export async function determineCollectionAndKind(session, venue, collections, opts) { const openCollections = [ ...collections.items.filter((c) => c.open && c.default), ...collections.items.filter((c) => c.open && !c.default), ]; if (!(opts === null || opts === void 0 ? void 0 : opts.allowClosedCollection) && openCollections.length === 0) { session.log.info(`${chalk.red(`⛔️ no collections are open for submissions at venue "${venue}"`)}`); process.exit(1); } if (opts === null || opts === void 0 ? void 0 : opts.collection) session.log.debug(`Explicit collection provided: ${opts === null || opts === void 0 ? void 0 : opts.collection}`); let selectedCollection = (opts === null || opts === void 0 ? void 0 : opts.collection) ? collections.items.find((c) => c.name === (opts === null || opts === void 0 ? void 0 : opts.collection)) : undefined; if (opts === null || opts === void 0 ? void 0 : opts.collection) { if (!selectedCollection) { session.log.info(`${chalk.bold.red(`⛔️ collection "${opts === null || opts === void 0 ? void 0 : opts.collection}" does not exist at venue "${venue}"`)}`); session.log.info(`${chalk.bold(`🗂 open collections are: ${openCollections.map((c) => collectionMoniker(c)).join(', ')}`)}`); process.exit(1); } if (selectedCollection && !(selectedCollection === null || selectedCollection === void 0 ? void 0 : selectedCollection.open)) { session.log.info(`${chalk.bold.red(`⛔️ collection "${opts === null || opts === void 0 ? void 0 : opts.collection}" is not open for submissions at venue "${venue}"`)}`); if (!(opts === null || opts === void 0 ? void 0 : opts.allowClosedCollection)) { session.log.info(`${chalk.bold(`🗂 open collections are: ${openCollections .map((c) => collectionMoniker(c)) .join(', ')}`)}`); process.exit(1); } } } let prompted = false; if (!selectedCollection) { const defaultCollection = collections.items.find((c) => c.default); if (defaultCollection && !defaultCollection.open) { session.log.info(`${chalk.red(`🗂 default collection "${defaultCollection.name}" is not open for submissions at venue "${venue}"`)}`); } if (!(opts === null || opts === void 0 ? void 0 : opts.allowClosedCollection) && openCollections.length === 1) { selectedCollection = openCollections[0]; } else if ((opts === null || opts === void 0 ? void 0 : opts.allowClosedCollection) && collections.items.length === 1) { selectedCollection = collections.items[0]; } else if ((opts === null || opts === void 0 ? void 0 : opts.yes) && defaultCollection && (defaultCollection.open || (opts === null || opts === void 0 ? void 0 : opts.allowClosedCollection))) { selectedCollection = defaultCollection; } else if (opts === null || opts === void 0 ? void 0 : opts.yes) { session.log.info(`${chalk.red(`⛔️ collection must be specified to continue submission`)}`); process.exit(1); } else { const response = await inquirer.prompt([ collectionQuestions(venue, (opts === null || opts === void 0 ? void 0 : opts.allowClosedCollection) ? collections.items : openCollections, opts), ]); selectedCollection = response.collections; prompted = true; } } session.log.info(`🗂 Collection ${collectionMoniker(selectedCollection)} selected`); const results = await determineKindFromCollection(session, venue, selectedCollection, opts); return { collection: selectedCollection, kind: results.kind, prompted: results.prompted || prompted, }; } /** * Fetch a list of kinds from `venue` API */ export async function listSubmissionKinds(session, venue) { return getFromJournals(session, `/sites/${venue}/kinds`); } /** * Fetch a single `venue` kind from API */ export async function getSubmissionKind(session, venue, kindIdOrName) { return getFromJournals(session, `/sites/${venue}/kinds/${kindIdOrName}`); } /** * Choose and return one kind based on venue and collection * * Successful cases include: * - `kind` is provided and available on the `collection` * - `collection` with a single `kind`, which is returned * - `opts.yes` is `true` and the `collection` has a default `kind`, which is returned * - user interactively selects one of the available `kinds` on the `collection` * * On failure, this function will `process.exit(1)`. Failure cases include: * - Invalid `kind` is provided * - `opts.yes` is `true` but there is no default `kind` * - API fetch for selected kind fails (user is not authorized, venue does not exist, etc) */ export async function determineKindFromCollection(session, venue, collection, opts) { const kinds = collection.kinds; let kindId; let prompted = false; if (opts === null || opts === void 0 ? void 0 : opts.kind) { const match = kinds.find(({ name }) => { var _a; return name.toLowerCase() === ((_a = opts.kind) === null || _a === void 0 ? void 0 : _a.toLowerCase()); }); if (!match) { session.log.info(`${chalk.bold.red(`⛔️ submission kind "${opts.kind}" is not accepted in the collection "${collectionMoniker(collection)}"`)}`); session.log.info(`${chalk.bold(`📚 accepted kinds are: ${kinds.map((k) => k.name).join(', ')}`)}`); process.exit(1); } // Return the actual kind (including case) kindId = match.id; session.log.debug(`kindId from options`); } else if (kinds.length === 1) { kindId = kinds[0].id; session.log.debug(`kindId from only kind`); } else if (opts === null || opts === void 0 ? void 0 : opts.yes) { const defaultKind = kinds.find((k) => k.default); if (defaultKind) { session.log.debug(`kindId from default kind`); kindId = defaultKind.id; } else { session.log.info(`${chalk.red(`⛔️ kind must be specified to continue submission`)}`); process.exit(1); } } else { const response = await inquirer.prompt([kindQuestions(kinds)]); kindId = response.kinds; prompted = true; session.log.debug(`kindId from prompt`); } session.log.debug(`kindId: ${kindId}`); let kind; // retrieve the full kind DTO from the API try { kind = await getSubmissionKind(session, venue, kindId); } catch (err) { session.log.info(`${chalk.red(`🚨 could not get submission kind details "${kindId}" from venue ${venue}`)}`); process.exit(1); } return { kind, prompted }; } export function getSiteConfig(session) { const siteConfig = selectors.selectCurrentSiteConfig(session.store.getState()); if (!siteConfig) { throw new Error('🧐 No site config found.'); } return siteConfig; } export async function ensureVenue(session, venue, opts = { action: 'submit' }) { if (venue) return venue; if (opts === null || opts === void 0 ? void 0 : opts.yes) { throw new Error(`⛔️ venue must be specified to continue submission`); } session.log.debug('No venue provided, prompting user...'); const answer = await inquirer.prompt([venueQuestion(session, opts.action)]); return answer.venue; } /** * Prompt user for a new work key * * First, gives a simple Y/n with a default UUID. If the user is unhappy with that, * they are prompted to write their own key. * * This key cannot already exist as a work key; if you want to link to an existing * work, you must put the key directly in your project config file. */ export async function promptForNewKey(session, opts) { const defaultKey = uuid(); if (opts === null || opts === void 0 ? void 0 : opts.yes) { session.log.debug(`Using autogenerated key: ${defaultKey}`); return defaultKey; } const { useDefault } = await inquirer.prompt([ { name: 'useDefault', message: `Work key is required. Use autogenerated value? (${defaultKey})`, type: 'confirm', default: true, }, ]); if (useDefault) return defaultKey; const { customKey } = await inquirer.prompt([ { name: 'customKey', type: 'input', message: 'Enter a unique key for your work?', validate: async (key) => { if (key.length < 8) { return 'Key must be at least 8 characters'; } if (key.length > 50) { return 'Key must be no more than 50 characters'; } try { const { exists } = await getFromJournals(session, `/works/key/${key}`); if (exists) return `Key "${key}" not available.`; } catch (err) { return 'Key validation failed'; } return true; }, }, ]); return customKey; } /** * Ensure that a `venue` exists by performing a basic request to the venue * * If venue does not exist, fails with `process.exit(1)`. */ export async function checkVenueExists(session, venue) { try { session.log.debug(`GET from journals API /sites/${venue}`); await getFromJournals(session, `/sites/${venue}`); session.log.debug(`found venue "${venue}"`); } catch (err) { session.log.debug(err); session.log.error(`${chalk.red(`😟 venue "${venue}" not found.`)}`); process.exit(1); } } export async function checkVenueSubmitAccess(session, venue) { try { session.log.debug('checking submit access'); const { submit } = (await getFromJournals(session, `/sites/${venue}/access`)); session.log.debug('checking submit access - GET successful', submit); if (submit) return true; session.log.debug('You do not have permission to submit to this venue.'); throw new Error('You do not have permission to submit to this venue.'); } catch (err) { session.log.info(`${chalk.red(`🚦 venue "${venue}" is not accepting submissions.`)}`); session.log.info(`${chalk.gray(`🤖 API Response: ${err.message}`)}`); session.log.debug(JSON.stringify(err, null, 2)); process.exit(1); } } /** * Fetch `venue` collections from API */ export async function listCollections(session, venue) { return getFromJournals(session, `/sites/${venue}/collections`); } /** * Get collections from `venue` and log information about open collections * * This will fail with `process.exit(1)` if the fetch for venue collections fails. * By default, it also fails if there are no open collections. * * If `requireOpenCollections` is false, this function will not fail if there are * only closed collections or no collections at all. */ export async function getVenueCollections(session, venue, requireOpenCollections = true) { let collections; try { collections = await listCollections(session, venue); } catch (err) { session.log.info(`${chalk.red(`🚦 venue "${venue}" is unavailable; make sure the name is correct and you have permission to access`)}`); process.exit(1); } const openCollections = collections.items.filter((c) => c.open); if (openCollections.length === 0) { session.log.info(`${chalk.red(`🚦 venue "${venue}" has no open collections.`)}`); if (requireOpenCollections) process.exit(1); } else if (openCollections.length > 1) { session.log.info(`${chalk.green(`💚 venue "${venue}" is accepting submissions in the following collections: ${openCollections .map((c) => collectionMoniker(c)) .join(', ')}`)}`); } else { session.log.info(`${chalk.green(`💚 venue "${venue}" is accepting submissions (${collectionMoniker(openCollections[0])}).`)}`); } return collections; } export async function checkForSubmissionKeyInUse(session, venue, key) { session.log.debug(`checking to see if submission key is in use: "${key}"`); try { const { exists } = (await getFromJournals(session, `/sites/${venue}/submissions/key/${key}`)); return exists; } catch (err) { session.log.debug(err); return null; } } export async function chooseSubmission(session, submissions, opts) { if (opts === null || opts === void 0 ? void 0 : opts.yes) { session.log.debug(`Updating latest submission`); return submissions[0]; } const { useLatest } = await inquirer.prompt([ { name: 'useLatest', message: `Update latest submission?`, type: 'confirm', default: true, }, ]); if (useLatest) return submissions[0]; throw new Error('Using non-latest submission not yet supported...'); } export async function getAllSubmissionsUsingKey(session, venue, key) { session.log.debug(`checking for existing submission using key "${key}"`); const submissions = []; try { const siteSubmissions = await getFromJournals(session, `/sites/${venue}/submissions?key=${key}`); submissions.push(...siteSubmissions.items); } catch (err) { session.log.debug(err); } try { const mySubmissions = await getFromJournals(session, `/my/submissions?key=${key}`); // These extra fetches are required for the old version of the API where // the 'key' query parameter is not respected on /my/submissions. // This may be removed (along with the 'correctKey' check below) once // the API is updated. const works = await Promise.all(mySubmissions.items.map((sub) => { return getFromUrl(session, sub.links.work); })); submissions.push(...mySubmissions.items.filter((submission, ind) => { const correctVenue = submission.site_name === venue; const correctKey = works[ind].key === key; const submissionIsDuplicate = submissions.map(({ id }) => id).includes(submission.id); return correctVenue && correctKey && !submissionIsDuplicate; })); } catch (err) { session.log.debug(err); } return submissions; } export async function getSubmissionToUpdate(session, submissions) { const draftSubmissions = submissions.filter((submission) => { return submission.status === 'DRAFT'; }); const nonDraftSubmissions = submissions.filter((submission) => { return submission.status !== 'DRAFT'; }); if (draftSubmissions.length > 0) { session.log.debug(`Ignoring ${plural('%s draft submission(s)', draftSubmissions)}`); } if (nonDraftSubmissions.length === 0) { session.log.debug('existing submission not found'); return; } if (nonDraftSubmissions.length === 1) { session.log.debug(`${chalk.bold(`🔍 Found one existing submission`)}`); return nonDraftSubmissions[0]; } session.log.info(`🔍 Found ${plural('%s existing submission(s)', nonDraftSubmissions)} for this work`); const submission = await chooseSubmission(session, nonDraftSubmissions); return submission; } export async function confirmUpdateToExistingSubmission(session, venue, collections, submission, key, opts) { var _a, _b, _c; session.log.debug('found existing submission with work'); const lastSubDate = submission.active_version.date_created; session.log.info(chalk.bold(`🗓 Work was last submitted to "${venue}" on ${lastSubDate ? format(new Date(lastSubDate), 'dd MMM, yyyy HH:mm:ss') : 'unknown'}.`)); try { session.log.debug('existing submission collection id', (_a = submission.collection) === null || _a === void 0 ? void 0 : _a.id); const collection = collections.items.find((c) => { var _a; return c.id === ((_a = submission.collection) === null || _a === void 0 ? void 0 : _a.id); }); const openCollections = collections.items.filter((c) => c.open); if ((opts === null || opts === void 0 ? void 0 : opts.collection) && opts.collection !== ((_b = submission.collection) === null || _b === void 0 ? void 0 : _b.name)) { session.log.info(`🪧 NOTE: the --collection option was provided, but will be ignored as you are updating an existing submission`); } session.log.info(`✅ Submission found, collection: ${collection ? collectionMoniker(collection) : 'unknown'}, ${plural('%s version(s)', submission.num_versions)} present, active status: ${submission.active_version.status}.`); if (!(collection === null || collection === void 0 ? void 0 : collection.open)) { session.log.error(chalk.bold.red('⛔️ The collection for this submission is not accepting submissions')); session.log.info(`${chalk.bold(`📚 Open collections are: ${openCollections.map((c) => collectionMoniker(c)).join(', ')}`)}`); process.exit(1); } const work = await getWorkFromKey(session, key); if (!work) { session.log.info(`${chalk.yellow('👉 You do not own the work associated with this submission, but you may still be able to update it')}`); } const kindId = (submission === null || submission === void 0 ? void 0 : submission.kind).id; const kindName = (_c = (submission === null || submission === void 0 ? void 0 : submission.kind).name) !== null && _c !== void 0 ? _c : submission === null || submission === void 0 ? void 0 : submission.kind; if ((opts === null || opts === void 0 ? void 0 : opts.kind) && opts.kind !== kindName) { session.log.info(`🪧 NOTE: the --kind option was provided, but will be ignored as you are updating an existing submission`); } session.log.debug(`resolved kind to ${kindName}`); const kind = await getSubmissionKind(session, venue, kindId); if (!collection.kinds.find((k) => k.id === kindId)) { session.log.error(`${chalk.red(`⛔️ The kind "${kind.name}" is not accepted in the collection "${collectionMoniker(collection)}". This indicates a problem with your previous submission, please contact support@curvenote.com.`)}`); process.exit(1); } await confirmOrExit(`Update your submission to "${venue}" based on the contents of your local folder?`, opts); return { kind, collection }; } catch (err) { session.log.debug(err); session.log.info(`${chalk.red(`🚨 Submission not found, or you do not have permission to update it`)}`); process.exit(1); } } export async function createNewSubmission(session, submitLog, venue, collection, kind, cdn, cdnKey, jobId, key, opts) { var _a, _b; const workResp = await getWorkFromKey(session, key); let work; if (workResp) { session.log.debug(`posting new work version...`); work = await postNewWorkVersion(session, workResp.links.versions, cdnKey, cdn); session.log.debug(`new work posted with version id ${work.version_id}`); } else { session.log.debug(`posting new work...`); try { work = await postNewWork(session, cdnKey, cdn, key); } catch (err) { if (opts === null || opts === void 0 ? void 0 : opts.draft) { session.log.debug(`unable to create a work with key ${key} - attempting to create un-keyed work for draft submission`); work = await postNewWork(session, cdnKey, cdn); } else { throw err; } } session.log.debug(`new work posted with id ${work.id}`); } if (!work.version_id) { throw new Error('Failed to create a work version'); } session.log.debug(`posting new submission...`); const submission = await postNewSubmission(session, venue, collection.id, kind.id, work.version_id, (_a = opts === null || opts === void 0 ? void 0 : opts.draft) !== null && _a !== void 0 ? _a : false, jobId); session.log.debug(`new submission posted with id ${submission.id}`); if (opts === null || opts === void 0 ? void 0 : opts.draft) { session.log.info(`🚀 ${chalk.green(`Your draft was successfully submitted to "${venue}"`)}.`); } else { session.log.info(`🚀 ${chalk.green(`Your work was successfully submitted to "${venue}"`)}.`); } submitLog.work = { id: work.id, date_created: (_b = workResp === null || workResp === void 0 ? void 0 : workResp.date_created) !== null && _b !== void 0 ? _b : work.date_created, }; submitLog.workVersion = { id: work.version_id, date_created: work.date_created, }; submitLog.submission = { id: submission.id, date_created: submission.date_created, }; submitLog.submissionVersion = { id: submission.active_version_id, date_created: submission.date_created, }; } export async function updateExistingSubmission(session, submitLog, venue, cdnKey, existingSubmission, jobId) { var _a; session.log.debug(`existing submission - upload & post`); try { if (!existingSubmission.links.work) { throw new Error('No work associated with existing submission'); } session.log.debug(`getting existing work...`); const workResp = await getFromUrl(session, `${existingSubmission.links.work}?submission=${existingSubmission.id}`); if (!workResp) { throw new Error('Unable to fetch work associated with existing submission'); } session.log.debug(`posting new work version...`); const work = await postNewWorkVersion(session, workResp.links.versions, cdnKey, session.config.privateCdnUrl); if (!work.version_id) { throw new Error('Failed to create a work version'); } session.log.debug(`work version posted with id ${work.version_id}`); session.log.debug(`posting new version to existing submission...`); const submissionVersion = await postUpdateSubmissionWorkVersion(session, venue, existingSubmission.links.versions, work.version_id, jobId); session.log.debug(`submission version posted with id ${submissionVersion.id}`); session.log.info(`🚀 ${chalk.bold.green(`Your submission was successfully updated at "${venue}"`)}.`); submitLog.work = { id: work.id, date_created: (_a = workResp === null || workResp === void 0 ? void 0 : workResp.date_created) !== null && _a !== void 0 ? _a : work.date_created, }; submitLog.workVersion = { id: work.version_id, date_created: work.date_created, }; submitLog.submission = { id: existingSubmission.id, date_created: existingSubmission.date_created, }; submitLog.submissionVersion = { id: submissionVersion.id, date_created: submissionVersion.date_created, }; } catch (err) { session.log.error(err.message); throw new Error(`🚨 ${chalk.bold.red('Could not update your submission')}`); } }