contentful-migration
Version:
Migration tooling for contentful
561 lines • 22.2 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.OfflineAPI = exports.default = exports.ApiHook = void 0;
const lodash_1 = require("lodash");
const field_deletion_1 = __importDefault(require("./validator/field-deletion"));
const tag_1 = require("./validator/tag");
const content_type_1 = require("../entities/content-type");
const entry_1 = require("../entities/entry");
const tag_2 = require("../entities/tag");
const display_field_1 = __importDefault(require("./validator/display-field"));
const index_1 = __importDefault(require("./validator/schema/index"));
const type_change_1 = __importDefault(require("./validator/type-change"));
const annotations_1 = __importDefault(require("./validator/annotations"));
const tags_on_entry_1 = __importDefault(require("./validator/tags-on-entry"));
const link_1 = __importDefault(require("../entities/link"));
const editor_interface_1 = require("./validator/editor-interface");
const field_groups_count_1 = __importDefault(require("./validator/field-groups-count"));
const resource_links_1 = __importDefault(require("./validator/resource-links"));
var ApiHook;
(function (ApiHook) {
ApiHook["SaveContentType"] = "SAVE_CONTENT_TYPE";
ApiHook["PublishContentType"] = "PUBLISH_CONTENT_TYPE";
ApiHook["UnpublishContentType"] = "UNPUBLISH_CONTENT_TYPE";
ApiHook["SaveTag"] = "SAVE_TAG";
ApiHook["SaveEntry"] = "SAVE_ENTRY";
ApiHook["SaveEditorInterface"] = "SAVE_EDITOR_INTERFACE";
})(ApiHook = exports.ApiHook || (exports.ApiHook = {}));
const saveContentTypeRequest = function (ct) {
const apiContentType = (0, lodash_1.omit)(ct.toAPI(), 'sys');
apiContentType.fields = apiContentType.fields.filter((field) => !field.deleted);
return {
method: 'PUT',
url: `/content_types/${ct.id}`,
headers: {
'X-Contentful-Version': ct.version
},
data: apiContentType
};
};
const saveEntryRequest = function (entry) {
return {
method: 'PUT',
url: `/entries/${entry.id}`,
headers: {
'X-Contentful-Version': entry.version,
'X-Contentful-Content-Type': entry.contentTypeId
},
data: entry.toApiEntry()
};
};
const publishEntryRequest = function (entry) {
return {
method: 'PUT',
url: `/entries/${entry.id}/published`,
headers: {
'X-Contentful-Version': entry.version,
'X-Contentful-Content-Type': entry.contentTypeId
}
};
};
const localeBasedPublishEntryRequest = function (entry, locales) {
return {
method: 'PUT',
url: `/entries/${entry.id}/published`,
headers: {
'X-Contentful-Version': entry.version,
'X-Contentful-Content-Type': entry.contentTypeId
},
data: {
add: {
fields: {
'*': locales
}
}
}
};
};
const unpublishEntryRequest = function (entry) {
return {
method: 'DELETE',
url: `/entries/${entry.id}/published`,
headers: {
'X-Contentful-Version': entry.version
}
};
};
const deleteEntryRequest = function (entry) {
return {
method: 'DELETE',
url: `/entries/${entry.id}`,
headers: {
'X-Contentful-Version': entry.version
}
};
};
const publishContentTypeRequest = function (ct) {
return {
method: 'PUT',
url: `/content_types/${ct.id}/published`,
headers: {
'X-Contentful-Version': ct.version
}
};
};
const unpublishRequest = function (ct) {
return {
method: 'DELETE',
url: `/content_types/${ct.id}/published`,
headers: {
'X-Contentful-Version': ct.version
}
};
};
const deleteRequest = function (ct) {
return {
method: 'DELETE',
url: `/content_types/${ct.id}`,
headers: {
'X-Contentful-Version': ct.version
}
};
};
const saveEditorInterfacesRequest = function (contentTypeId, editorInterfaces) {
return {
method: 'PUT',
url: `/content_types/${contentTypeId}/editor_interface`,
headers: {
'X-Contentful-Version': editorInterfaces.version
},
data: editorInterfaces.toAPI()
};
};
const saveTagRequest = function (tag) {
return {
method: 'PUT',
url: `/tags/${tag.id}`,
headers: {
'X-Contentful-Version': tag.version
},
data: tag.toApiTag()
};
};
const deleteTagRequest = function (tag) {
return {
method: 'DELETE',
url: `/tags/${tag.id}`,
headers: {
'X-Contentful-Version': tag.version
}
};
};
class OfflineAPI {
constructor(options) {
this.modifiedContentTypes = null;
this.savedContentTypes = null;
this.publishedContentTypes = null;
this.editorInterfaces = null;
this.entries = null;
this.isRecordingRequests = false;
this.currentRequestsRecorded = null;
this.currentValidationErrorsRecorded = null;
this.intent = null;
this.requestBatches = [];
this.contentTypeValidators = [];
this.editorInterfaceValidators = [];
this.locales = [];
this.modifiedTags = null;
this.savedTags = null;
this.tagValidators = [];
this.entryValidators = [];
const { contentTypes, entries, locales, editorInterfacesByContentType = new Map(), tags = new Map() } = options;
this.modifiedContentTypes = contentTypes;
this.modifiedTags = tags;
// Initialize saved and published state
// These are (currently) exclusively needed for stateful validations
// for example "cannot delete before omitted was published"
//
// Since the `modifiedContentTypes` are mutable,
// we need to perform a clone.
// TODO: Build a better abstraction over `Map` that allows easy cloning
// and also allows us to implement async iterators
this.savedContentTypes = new Map();
this.savedTags = new Map();
this.publishedContentTypes = new Map();
this.editorInterfaces = editorInterfacesByContentType;
for (const [id, contentType] of contentTypes.entries()) {
this.savedContentTypes.set(id, contentType.clone());
this.publishedContentTypes.set(id, contentType.clone());
}
this.contentTypeValidators.push(new field_deletion_1.default(), new display_field_1.default(), new index_1.default(), new type_change_1.default(), new annotations_1.default(), new resource_links_1.default());
this.editorInterfaceValidators.push(new editor_interface_1.EditorInterfaceSchemaValidator(), new field_groups_count_1.default());
this.tagValidators.push(new tag_1.TagSchemaValidator());
// TODO We skip a schema validator for now, because in order to
// properly implement it, we would need to bump joi.
// TagsOnEntryValidator will failed if modifiedTags (tags in environment) is empty
this.entryValidators.push(new tags_on_entry_1.default(this.modifiedTags));
this.entries = entries;
this.locales = locales;
}
async getContentType(id) {
if (!this.hasContentType(id)) {
throw new Error(`Cannot get Content Type ${id} because it does not exist`);
}
return this.modifiedContentTypes.get(id);
}
async getEditorInterfaces(contentTypeId) {
if (!this.editorInterfaces.has(contentTypeId)) {
throw new Error(`Cannot get editor interfaces for Content Type ${contentTypeId} because it does not exist`);
}
return this.editorInterfaces.get(contentTypeId);
}
async hasContentType(id) {
return this.modifiedContentTypes.has(id);
}
async createContentType(id) {
this.assertRecording();
const ct = new content_type_1.ContentType({ sys: { id, version: 0 }, fields: [], name: undefined });
this.modifiedContentTypes.set(id, ct);
return ct;
}
async saveContentType(id) {
this.assertRecording();
const hasContentType = this.modifiedContentTypes.has(id);
if (!hasContentType) {
throw new Error(`Cannot save the content type (id: ${id}) because it does not exist`);
}
const ct = await this.getContentType(id);
// Store clone as a request
this.currentRequestsRecorded.push(saveContentTypeRequest(ct.clone()));
// Mutate version bump
ct.version = ct.version + 1;
this.modifiedContentTypes.set(id, ct);
this.savedContentTypes.set(id, ct.clone());
for (const validator of this.contentTypeValidators) {
if (validator.hooks.includes(ApiHook.SaveContentType)) {
const errors = validator.validate({
contentType: ct,
savedContentType: this.savedContentTypes.get(id),
publishedContentType: this.publishedContentTypes.get(id),
locales: this.locales
});
this.currentValidationErrorsRecorded = this.currentValidationErrorsRecorded.concat(errors);
}
}
return ct;
}
async publishContentType(id) {
this.assertRecording();
const ct = await this.getContentType(id);
// Store clone as a request
this.currentRequestsRecorded.push(publishContentTypeRequest(ct.clone()));
// Mutate version bump
ct.version = ct.version + 1;
this.modifiedContentTypes.set(id, ct);
this.savedContentTypes.set(id, ct.clone());
this.publishedContentTypes.set(id, ct.clone());
if (this.editorInterfaces.has(id)) {
const editorInterfaces = this.editorInterfaces.get(id);
editorInterfaces.version = editorInterfaces.version + 1;
}
for (const validator of this.contentTypeValidators) {
if (validator.hooks.includes(ApiHook.PublishContentType)) {
const errors = validator.validate({
contentType: ct,
savedContentType: this.savedContentTypes.get(id),
publishedContentType: this.publishedContentTypes.get(id),
locales: this.locales
});
this.currentValidationErrorsRecorded = this.currentValidationErrorsRecorded.concat(errors);
}
}
return ct;
}
async unpublishContentType(id) {
this.assertRecording();
const ct = await this.getContentType(id);
// Store clone as a request
this.currentRequestsRecorded.push(unpublishRequest(ct.clone()));
// Mutate version bump
ct.version = ct.version + 1;
this.modifiedContentTypes.set(id, ct);
this.savedContentTypes.set(id, ct);
this.publishedContentTypes.delete(id);
for (const validator of this.contentTypeValidators) {
if (validator.hooks.includes(ApiHook.UnpublishContentType)) {
const errors = validator.validate({
contentType: ct,
savedContentType: this.savedContentTypes.get(id),
publishedContentType: this.publishedContentTypes.get(id),
locales: this.locales
});
this.currentValidationErrorsRecorded = this.currentValidationErrorsRecorded.concat(errors);
}
}
return ct;
}
async deleteContentType(id) {
this.assertRecording();
const ct = await this.getContentType(id);
// Store clone as a request
this.currentRequestsRecorded.push(deleteRequest(ct.clone()));
this.modifiedContentTypes.delete(id);
this.publishedContentTypes.delete(id);
this.savedContentTypes.delete(id);
}
async saveEditorInterfaces(contentTypeId) {
this.assertRecording();
if (!this.editorInterfaces.has(contentTypeId)) {
throw new Error(`Cannot save editor interfaces for Content Type ${contentTypeId} because they do not exist`);
}
const editorInterfaces = this.editorInterfaces.get(contentTypeId);
this.currentRequestsRecorded.push(saveEditorInterfacesRequest(contentTypeId, editorInterfaces));
editorInterfaces.version = editorInterfaces.version + 1;
for (const validator of this.editorInterfaceValidators) {
if (validator.hooks.includes(ApiHook.SaveEditorInterface)) {
const errors = validator.validate(editorInterfaces);
this.currentValidationErrorsRecorded = this.currentValidationErrorsRecorded.concat(errors);
}
}
return editorInterfaces;
}
async createEntry(contentTypeId, id) {
this.assertRecording();
const entryData = {
sys: {
id,
version: 0,
contentType: {
sys: { type: 'Link', linkType: 'ContentType', id: contentTypeId }
}
},
fields: {}
};
const entry = new entry_1.Entry(entryData);
this.entries.push(entry);
return entry;
}
async saveEntry(id) {
this.assertRecording();
const hasEntry = await this.hasEntry(id);
if (!hasEntry) {
throw new Error(`Cannot save Entry ${id} because it does not exist`);
}
const entry = this.entries.find((entry) => entry.id === id);
// Store clone as a request
this.currentRequestsRecorded.push(saveEntryRequest(entry.clone()));
// Mutate version bump
entry.version = entry.version + 1;
// TODO: Add a validator for entries here that checks their final
// payload and checks it against existing tags
for (const validator of this.entryValidators) {
if (validator.hooks.includes(ApiHook.SaveEntry)) {
const errors = validator.validate(entry);
this.currentValidationErrorsRecorded = this.currentValidationErrorsRecorded.concat(errors);
}
}
return entry;
}
async hasEntry(id) {
return this.entries.some((entry) => entry.id === id);
}
async publishEntry(id) {
this.assertRecording();
const hasEntry = this.entries.some((entry) => entry.id === id);
if (!hasEntry) {
throw new Error(`Cannot publish Entry ${id} because it does not exist`);
} // Store clone as a request
const entry = this.entries.find((entry) => entry.id === id);
this.currentRequestsRecorded.push(publishEntryRequest(entry.clone()));
// Mutate version bump
entry.publishedVersion = entry.version;
entry.version = entry.version + 1;
// Mutate fieldStatus
entry.fieldStatus = {
'*': Object.fromEntries((entry.fieldStatus
? Object.keys(entry.fieldStatus['*'])
: await this.getLocalesForSpace()).map((locale) => [locale, 'published']))
};
return entry;
}
async localeBasedPublishEntry(id, locales) {
var _a;
this.assertRecording();
const hasEntry = this.entries.some((entry) => entry.id === id);
if (!hasEntry) {
throw new Error(`Cannot publish Entry ${id} because it does not exist`);
} // Store clone as a request
const entry = this.entries.find((entry) => entry.id === id);
this.currentRequestsRecorded.push(localeBasedPublishEntryRequest(entry.clone(), locales));
// Mutate version bump
entry.publishedVersion = entry.version;
entry.version = entry.version + 1;
// Mutate fieldStatus
entry.fieldStatus = {
'*': Object.assign(Object.assign({}, (_a = entry.fieldStatus) === null || _a === void 0 ? void 0 : _a['*']), locales.reduce((acc, locale) => {
acc[locale] = 'published';
return acc;
}, {}))
};
return entry;
}
async unpublishEntry(id) {
this.assertRecording();
const hasEntry = this.entries.some((entry) => entry.id === id);
if (!hasEntry) {
throw new Error(`Cannot unpublish Entry ${id} because it does not exist`);
}
const entry = this.entries.find((entry) => entry.id === id);
// Store clone as a request
this.currentRequestsRecorded.push(unpublishEntryRequest(entry.clone()));
// Mutate version bump
entry.publishedVersion = null;
entry.version = entry.version + 1;
return entry;
}
async deleteEntry(id) {
this.assertRecording();
const hasEntry = this.entries.some((entry) => entry.id === id);
if (!hasEntry) {
throw new Error(`Cannot delete Entry ${id} because it does not exist`);
} // Store clone as a request
const entry = this.entries.find((entry) => entry.id === id);
const index = this.entries.indexOf(entry);
this.entries.splice(index, 1);
this.currentRequestsRecorded.push(deleteEntryRequest(entry.clone()));
return entry;
}
async getEntriesForContentType(ctId) {
const entries = this.entries.filter((entry) => entry.contentTypeId === ctId);
return entries;
}
async getLinks(childId, locales) {
const links = [];
for (let entry of this.entries) {
const fields = entry.fields;
for (let key of Object.keys(fields)) {
for (let locale of locales) {
const field = (0, lodash_1.get)(entry.fields, `${key}.${locale}`);
if ((0, lodash_1.get)(field, 'sys.id') === childId) {
links.push(new link_1.default(entry, key, locale));
}
if ((0, lodash_1.isArray)(field)) {
const fieldArray = field;
fieldArray.forEach((fieldEntry, index) => {
if ((0, lodash_1.get)(fieldEntry, 'sys.id') === childId) {
links.push(new link_1.default(entry, key, locale, index));
}
});
}
}
}
}
return links;
}
async getLocalesForSpace() {
return this.locales;
}
async startRecordingRequests(intent) {
if (this.isRecordingRequests) {
throw new Error('You need to stop recording before starting again');
}
this.isRecordingRequests = true;
this.currentRequestsRecorded = [];
this.currentValidationErrorsRecorded = [];
this.currentRuntimeErrorsRecorded = [];
this.intent = intent;
}
// Returns all requests that needed to happen
// for all changes
async stopRecordingRequests() {
if (!this.isRecordingRequests) {
throw new Error('You need to start recording before stopping');
}
const batch = {
intent: this.intent,
requests: this.currentRequestsRecorded,
validationErrors: (0, lodash_1.compact)(this.currentValidationErrorsRecorded),
runtimeErrors: this.currentRuntimeErrorsRecorded
};
this.requestBatches.push(batch);
this.isRecordingRequests = false;
this.currentRequestsRecorded = [];
this.currentValidationErrorsRecorded = [];
this.intent = null;
}
async getRequestBatches() {
if (this.isRecordingRequests) {
throw new Error('Cannot get batches while still recording');
}
return this.requestBatches;
}
async createTag(id, visibility = 'private') {
this.assertRecording();
const tagData = {
sys: {
id,
version: 0,
visibility
},
name: undefined
};
const tag = new tag_2.Tag(tagData);
this.modifiedTags.set(id, tag);
return tag;
}
async saveTag(id) {
this.assertRecording();
const hasTag = await this.hasTag(id);
if (!hasTag) {
throw new Error(`Cannot save the tag (id: ${id}) because it does not exist`);
}
const tag = await this.getTag(id);
// Store clone as a request
this.currentRequestsRecorded.push(saveTagRequest(tag.clone()));
// Mutate version bump
tag.version = tag.version + 1;
this.modifiedTags.set(id, tag);
this.savedTags.set(id, tag.clone());
for (const validator of this.tagValidators) {
if (validator.hooks.includes(ApiHook.SaveTag)) {
const errors = validator.validate(tag);
this.currentValidationErrorsRecorded = this.currentValidationErrorsRecorded.concat(errors);
}
}
return tag;
}
async deleteTag(id) {
this.assertRecording();
const tag = await this.getTag(id);
this.currentRequestsRecorded.push(deleteTagRequest(tag.clone()));
this.modifiedTags.delete(id);
this.savedTags.delete(id);
}
async hasTag(id) {
return this.modifiedTags.has(id);
}
async getTag(id) {
if (!this.hasTag(id)) {
throw new Error(`Cannot get Tag ${id} because it does not exist`);
}
return this.modifiedTags.get(id);
}
async getTagsForEnvironment() {
return this.modifiedTags;
}
async recordRuntimeError(error) {
this.currentRuntimeErrorsRecorded.push(error);
}
assertRecording() {
if (this.isRecordingRequests) {
return;
}
throw new Error('You need to be recording to use the API methods.');
}
}
exports.default = OfflineAPI;
exports.OfflineAPI = OfflineAPI;
//# sourceMappingURL=index.js.map