UNPKG

contentful-migration

Version:
561 lines 22.2 kB
"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