UNPKG

@dossierhq/integration-test

Version:

Integration test to ensure that different Dossier database adapters work as expected.

1,002 lines 95 kB
/// <reference types="./SchemaUpdateSchemaSpecificationSubSuite.d.ts" /> import { copyEntity, ErrorType, EventType, FieldType, getAllNodesForConnection, ok, } from '@dossierhq/core'; import { assertEquals, assertErrorResult, assertOkResult, assertResultValue, assertSame, assertTruthy, } from '../Asserts.js'; import {} from '../Builder.js'; import { assertIsComponents, assertIsPublishedComponents, } from '../SchemaTypes.js'; import { assertChangelogEventsConnection } from '../shared-entity/EventsTestUtils.js'; import { CHANGE_VALIDATIONS_CREATE, MIGRATIONS_ENTITY_CREATE, VALUE_ITEMS_CREATE, } from '../shared-entity/Fixtures.js'; import { withSchemaAdvisoryLock } from '../shared-entity/SchemaTestUtils.js'; import { collectMatchingSearchResultNodes, countSearchResultWithEntity, } from '../shared-entity/SearchTestUtils.js'; export const SchemaUpdateSchemaSpecificationSubSuite = [ updateSchemaSpecification_removeAllFieldsFromMigrationEntity, updateSchemaSpecification_removeAllFieldsFromMigrationComponent, updateSchemaSpecification_removeAllTemporaryEntityTypes, updateSchemaSpecification_removeAllTemporaryComponentTypes, updateSchemaSpecification_updateSchemaEvent, updateSchemaSpecification_concurrentUpdates, updateSchemaSpecification_adminOnlyEntityMakesPublishedEntityInvalidAndRemovedFromFtsIndex, updateSchemaSpecification_adminOnlyComponentTypeMakesPublishedEntityInvalid, updateSchemaSpecification_adminOnlyComponentTypeRemovesFromIndex, updateSchemaSpecification_adminOnlyFieldMakesPublishedEntityValid, updateSchemaSpecification_adminOnlyFieldRemovesFromIndex, updateSchemaSpecification_deleteFieldOnEntity, updateSchemaSpecification_deleteFieldOnEntityAndReplaceWithAnotherField, updateSchemaSpecification_deleteFieldOnEntityInvalidBecomesValid, updateSchemaSpecification_deleteFieldOnEntityIndexesUpdated, updateSchemaSpecification_deleteFieldOnEntityUpdatesFtsIndexEvenWhenInvalid, updateSchemaSpecification_deleteFieldOnComponent, updateSchemaSpecification_deleteFieldOnComponentIndexesUpdated, updateSchemaSpecification_renameFieldOnEntity, updateSchemaSpecification_renameFieldOnEntityAndReplaceWithAnotherField, updateSchemaSpecification_renameFieldOnComponent, updateSchemaSpecification_deleteTypeOnEntityType, updateSchemaSpecification_deleteTypeOnTwoEntityTypes, updateSchemaSpecification_deleteTypeOnComponent, updateSchemaSpecification_deleteTypeOnComponentAndReplaceWithAnotherType, updateSchemaSpecification_deleteTypeOnComponentInvalidBecomesValid, updateSchemaSpecification_deleteTypeOnComponentIndexesUpdated, updateSchemaSpecification_renameTypeOnEntity, updateSchemaSpecification_renameTypeOnEntityAndReplaceWithAnotherType, updateSchemaSpecification_renameTypeOnComponent, updateSchemaSpecification_renameTypeOnComponentAndReplaceWithAnotherType, updateSchemaSpecification_renameTypeOnComponentUpdatesComponentTypeIndexes, updateSchemaSpecification_renameFieldAndRenameTypeOnEntity, updateSchemaSpecification_renameTypeAndRenameFieldOnEntity, updateSchemaSpecification_renameFieldAndRenameTypeOnComponent, updateSchemaSpecification_renameTypeAndRenameFieldOnComponent, updateSchemaSpecification_addingIndexToField, updateSchemaSpecification_deleteIndexClearsIndexDirectly, updateSchemaSpecification_deleteIndexMultiple, updateSchemaSpecification_renameIndexMaintainsLinkDirectly, updateSchemaSpecification_errorWrongVersion, updateSchemaSpecification_errorDeleteTypeOnEntityTypeWithExistingEntities, updateSchemaSpecification_errorReadonlySession, ]; async function updateSchemaSpecification_removeAllFieldsFromMigrationEntity({ clientProvider, }) { const client = clientProvider.dossierClient(); // Lock since the version needs to be consecutive const result = await withSchemaAdvisoryLock(client, async () => { const schemaSpec = (await client.getSchemaSpecification({ includeMigrations: true })).valueOrThrow(); const migrationEntitySpec = schemaSpec.entityTypes.find((it) => it.name === 'MigrationEntity'); if (migrationEntitySpec && migrationEntitySpec.fields.length > 0) { const updateResult = await client.updateSchemaSpecification({ migrations: [ { version: schemaSpec.version + 1, actions: migrationEntitySpec.fields.map((it) => ({ action: 'deleteField', entityType: 'MigrationEntity', field: it.name, })), }, ], }); assertOkResult(updateResult); } return ok(undefined); }); assertOkResult(result); } async function updateSchemaSpecification_removeAllFieldsFromMigrationComponent({ clientProvider, }) { const client = clientProvider.dossierClient(); // Lock since the version needs to be consecutive const result = await withSchemaAdvisoryLock(client, async () => { const schemaSpec = (await client.getSchemaSpecification({ includeMigrations: true })).valueOrThrow(); const migrationValueSpec = schemaSpec.componentTypes.find((it) => it.name === 'MigrationComponent'); if (migrationValueSpec && migrationValueSpec.fields.length > 0) { const updateResult = await client.updateSchemaSpecification({ migrations: [ { version: schemaSpec.version + 1, actions: migrationValueSpec.fields.map((it) => ({ action: 'deleteField', componentType: 'MigrationComponent', field: it.name, })), }, ], }); assertOkResult(updateResult); } return ok(undefined); }); assertOkResult(result); } async function updateSchemaSpecification_removeAllTemporaryEntityTypes({ clientProvider, }) { const client = clientProvider.dossierClient(); // Lock since the version needs to be consecutive const result = await withSchemaAdvisoryLock(client, async () => { const schemaSpec = (await client.getSchemaSpecification({ includeMigrations: true })).valueOrThrow(); const entitySpecs = schemaSpec.entityTypes.filter((it) => it.name.startsWith('MigrationEntity') && it.name !== 'MigrationEntity'); if (entitySpecs.length > 0) { // TODO deleting all entities should probably not be needed in the future, but is now for await (const node of getAllNodesForConnection({}, (paging) => client.getEntities({ entityTypes: entitySpecs.map((it) => it.name) }, paging))) { const { id, info: { status }, } = node.valueOrThrow(); if (status === 'published' || status === 'modified') { assertOkResult(await client.unpublishEntities([{ id }])); } assertOkResult(await client.archiveEntity({ id })); assertOkResult(await client.deleteEntities([{ id }])); } const updateResult = await client.updateSchemaSpecification({ migrations: [ { version: schemaSpec.version + 1, actions: entitySpecs.map((it) => ({ action: 'deleteType', entityType: it.name })), }, ], }); assertOkResult(updateResult); } return ok(undefined); }); assertOkResult(result); } async function updateSchemaSpecification_removeAllTemporaryComponentTypes({ clientProvider, }) { const client = clientProvider.dossierClient(); // Lock since the version needs to be consecutive const result = await withSchemaAdvisoryLock(client, async () => { const schemaSpec = (await client.getSchemaSpecification({ includeMigrations: true })).valueOrThrow(); const valueSpecs = schemaSpec.componentTypes.filter((it) => it.name.startsWith('MigrationComponent') && it.name !== 'MigrationComponent'); if (valueSpecs.length > 0) { const updateResult = await client.updateSchemaSpecification({ migrations: [ { version: schemaSpec.version + 1, actions: valueSpecs.map((it) => ({ action: 'deleteType', componentType: it.name })), }, ], }); assertOkResult(updateResult); } return ok(undefined); }); assertOkResult(result); } async function updateSchemaSpecification_updateSchemaEvent({ clientProvider }) { const client = clientProvider.dossierClient(); const fieldName = `field${new Date().getTime()}`; const result = await withSchemaAdvisoryLock(client, async () => { return await client.updateSchemaSpecification({ entityTypes: [ { name: 'MigrationEntity', fields: [{ name: fieldName, type: FieldType.String }] }, ], }); }); const { version } = result.valueOrThrow().schemaSpecification; const connectionResult = await client.getChangelogEvents({ types: [EventType.updateSchema], reverse: true, }); assertOkResult(connectionResult); assertTruthy(connectionResult.value); const eventsWithVersion = connectionResult.value.edges.filter((it) => it.node.isOk() && it.node.value.type === EventType.updateSchema && it.node.value.version === version); assertEquals(eventsWithVersion.length, 1); } async function updateSchemaSpecification_concurrentUpdates({ clientProvider }) { const client = clientProvider.dossierClient(); const fieldName1 = `field${new Date().getTime()}`; const fieldName2 = `${fieldName1}2`; // Lock since the version needs to be consecutive const result = await withSchemaAdvisoryLock(client, async () => { const schemaSpec = (await client.getSchemaSpecification({ includeMigrations: true })).valueOrThrow(); const newVersion = schemaSpec.version + 1; const updateOnePromise = client.updateSchemaSpecification({ version: newVersion, entityTypes: [ { name: 'MigrationEntity', fields: [{ name: fieldName1, type: FieldType.Boolean }] }, ], }); const updateTwoPromise = client.updateSchemaSpecification({ version: newVersion, entityTypes: [ { name: 'MigrationEntity', fields: [{ name: fieldName2, type: FieldType.String }] }, ], }); const [updateOneResult, updateTwoResult] = await Promise.all([ updateOnePromise, updateTwoPromise, ]); // Since one update will finish before the other, we expect one of them to fail claiming that the version is wrong if (updateOneResult.isOk()) { assertErrorResult(updateTwoResult, ErrorType.BadRequest, `Expected version ${newVersion + 1}, got ${newVersion}`); } else if (updateTwoResult.isOk()) { assertErrorResult(updateOneResult, ErrorType.BadRequest, `Expected version ${newVersion + 1}, got ${newVersion}`); } else { throw new Error('Expected one of the updates to succeed'); } return ok(undefined); }); assertOkResult(result); } async function updateSchemaSpecification_adminOnlyEntityMakesPublishedEntityInvalidAndRemovedFromFtsIndex({ clientProvider, }) { const client = clientProvider.dossierClient(); const publishedClient = clientProvider.publishedClient(); const query = { text: 'splendid presentation', }; // Lock since the version needs to be consecutive const result = await withSchemaAdvisoryLock(client, async () => { // Create entity const { entity: { id: entityId }, } = (await client.createEntity(copyEntity(CHANGE_VALIDATIONS_CREATE, { fields: { required: query.text } }), { publish: true })).valueOrThrow(); const reference = { id: entityId }; // Make the entity type not publishable assertOkResult(await client.updateSchemaSpecification({ entityTypes: [{ name: 'ChangeValidations', publishable: false, fields: [] }], })); // Process the entity assertResultValue(await client.processDirtyEntity(reference), { id: entityId, valid: true, validPublished: false, previousValid: true, previousValidPublished: true, }); // Check that the entity is invalid const adminEntity = (await client.getEntity(reference)).valueOrThrow(); assertEquals(adminEntity.info.valid, true); assertEquals(adminEntity.info.validPublished, false); // Check that we can't get the published entity const publishedEntityResult = await publishedClient.getEntity(reference); assertErrorResult(publishedEntityResult, ErrorType.BadRequest, `No entity spec for type ChangeValidations (id: ${entityId})`); // Check that it's not in the index const matchingNodesResult = await collectMatchingSearchResultNodes(publishedClient, query, (it) => (it.isOk() && it.value.id === entityId) || (it.isError() && it.message.includes(entityId))); assertResultValue(matchingNodesResult, []); // Make the entity type normal assertOkResult(await client.updateSchemaSpecification({ entityTypes: [{ name: 'ChangeValidations', publishable: true, fields: [] }], })); return ok(entityId); }); assertOkResult(result); } async function updateSchemaSpecification_adminOnlyComponentTypeMakesPublishedEntityInvalid({ clientProvider, }) { const client = clientProvider.dossierClient(); const publishedClient = clientProvider.publishedClient(); // Lock since the version needs to be consecutive const result = await withSchemaAdvisoryLock(client, async () => { // Create entity const { entity: { id: entityId }, } = (await client.createEntity(copyEntity(VALUE_ITEMS_CREATE, { fields: { any: { type: 'ChangeValidationsComponent', matchPattern: null } }, }), { publish: true })).valueOrThrow(); // Make the component adminOnly assertOkResult(await client.updateSchemaSpecification({ componentTypes: [{ name: 'ChangeValidationsComponent', adminOnly: true, fields: [] }], })); // Process the entity assertResultValue(await client.processDirtyEntity({ id: entityId }), { id: entityId, valid: true, validPublished: false, previousValid: true, previousValidPublished: true, }); // Check that the entity is invalid const publishedEntity = (await publishedClient.getEntity({ id: entityId })).valueOrThrow(); assertEquals(publishedEntity.info.valid, false); // Make the component normal assertOkResult(await client.updateSchemaSpecification({ componentTypes: [{ name: 'ChangeValidationsComponent', adminOnly: false, fields: [] }], })); return ok(entityId); }); assertOkResult(result); } async function updateSchemaSpecification_adminOnlyComponentTypeRemovesFromIndex({ clientProvider, }) { const client = clientProvider.dossierClient(); const publishedClient = clientProvider.publishedClient(); const query = { entityTypes: ['Components'], text: 'baz', }; // Lock since the version needs to be consecutive const result = await withSchemaAdvisoryLock(client, async () => { // Create entity const { entity: { id: entityId }, } = (await client.createEntity(copyEntity(VALUE_ITEMS_CREATE, { fields: { any: { type: 'ChangeValidationsComponent', matchPattern: query.text }, }, }), { publish: true })).valueOrThrow(); // Check that it's in the index const countBeforeSchemaUpdate = (await countSearchResultWithEntity(publishedClient, query, entityId)).valueOrThrow(); assertEquals(countBeforeSchemaUpdate, 1); // Make the component adminOnly assertOkResult(await client.updateSchemaSpecification({ componentTypes: [{ name: 'ChangeValidationsComponent', adminOnly: true, fields: [] }], })); // Process the entity assertResultValue(await client.processDirtyEntity({ id: entityId }), { id: entityId, valid: true, validPublished: false, previousValid: true, previousValidPublished: true, }); // Check that it's no longer in the index const countAfterSchemaUpdate = (await countSearchResultWithEntity(publishedClient, query, entityId)).valueOrThrow(); assertEquals(countAfterSchemaUpdate, 0); // Make the component normal assertOkResult(await client.updateSchemaSpecification({ componentTypes: [{ name: 'ChangeValidationsComponent', adminOnly: false, fields: [] }], })); return ok(entityId); }); assertOkResult(result); } async function updateSchemaSpecification_adminOnlyFieldMakesPublishedEntityValid({ clientProvider, }) { const client = clientProvider.dossierClient(); const publishedClient = clientProvider.publishedClient(); const fieldName = `field${new Date().getTime()}`; // Lock since the version needs to be consecutive const entityResult = await withSchemaAdvisoryLock(client, async () => { // First add new field assertOkResult(await client.updateSchemaSpecification({ entityTypes: [ { name: 'MigrationEntity', fields: [{ name: fieldName, type: FieldType.String }] }, ], })); // Create entity without the new field const { entity: { id: entityId }, } = (await client.createEntity(MIGRATIONS_ENTITY_CREATE, { publish: true })).valueOrThrow(); // Make it required assertOkResult(await client.updateSchemaSpecification({ entityTypes: [ { name: 'MigrationEntity', fields: [{ name: fieldName, type: FieldType.String, required: true }], }, ], })); // Process the entity assertResultValue(await client.processDirtyEntity({ id: entityId }), { id: entityId, valid: true, validPublished: false, previousValid: true, previousValidPublished: true, }); // Check that the entity is invalid const publishedEntity = (await publishedClient.getEntity({ id: entityId })).valueOrThrow(); assertEquals(publishedEntity.info.valid, false); // Make the field adminOnly assertOkResult(await client.updateSchemaSpecification({ entityTypes: [ { name: 'MigrationEntity', fields: [{ name: fieldName, type: FieldType.String, adminOnly: true }], }, ], })); // Process the entity assertResultValue(await client.processDirtyEntity({ id: entityId }), { id: entityId, valid: true, validPublished: true, previousValid: true, previousValidPublished: false, }); return ok(entityId); }); assertOkResult(entityResult); const entityId = entityResult.value; // Check that the entity is valid const publishedEntity = (await publishedClient.getEntity({ id: entityId })).valueOrThrow(); assertEquals(publishedEntity.info.valid, true); } async function updateSchemaSpecification_adminOnlyFieldRemovesFromIndex({ clientProvider, }) { const client = clientProvider.dossierClient(); const publishedClient = clientProvider.publishedClient(); const fieldName = `field${new Date().getTime()}`; const query = { entityTypes: ['MigrationEntity'], text: 'Scrumptious', }; // Lock since the version needs to be consecutive const entityResult = await withSchemaAdvisoryLock(client, async () => { // First add new field assertOkResult(await client.updateSchemaSpecification({ entityTypes: [ { name: 'MigrationEntity', fields: [{ name: fieldName, type: FieldType.String }] }, ], })); // Create entity const { entity: { id: entityId }, } = (await client.createEntity(copyEntity(MIGRATIONS_ENTITY_CREATE, { fields: { [fieldName]: query.text } }), { publish: true })).valueOrThrow(); // Check that it's in the index const countBeforeSchemaUpdate = (await countSearchResultWithEntity(publishedClient, query, entityId)).valueOrThrow(); assertEquals(countBeforeSchemaUpdate, 1); // Make the field adminOnly assertOkResult(await client.updateSchemaSpecification({ entityTypes: [ { name: 'MigrationEntity', fields: [{ name: fieldName, type: FieldType.String, adminOnly: true }], }, ], })); // Process the entity assertResultValue(await client.processDirtyEntity({ id: entityId }), { id: entityId, valid: true, validPublished: true, previousValid: true, previousValidPublished: true, }); return ok(entityId); }); const entityId = entityResult.valueOrThrow(); // Check that it's no longer in the index const countAfterSchemaUpdate = (await countSearchResultWithEntity(publishedClient, query, entityId)).valueOrThrow(); assertEquals(countAfterSchemaUpdate, 0); } async function updateSchemaSpecification_deleteFieldOnEntity({ clientProvider, }) { const client = clientProvider.dossierClient(); const publishedClient = clientProvider.publishedClient(); const fieldName = `field${new Date().getTime()}`; // Lock since the version needs to be consecutive const result = await withSchemaAdvisoryLock(client, async () => { // First add new field const firstUpdateResult = await client.updateSchemaSpecification({ entityTypes: [ { name: 'MigrationEntity', fields: [{ name: fieldName, type: FieldType.String }] }, ], }); const { schemaSpecification } = firstUpdateResult.valueOrThrow(); // Create entity with the new field set const { entity } = (await client.createEntity(copyEntity(MIGRATIONS_ENTITY_CREATE, { fields: { [fieldName]: 'value' } }), { publish: true })).valueOrThrow(); // Delete the field const secondUpdateResult = await client.updateSchemaSpecification({ migrations: [ { version: schemaSpecification.version + 1, actions: [{ action: 'deleteField', entityType: 'MigrationEntity', field: fieldName }], }, ], }); assertOkResult(secondUpdateResult); return ok(entity); }); assertOkResult(result); // Check that the field is removed const entityAfterMigration = (await client.getEntity({ id: result.value.id })).valueOrThrow(); assertEquals(fieldName in entityAfterMigration.fields, false); // And in published entity const publishedEntityAfterMigration = (await publishedClient.getEntity({ id: result.value.id })).valueOrThrow(); assertEquals(fieldName in publishedEntityAfterMigration.fields, false); // Ensure it's not possible to use the field const updateResult = await client.updateEntity({ id: result.value.id, fields: { [fieldName]: 'new value' }, }, { publish: true }); assertErrorResult(updateResult, ErrorType.BadRequest, `entity.fields: MigrationEntity does not include the fields: ${fieldName}`); } async function updateSchemaSpecification_deleteFieldOnEntityAndReplaceWithAnotherField({ clientProvider, }) { const client = clientProvider.dossierClient(); const publishedClient = clientProvider.publishedClient(); const fieldName = `field${new Date().getTime()}`; // Lock since the version needs to be consecutive const result = await withSchemaAdvisoryLock(client, async () => { // First add new field const firstUpdateResult = await client.updateSchemaSpecification({ entityTypes: [ { name: 'MigrationEntity', fields: [{ name: fieldName, type: FieldType.String }] }, ], }); const { schemaSpecification } = firstUpdateResult.valueOrThrow(); // Create entity with the new field set const { entity } = (await client.createEntity(copyEntity(MIGRATIONS_ENTITY_CREATE, { fields: { [fieldName]: 'value' } }), { publish: true })).valueOrThrow(); // Delete/replace the field const secondUpdateResult = await client.updateSchemaSpecification({ entityTypes: [ { name: 'MigrationEntity', fields: [{ name: fieldName, type: FieldType.Location, list: true }], }, ], migrations: [ { version: schemaSpecification.version + 1, actions: [{ action: 'deleteField', entityType: 'MigrationEntity', field: fieldName }], }, ], }); assertOkResult(secondUpdateResult); return ok(entity); }); assertOkResult(result); const entity = result.value; // Check that the field is reset const entityAfterMigration = (await client.getEntity({ id: entity.id })).valueOrThrow(); assertEquals(entityAfterMigration.fields[fieldName], null); // And for published entity const publishedEntityAfterMigration = (await publishedClient.getEntity({ id: entity.id })).valueOrThrow(); assertEquals(publishedEntityAfterMigration.fields[fieldName], null); // Check that the new field is usable const updatedEntity = (await client.updateEntity({ id: entity.id, fields: { [fieldName]: [{ lat: 1, lng: 2 }] } }, { publish: true })).valueOrThrow().entity; assertEquals(updatedEntity.fields[fieldName], [{ lat: 1, lng: 2 }]); } async function updateSchemaSpecification_deleteFieldOnEntityInvalidBecomesValid({ clientProvider, }) { const client = clientProvider.dossierClient(); const fieldName = `field${new Date().getTime()}`; // Lock since the version needs to be consecutive const result = await withSchemaAdvisoryLock(client, async () => { // First add new field const firstUpdateResult = await client.updateSchemaSpecification({ entityTypes: [ { name: 'MigrationEntity', fields: [{ name: fieldName, type: FieldType.String }] }, ], }); assertOkResult(firstUpdateResult); // Create entity with the new field set const { entity } = (await client.createEntity(copyEntity(MIGRATIONS_ENTITY_CREATE, { fields: { [fieldName]: 'this value will become invalid' }, }), { publish: true })).valueOrThrow(); // Change validations to make the field invalid const secondUpdateResult = await client.updateSchemaSpecification({ entityTypes: [ { name: 'MigrationEntity', fields: [{ name: fieldName, type: FieldType.String, values: [{ value: 'valid' }] }], }, ], }); const { schemaSpecification } = secondUpdateResult.valueOrThrow(); // Process the entity assertResultValue(await client.processDirtyEntity({ id: entity.id }), { id: entity.id, valid: false, validPublished: false, previousValid: true, previousValidPublished: true, }); // Delete the field const thirdUpdateResult = await client.updateSchemaSpecification({ migrations: [ { version: schemaSpecification.version + 1, actions: [{ action: 'deleteField', entityType: 'MigrationEntity', field: fieldName }], }, ], }); assertOkResult(thirdUpdateResult); // Process the entity assertResultValue(await client.processDirtyEntity({ id: entity.id }), { id: entity.id, valid: true, validPublished: true, previousValid: false, previousValidPublished: false, }); return ok(undefined); }); assertOkResult(result); } async function updateSchemaSpecification_deleteFieldOnEntityIndexesUpdated({ clientProvider, }) { const client = clientProvider.dossierClient(); const fieldName = `field${new Date().getTime()}`; const query = { entityTypes: ['MigrationEntity'], text: 'Supercalifragilisticexpialidocious', }; // Lock since the version needs to be consecutive const result = await withSchemaAdvisoryLock(client, async () => { // First add new field const firstUpdateResult = await client.updateSchemaSpecification({ entityTypes: [ { name: 'MigrationEntity', fields: [{ name: fieldName, type: FieldType.String }] }, ], }); const { schemaSpecification } = firstUpdateResult.valueOrThrow(); // Create entity with the new field set const { entity } = (await client.createEntity(copyEntity(MIGRATIONS_ENTITY_CREATE, { fields: { [fieldName]: query.text } }))).valueOrThrow(); // Check that it's in the index const countBeforeSchemaUpdate = (await countSearchResultWithEntity(client, query, entity.id)).valueOrThrow(); assertEquals(countBeforeSchemaUpdate, 1); // Delete the field const secondUpdateResult = await client.updateSchemaSpecification({ migrations: [ { version: schemaSpecification.version + 1, actions: [{ action: 'deleteField', entityType: 'MigrationEntity', field: fieldName }], }, ], }); assertOkResult(secondUpdateResult); // Process the entity assertResultValue(await client.processDirtyEntity({ id: entity.id }), { id: entity.id, valid: true, validPublished: null, previousValid: true, previousValidPublished: null, }); return ok(entity); }); assertOkResult(result); // Check that it's no longer in the index const countAfterSchemaUpdate = (await countSearchResultWithEntity(client, query, result.value.id)).valueOrThrow(); assertEquals(countAfterSchemaUpdate, 0); } async function updateSchemaSpecification_deleteFieldOnEntityUpdatesFtsIndexEvenWhenInvalid({ clientProvider, }) { // The reason why we test this is that we want to ensure that we update indexes even when an entity // becomes invalid. const client = clientProvider.dossierClient(); const fieldToDeleteName = `field${new Date().getTime()}Delete`; const fieldToBecomeInvalidName = `field${new Date().getTime()}Invalid`; const query = { entityTypes: ['MigrationEntity'], text: 'Kaboom', }; // Lock since the version needs to be consecutive const result = await withSchemaAdvisoryLock(client, async () => { // First add the fields const firstUpdateResult = await client.updateSchemaSpecification({ entityTypes: [ { name: 'MigrationEntity', fields: [ { name: fieldToDeleteName, type: FieldType.String }, { name: fieldToBecomeInvalidName, type: FieldType.String }, ], }, ], }); const { schemaSpecification } = firstUpdateResult.valueOrThrow(); // Create entity with the new fields set const { entity } = (await client.createEntity(copyEntity(MIGRATIONS_ENTITY_CREATE, { fields: { [fieldToDeleteName]: query.text, [fieldToBecomeInvalidName]: 'invalid' }, }))).valueOrThrow(); // Check that it's in the index const countBeforeSchemaUpdate = (await countSearchResultWithEntity(client, query, entity.id)).valueOrThrow(); assertEquals(countBeforeSchemaUpdate, 1); // Delete the field and change the other field to be invalid const secondUpdateResult = await client.updateSchemaSpecification({ entityTypes: [ { name: 'MigrationEntity', fields: [ { name: fieldToBecomeInvalidName, type: FieldType.String, values: [{ value: 'valid' }], }, ], }, ], migrations: [ { version: schemaSpecification.version + 1, actions: [ { action: 'deleteField', entityType: 'MigrationEntity', field: fieldToDeleteName }, ], }, ], }); assertOkResult(secondUpdateResult); // Process the entity assertResultValue(await client.processDirtyEntity({ id: entity.id }), { id: entity.id, valid: false, validPublished: null, previousValid: true, previousValidPublished: null, }); return ok(entity); }); assertOkResult(result); // Check that it's no longer in the index const countAfterSchemaUpdate = (await countSearchResultWithEntity(client, query, result.value.id)).valueOrThrow(); assertEquals(countAfterSchemaUpdate, 0); } async function updateSchemaSpecification_deleteFieldOnComponent({ clientProvider, }) { const client = clientProvider.dossierClient(); const publishedClient = clientProvider.publishedClient(); const fieldName = `field${new Date().getTime()}`; // Lock since the version needs to be consecutive const result = await withSchemaAdvisoryLock(client, async () => { // First add new field const firstUpdateResult = await client.updateSchemaSpecification({ componentTypes: [ { name: 'MigrationComponent', fields: [{ name: fieldName, type: FieldType.String }] }, ], }); const { schemaSpecification } = firstUpdateResult.valueOrThrow(); // Create entity with the new field set const { entity } = (await client.createEntity(copyEntity(VALUE_ITEMS_CREATE, { fields: { any: { type: 'MigrationComponent', [fieldName]: 'value' } }, }), { publish: true })).valueOrThrow(); // Delete the field const secondUpdateResult = await client.updateSchemaSpecification({ migrations: [ { version: schemaSpecification.version + 1, actions: [ { action: 'deleteField', componentType: 'MigrationComponent', field: fieldName }, ], }, ], }); assertOkResult(secondUpdateResult); return ok(entity); }); assertOkResult(result); // Check that the field is removed const entityAfterMigration = (await client.getEntity({ id: result.value.id })).valueOrThrow(); assertIsComponents(entityAfterMigration); const adminComponent = entityAfterMigration.fields.any; assertEquals(adminComponent.type, 'MigrationComponent'); assertEquals(fieldName in adminComponent, false); // And in published entity const publishedEntityAfterMigration = (await publishedClient.getEntity({ id: result.value.id })).valueOrThrow(); assertIsPublishedComponents(publishedEntityAfterMigration); const publishedComponent = publishedEntityAfterMigration.fields.any; assertEquals(publishedComponent.type, 'MigrationComponent'); assertEquals(fieldName in publishedComponent, false); // Ensure it's not possible to use the field const updateResult = await client.updateEntity({ id: result.value.id, fields: { any: { type: 'MigrationComponent', [fieldName]: 'new value' } }, }, { publish: true }); assertErrorResult(updateResult, ErrorType.BadRequest, `entity.fields.any: Invalid fields for component of type MigrationComponent: ${fieldName}`); } async function updateSchemaSpecification_deleteFieldOnComponentIndexesUpdated({ clientProvider, }) { const client = clientProvider.dossierClient(); const fieldName = `field${new Date().getTime()}`; const query = { entityTypes: ['Components'], text: 'Copyrightable', }; // Lock since the version needs to be consecutive const result = await withSchemaAdvisoryLock(client, async () => { // First add new field const firstUpdateResult = await client.updateSchemaSpecification({ componentTypes: [ { name: 'MigrationComponent', fields: [{ name: fieldName, type: FieldType.String }] }, ], }); const { schemaSpecification } = firstUpdateResult.valueOrThrow(); // Create entity with the new field set const { entity } = (await client.createEntity(copyEntity(VALUE_ITEMS_CREATE, { fields: { any: { type: 'MigrationComponent', [fieldName]: query.text } }, }))).valueOrThrow(); // Check that it's in the index const countBeforeSchemaUpdate = (await countSearchResultWithEntity(client, query, entity.id)).valueOrThrow(); assertEquals(countBeforeSchemaUpdate, 1); // Delete the field const secondUpdateResult = await client.updateSchemaSpecification({ migrations: [ { version: schemaSpecification.version + 1, actions: [ { action: 'deleteField', componentType: 'MigrationComponent', field: fieldName }, ], }, ], }); assertOkResult(secondUpdateResult); // Process the entity assertResultValue(await client.processDirtyEntity({ id: entity.id }), { id: entity.id, valid: true, validPublished: null, previousValid: true, previousValidPublished: null, }); return ok(entity); }); assertOkResult(result); // Check that it's no longer in the index const countAfterSchemaUpdate = (await countSearchResultWithEntity(client, query, result.value.id)).valueOrThrow(); assertEquals(countAfterSchemaUpdate, 0); } async function updateSchemaSpecification_renameFieldOnEntity({ clientProvider, }) { const client = clientProvider.dossierClient(); const publishedClient = clientProvider.publishedClient(); const oldFieldName = `field${new Date().getTime()}`; const newFieldName = `${oldFieldName}New`; // Lock since the version needs to be consecutive const result = await withSchemaAdvisoryLock(client, async () => { // First add new field const firstUpdateResult = await client.updateSchemaSpecification({ entityTypes: [ { name: 'MigrationEntity', fields: [{ name: oldFieldName, type: FieldType.String }] }, ], }); const { schemaSpecification } = firstUpdateResult.valueOrThrow(); // Create entity with the new field set const { entity } = (await client.createEntity(copyEntity(MIGRATIONS_ENTITY_CREATE, { fields: { [oldFieldName]: 'value' } }), { publish: true })).valueOrThrow(); // Rename the field const secondUpdateResult = await client.updateSchemaSpecification({ migrations: [ { version: schemaSpecification.version + 1, actions: [ { action: 'renameField', entityType: 'MigrationEntity', field: oldFieldName, newName: newFieldName, }, ], }, ], }); assertOkResult(secondUpdateResult); return ok(entity); }); assertOkResult(result); const entityId = result.value.id; // Check that the field is renamed const entityAfterMigration = (await client.getEntity({ id: entityId })).valueOrThrow(); assertEquals(oldFieldName in entityAfterMigration.fields, false); assertEquals(entityAfterMigration.fields[newFieldName], 'value'); // And in published entity const publishedEntityAfterMigration = (await publishedClient.getEntity({ id: entityId })).valueOrThrow(); assertEquals(oldFieldName in publishedEntityAfterMigration.fields, false); assertEquals(publishedEntityAfterMigration.fields[newFieldName], 'value'); // Check that the new name is usable const updatedNewNameEntity = (await client.updateEntity({ id: entityId, fields: { [newFieldName]: 'updated value' } }, { publish: true })).valueOrThrow().entity; assertEquals(updatedNewNameEntity.fields[newFieldName], 'updated value'); // Check that the old name is not usable const updatedOldNameResult = await client.updateEntity({ id: entityId, fields: { [oldFieldName]: 'updated value' } }, { publish: true }); assertErrorResult(updatedOldNameResult, ErrorType.BadRequest, `entity.fields: MigrationEntity does not include the fields: ${oldFieldName}`); } async function updateSchemaSpecification_renameFieldOnEntityAndReplaceWithAnotherField({ clientProvider, }) { const client = clientProvider.dossierClient(); const publishedClient = clientProvider.publishedClient(); const oldFieldName = `field${new Date().getTime()}`; const newFieldName = `${oldFieldName}New`; // Lock since the version needs to be consecutive const result = await withSchemaAdvisoryLock(client, async () => { // First add new field const firstUpdateResult = await client.updateSchemaSpecification({ entityTypes: [ { name: 'MigrationEntity', fields: [{ name: oldFieldName, type: FieldType.String }] }, ], }); const { schemaSpecification } = firstUpdateResult.valueOrThrow(); // Create entity with the new field set const { entity } = (await client.createEntity(copyEntity(MIGRATIONS_ENTITY_CREATE, { fields: { [oldFieldName]: 'value' } }), { publish: true })).valueOrThrow(); // Rename/replace the field const secondUpdateResult = await client.updateSchemaSpecification({ entityTypes: [ { name: 'MigrationEntity', fields: [{ name: oldFieldName, type: FieldType.Location, list: true }], }, ], migrations: [ { version: schemaSpecification.version + 1, actions: [ { action: 'renameField', entityType: 'MigrationEntity', field: oldFieldName, newName: newFieldName, }, ], }, ], }); assertOkResult(secondUpdateResult); return ok(entity); }); assertOkResult(result); const entity = result.value; // Check that the renamed field got the old value and the original field is reset const entityAfterMigration = (await client.getEntity({ id: entity.id })).valueOrThrow(); assertEquals(entityAfterMigration.fields[oldFieldName], null); assertEquals(entityAfterMigration.fields[newFieldName], 'value'); // And for published entity const publishedEntityAfterMigration = (await publishedClient.getEntity({ id: entity.id })).valueOrThrow(); assertEquals(publishedEntityAfterMigration.fields[oldFieldName], null); assertEquals(publishedEntityAfterMigration.fields[newFieldName], 'value'); // Check that both fields are usable const updatedEntity = (await client.updateEntity({ id: entity.id, fields: { [oldFieldName]: [{ lat: 1, lng: 2 }], [newFieldName]: 'updated value' }, }, { publish: true })).valueOrThrow().entity; assertEquals(updatedEntity.fields[oldFieldName], [{ lat: 1, lng: 2 }]); assertEquals(updatedEntity.fields[newFieldName], 'updated value'); } async function updateSchemaSpecification_renameFieldOnComponent({ clientProvider, }) { const client = clientProvider.dossierClient(); const publishedClient = clientProvider.publishedClient(); const oldFieldName = `field${new Date().getTime()}`; const newFieldName = `${oldFieldName}New`; // Lock since the version needs to be consecutive const result = await withSchemaAdvisoryLock(client, async () => { // First add new field const firstUpdateResult = await client.updateSchemaSpecification({ componentTypes: [ { name: 'MigrationComponent', fields: [{ name: oldFieldName, type: FieldType.String }] }, ], }); const { schemaSpecification } = firstUpdateResult.valueOrThrow(); // Create entity with the new field set const { entity } = (await client.createEntity(copyEntity(VALUE_ITEMS_CREATE, { fields: { any: { type: 'MigrationComponent', [oldFieldName]: 'value' } }, }), { publish: true })).valueOrThrow(); // Rename the field const secondUpdateResult = await client.updateSchemaSpecification({ migrations: [ { version: schemaSpecification.version + 1, actions: [ { action: 'renameField', componentType: 'MigrationComponent', field: oldFieldName, newName: newFieldName, }, ], }, ], }); assertOkResult(secondUpdateResult); return ok(entity); }); assertOkResult(result); const entityId = result.value.id; // Check that the field is renamed const entityAfterMigration = (await client.getEntity({ id: entityId })).valueOrThrow(); const componentAfterMigration = entityAfterMigration.fields.any; assertEquals(oldFieldName in componentAfterMigration, false); assertEquals(componentAfterMigration[newFieldName], 'value'); // And in published entity const publishedEntityAfterMigration = (await publishedClient.getEntity({ id: entityId })).valueOrThrow(); const publishedComponent = publishedEntityAfterMigration.fields.any; assertEquals(oldFieldName in publishedComponent, false); assertEquals(publishedComponent[newFieldName], 'value'); // Check that the new name is usable const updatedNewNameEntity = (await client.updateEntity({ id: entityId, fields: { any: { type: 'MigrationComponent', [newFieldName]: 'updated value' } }, }, { publish: true })).valueOrThrow().entity; assertEquals(updatedNewNameEntity.fields.any[newFieldName], 'updated value'); // Check that the old name is not usable const updatedOldNameResult = await client.updateEntity({ id: entityId, fields: { any: { type: 'MigrationComponent', [oldFieldName]: 'updated value' } }, }, { publish: true }); assertErrorResult(updatedOldNameResult, ErrorType.BadRequest, `entity.fields.any: Invalid fields for component of type MigrationComponent: ${oldFieldName}`); } async function updateSchemaSpecification_deleteTypeOnEntityType({ clientProvider, }) { const client = clientProvider.dossierClient(); const typeName = `MigrationEntity${new Date().getTime()}`; // Lock since the version needs to be consecutive const result = await withSchemaAdvisoryLock(client, async () => { // First add new type const firstUpdateResult = await client.updateSchemaSpecification({ entityTypes: [{ name: typeName, fields: [{ name: 'field', type: FieldType.String }] }], }); const { schemaSpecification } = firstUpdateResult.valueOrThrow(); // Delete the type const secondUpdateResult = await client.updateSchemaSpecification({ migrations: [ { version: schemaSpecification.version + 1, actions: [{ action: 'deleteType', entityType: typeName }], }, ], }); return ok(secondUpdateResult.valueOrThrow().schemaSpecification); }); const schemaSpec = result.valueOrThrow(); const entityType = schemaSpec.entityTypes.find((et) => et.name === typeName); assertEquals(entityType, undefined); } async function updateSchemaSpecification_deleteTypeOnTwoEntityTypes({ clientProvider, }) { const client = clientProvider.dossierClient(); const typeName1 = `MigrationEntity${new Date().getTime()}`; const typeName2 = `${typeName1}2`; // Lock since the version needs to be consecutive const result = await withSchemaAdvisoryLock(client, async () => { // First add new types const firstUpdateResult = await client.updateSchemaSpecification({ entityTypes: [ { name: typeName1, fields: [{ name: 'field', type: FieldType.String }] }, { name: typeName2, fields: [{ name: 'field', type: FieldType.Stri