@dossierhq/integration-test
Version:
Integration test to ensure that different Dossier database adapters work as expected.
1,002 lines • 95 kB
JavaScript
/// <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