Skip to content
This repository has been archived by the owner on Oct 25, 2022. It is now read-only.

Commit

Permalink
Better error management, fix Contentful import
Browse files Browse the repository at this point in the history
  • Loading branch information
stefanoverna committed Sep 18, 2020
1 parent 810504d commit 8df807c
Show file tree
Hide file tree
Showing 24 changed files with 642 additions and 611 deletions.
42 changes: 39 additions & 3 deletions bin/dato.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,46 @@
#!/usr/bin/env node
#!/usr/bin/env node --async-stack-traces

const PrettyError = require('pretty-error');
const colors = require('colors');

require('../lib');
const runCli = require('../lib/cli');
const ApiException = require('../lib/ApiException').default;

runCli().catch(e => {
process.stdout.write(`Command failed with the following error:\n`);
process.stdout.write(`${e.message}\n`);
process.stderr.write(colors.brightRed(`\nCommand failed!\n`));

if (e instanceof ApiException) {
const humanMessage = e.humanMessageForFailedResponse();

if (humanMessage) {
process.stderr.write(`${colors.red.underline(humanMessage)} \n\n`);
}

process.stderr.write(colors.underline.gray(`\nFailed request:\n\n`));

process.stderr.write(`${e.requestMethod} ${e.requestUrl}\n\n`);
for (const [key, value] of Object.entries(e.requestHeaders)) {
process.stderr.write(`${key}: ${value}\n`);
}
if (e.requestBody) {
process.stderr.write(`\n${e.requestBody}`);
}

process.stderr.write(colors.underline.gray(`\n\nHTTP Response:\n\n`));

process.stderr.write(`${e.statusCode} ${e.statusText}\n\n`);
for (const [key, value] of Object.entries(e.headers)) {
process.stderr.write(`${key}: ${value}\n`);
}

if (e.body) {
process.stderr.write(`\n${JSON.stringify(e.body)}`);
}
}

process.stderr.write(colors.underline.gray(`\n\nException details:\n\n`));
process.stderr.write(new PrettyError().render(e));

process.exit(1);
});
98 changes: 81 additions & 17 deletions src/ApiException.js
Original file line number Diff line number Diff line change
@@ -1,35 +1,99 @@
export default function ApiException(response, body) {
export default function ApiException(
response,
body,
{ url, options, preCallStack },
) {
if ('captureStackTrace' in Error) {
Error.captureStackTrace(this, ApiException);
} else {
this.stack = new Error().stack;
}

if (response) {
if (response.status < 500) {
const error = body.data[0];
const details = JSON.stringify(error.attributes.details);
this.message = `${response.status} ${error.attributes.code} (details: ${details})`;
} else {
this.message = `${response.status} ${response.statusText}`;
}

this.body = body;
this.headers = response.headers;
this.statusCode = response.status;
this.statusText = response.statusText;
if (response.status < 500) {
const error = body.data[0];
const details = JSON.stringify(error.attributes.details);
this.message = `${response.status} ${error.attributes.code} (details: ${details})`;
} else {
this.message = 'Misconfigured exception';
this.message = `${response.status} ${response.statusText}`;
}

this.body = body;
this.headers = response.headers.raw();
this.statusCode = response.status;
this.statusText = response.statusText;
this.requestUrl = url;
this.requestMethod = options.method || 'GET';
this.requestHeaders = options.headers;
this.requestBody = options.body;
this.stack += `\nCaused By:\n${preCallStack}`;
}

ApiException.prototype = Object.create(Error.prototype);
ApiException.prototype.name = 'ApiException';
ApiException.prototype.constructor = ApiException;
ApiException.prototype.errorWithCode = function errorWithCode(code) {

ApiException.prototype.errorWithCode = function errorWithCode(codeOrCodes) {
const codes = Array.isArray(codeOrCodes) ? codeOrCodes : [codeOrCodes];

if (!this.body || !(this.body.data instanceof Array)) {
return null;
}

return this.body.data.find(error => error.attributes.code === code);
return this.body.data.find(error => codes.includes(error.attributes.code));
};

const humanMessageForCode = {
BATCH_DATA_VALIDATION_IN_PROGRESS: `The schema of this model changed, we're re-running validations over every record in background. Please retry with this operation in a few seconds!`,
INSUFFICIENT_PERMISSIONS: `Your role does not permit this action`,
MAINTENANCE_MODE: `The project is currently in maintenance mode!`,
DELETE_RESTRICTION: `Sorry, but you cannot delete this resource, as it's currently used/referenced elsewhere!`,
INVALID_CREDENTIALS: `Credentials are incorrect!`,
INVALID_EMAIL: `Email address is incorrect!`,
INVALID_FORMAT: `The format of the parameters passed is incorrect, take a look at the details of the error to know what's wrong!`,
ITEM_LOCKED: `The operation cannot be completed as some other user is currently editing this record!`,
LINKED_FROM_PUBLISHED_ITEMS: `Couldn't unpublish the record, as some published records are linked to it!`,
PLAN_UPGRADE_REQUIRED: `Cannot proceed, please upgrade plan!`,
PUBLISHED_CHILDREN: `Couldn't unpublish the record, some children records are still published!`,
REQUIRED_2FA_SETUP: `This project requires every user to turn on 2-factor authentication! Please go to your Dashboard and activate it! (https://dashboard.datocms.com/account/setup-2fa)`,
REQUIRED_BY_ASSOCIATION: `Cannot delete the record, as it's required by other records:`,
STALE_ITEM_VERSION: `Someone else made a change while you were editing this record, please refresh the page!`,
TITLE_ALREADY_PRESENT: `There can only be one Title field per model`,
UNPUBLISHED_LINK: `Couldn't publish the record, as it links some unpublished records!`,
UNPUBLISHED_PARENT: `Couldn't publish the record, as the parent record is not published!`,
UPLOAD_IS_CURRENTLY_IN_USE: `Couldn't delete this asset, as it's currently used by some records!`,
UPLOAD_NOT_PASSING_FIELD_VALIDATIONS: `Couldn't update this asset since some records are failing to pass the validations!`,
};

const humanMessageForPlanUpgradeLimit = {
build_triggers: `You've reached the maximum number of build triggers your plan allows`,
sandbox_environments: `You've reached the maximum number of environments your plan allows`,
item_types: `You've reached the maximum number of models your plan allows to create`,
items: `You've reached the maximum number of records your plan allows to create`,
locales: `You've reached the maximum number of locales your plan allows`,
mux_encoding_seconds: `You've reached the maximum video encoding limits of your plan`,
otp: `Two-factor authentication cannot be on the current plan`,
plugins: `You've reached the maximum number of plugins your plan allows`,
roles: `You've reached the maximum number of roles your plan allows to create`,
uploadable_bytes: `You've reached the file storage limits of your plan`,
users: `You've reached the maximum number of collaborators your plan allows to invite to the project`,
access_tokens: `You've reached the maximum number of API tokens your plan allows to create`,
};

ApiException.prototype.humanMessageForFailedResponse = function humanMessageForFailedResponse() {
const planUpgradeError = this.errorWithCode('PLAN_UPGRADE_REQUIRED');

if (planUpgradeError) {
const { limit } = planUpgradeError.attributes.details;
return `${humanMessageForPlanUpgradeLimit[limit]}. Please head over to your account dashboard (https://dashboard.datocms.com/) to upgrade the plan or, if no publicly available plan suits your needs, contact our Sales team (https://www.datocms.com/contact) to get a custom quote!`;
}

const errors = Object.keys(humanMessageForCode)
.filter(code => this.errorWithCode(code))
.map(code => humanMessageForCode[code]);

if (errors.length === 0) {
return null;
}

return errors.join('\n');
};
23 changes: 19 additions & 4 deletions src/Client.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,15 +59,24 @@ export default class Client {
return `${this.baseUrl}${path}${query}`;
}

request(url, options = {}, retryCount = 1) {
request(url, options = {}, retryCount = 1, preCallStack = null) {
const fullHeaders = {
...this.defaultHeaders(),
...this.extraHeaders,
...options.headers,
};

Object.keys(fullHeaders).forEach(
key => fullHeaders[key] == null && delete fullHeaders[key],
);

const fullOptions = { ...options, headers: fullHeaders };

if (!preCallStack) {
// eslint-disable-next-line no-param-reassign
preCallStack = new Error().stack;
}

return fetch(url, fullOptions).then(res => {
if (res.status === 429) {
const waitTime = parseInt(
Expand All @@ -78,7 +87,7 @@ export default class Client {
`Rate limit exceeded, waiting ${waitTime * retryCount} seconds...`,
);
return wait(waitTime * retryCount * 1000).then(() => {
return this.request(url, options, retryCount + 1);
return this.request(url, options, retryCount + 1, preCallStack);
});
}

Expand All @@ -87,7 +96,13 @@ export default class Client {
if (res.status >= 200 && res.status < 300) {
return Promise.resolve(body);
}
return Promise.reject(new ApiException(res, body));
return Promise.reject(
new ApiException(res, body, {
url,
options: fullOptions,
preCallStack,
}),
);
})
.catch(error => {
if (
Expand All @@ -102,7 +117,7 @@ export default class Client {
`Data validation in progress, waiting ${retryCount} seconds...`,
);
return wait(retryCount * 1000).then(() => {
return this.request(url, options, retryCount + 1);
return this.request(url, options, retryCount + 1, preCallStack);
});
}
throw error;
Expand Down
5 changes: 2 additions & 3 deletions src/check/command.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,8 @@ export default function() {
const token = process.env.DATO_API_TOKEN;

if (token) {
process.exit();
return;
return undefined;
}

requireToken().then(() => process.exit());
return requireToken();
}
50 changes: 19 additions & 31 deletions src/contentfulImport/addValidationsOnField.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import ora from 'ora';
import Progress from './progress';
import { toItemApiKey, toFieldApiKey } from './toApiKey';
import datoFieldValidatorsFor from './datoFieldValidatorsFor';
import delay from './delay';

export default async ({
itemTypes,
Expand All @@ -13,48 +12,37 @@ export default async ({
contentfulData,
}) => {
const spinner = ora('').start();
const { contentTypes } = contentfulData;
const fieldsSize = contentTypes
.map(contentType => contentType.fields.length)
.reduce((acc, length) => acc + length, 0);

const progress = new Progress(fieldsSize, 'Adding validations on fields');
spinner.text = progress.tick();
try {
const { contentTypes } = contentfulData;
const fieldsSize = contentTypes
.map(contentType => contentType.fields.length)
.reduce((acc, length) => acc + length, 0);

for (const contentType of contentTypes) {
const contentTypeApiKey = toItemApiKey(contentType.sys.id);
const progress = new Progress(fieldsSize, 'Adding validations on fields');
spinner.text = progress.tick();

const itemTypeFields = fieldsMapping[contentTypeApiKey];
for (const contentType of contentTypes) {
const contentTypeApiKey = toItemApiKey(contentType.sys.id);

for (const field of contentType.fields) {
while (true) {
const itemTypeFields = fieldsMapping[contentTypeApiKey];

for (const field of contentType.fields) {
const fieldApiKey = toFieldApiKey(field.id);
const datoField = itemTypeFields.find(f => f.apiKey === fieldApiKey);
if (!datoField) {
break;
}

const validators = await datoFieldValidatorsFor({ field, itemTypes });

try {
await datoClient.fields.update(datoField.id, { validators });
spinner.text = progress.tick();
break;
} catch (e) {
if (
!e.body ||
!e.body.data ||
!e.body.data.some(d => d.id === 'BATCH_DATA_VALIDATION_IN_PROGRESS')
) {
spinner.fail(typeof e === 'object' ? e.message : e);
process.exit();
} else {
await delay(1000);
}
}
await datoClient.fields.update(datoField.id, { validators });
spinner.text = progress.tick();
}
}
}

spinner.succeed();
spinner.succeed();
} catch (e) {
spinner.fail();
throw e;
}
};
25 changes: 14 additions & 11 deletions src/contentfulImport/appClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,22 @@ export default async (
datoCmsCmaBaseUrl,
) => {
const spinner = ora('Configuring DatoCMS/Contentful clients').start();
const contentfulClient = createClient({ accessToken: contentfulToken });
const dato = new SiteClient(
datoCmsToken,
{ environment: datoCmsEnvironment },
datoCmsCmaBaseUrl,
);
let contentful;

try {
contentful = await contentfulClient.getSpace(contentfulSpaceId);
const contentfulClient = createClient({ accessToken: contentfulToken });

const dato = new SiteClient(
datoCmsToken,
{ environment: datoCmsEnvironment },
datoCmsCmaBaseUrl,
);

const contentful = await contentfulClient.getSpace(contentfulSpaceId);
spinner.succeed();

return { dato, contentful };
} catch (e) {
spinner.fail(typeof e === 'object' ? e.message : e);
process.exit();
spinner.fail();
throw e;
}
return { dato, contentful };
};
Loading

0 comments on commit 8df807c

Please sign in to comment.