Skip to content

Commit 9d216cd

Browse files
authored
[SOR] use initialNamespaces when checking for conflict for create and bulkCreate (#111023)
* use initialNamespaces when checking for conflict * nits
1 parent 8f72897 commit 9d216cd

File tree

2 files changed

+104
-11
lines changed

2 files changed

+104
-11
lines changed

src/core/server/saved_objects/service/lib/repository.test.js

Lines changed: 78 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,16 @@ describe('SavedObjectsRepository', () => {
197197
{ type, id, references, namespace: objectNamespace, originId },
198198
namespace
199199
) => {
200-
const namespaceId = objectNamespace === 'default' ? undefined : objectNamespace ?? namespace;
200+
let namespaces;
201+
if (objectNamespace) {
202+
namespaces = [objectNamespace];
203+
} else if (namespace) {
204+
namespaces = Array.isArray(namespace) ? namespace : [namespace];
205+
} else {
206+
namespaces = ['default'];
207+
}
208+
const namespaceId = namespaces[0] === 'default' ? undefined : namespaces[0];
209+
201210
return {
202211
// NOTE: Elasticsearch returns more fields (_index, _type) but the SavedObjectsRepository method ignores these
203212
found: true,
@@ -207,7 +216,7 @@ describe('SavedObjectsRepository', () => {
207216
...mockVersionProps,
208217
_source: {
209218
...(registry.isSingleNamespace(type) && { namespace: namespaceId }),
210-
...(registry.isMultiNamespace(type) && { namespaces: [namespaceId ?? 'default'] }),
219+
...(registry.isMultiNamespace(type) && { namespaces }),
211220
...(originId && { originId }),
212221
type,
213222
[type]: { title: 'Testing' },
@@ -219,7 +228,9 @@ describe('SavedObjectsRepository', () => {
219228
};
220229

221230
const getMockMgetResponse = (objects, namespace) => ({
222-
docs: objects.map((obj) => (obj.found === false ? obj : getMockGetResponse(obj, namespace))),
231+
docs: objects.map((obj) =>
232+
obj.found === false ? obj : getMockGetResponse(obj, obj.initialNamespaces ?? namespace)
233+
),
223234
});
224235

225236
expect.extend({
@@ -797,6 +808,54 @@ describe('SavedObjectsRepository', () => {
797808
});
798809
});
799810

811+
it(`returns error when there is an unresolvable conflict with an existing multi-namespace saved object when using initialNamespaces (get)`, async () => {
812+
const obj = {
813+
...obj3,
814+
type: MULTI_NAMESPACE_TYPE,
815+
initialNamespaces: ['foo-namespace', 'default'],
816+
};
817+
const response1 = {
818+
status: 200,
819+
docs: [
820+
{
821+
found: true,
822+
_source: {
823+
type: obj.type,
824+
namespaces: ['bar-namespace'],
825+
},
826+
},
827+
],
828+
};
829+
client.mget.mockResolvedValueOnce(
830+
elasticsearchClientMock.createSuccessTransportRequestPromise(response1)
831+
);
832+
const response2 = getMockBulkCreateResponse([obj1, obj2]);
833+
client.bulk.mockResolvedValueOnce(
834+
elasticsearchClientMock.createSuccessTransportRequestPromise(response2)
835+
);
836+
837+
const options = { overwrite: true };
838+
const result = await savedObjectsRepository.bulkCreate([obj1, obj, obj2], options);
839+
840+
expect(client.bulk).toHaveBeenCalled();
841+
expect(client.mget).toHaveBeenCalled();
842+
843+
const body1 = { docs: [expect.objectContaining({ _id: `${obj.type}:${obj.id}` })] };
844+
expect(client.mget).toHaveBeenCalledWith(
845+
expect.objectContaining({ body: body1 }),
846+
expect.anything()
847+
);
848+
const body2 = [...expectObjArgs(obj1), ...expectObjArgs(obj2)];
849+
expect(client.bulk).toHaveBeenCalledWith(
850+
expect.objectContaining({ body: body2 }),
851+
expect.anything()
852+
);
853+
const expectedError = expectErrorConflict(obj, { metadata: { isNotOverwritable: true } });
854+
expect(result).toEqual({
855+
saved_objects: [expectSuccess(obj1), expectedError, expectSuccess(obj2)],
856+
});
857+
});
858+
800859
it(`returns bulk error`, async () => {
801860
const expectedErrorResult = { type: obj3.type, id: obj3.id, error: 'Oh no, a bulk error!' };
802861
await bulkCreateError(obj3, true, expectedErrorResult);
@@ -2197,6 +2256,22 @@ describe('SavedObjectsRepository', () => {
21972256
expect(client.get).toHaveBeenCalled();
21982257
});
21992258

2259+
it(`throws when there is an unresolvable conflict with an existing multi-namespace saved object when using initialNamespaces (get)`, async () => {
2260+
const response = getMockGetResponse({ type: MULTI_NAMESPACE_ISOLATED_TYPE, id }, namespace);
2261+
client.get.mockResolvedValueOnce(
2262+
elasticsearchClientMock.createSuccessTransportRequestPromise(response)
2263+
);
2264+
await expect(
2265+
savedObjectsRepository.create(MULTI_NAMESPACE_TYPE, attributes, {
2266+
id,
2267+
overwrite: true,
2268+
initialNamespaces: ['bar-ns', 'dolly-ns'],
2269+
namespace,
2270+
})
2271+
).rejects.toThrowError(createConflictError(MULTI_NAMESPACE_TYPE, id));
2272+
expect(client.get).toHaveBeenCalled();
2273+
});
2274+
22002275
it.todo(`throws when automatic index creation fails`);
22012276

22022277
it.todo(`throws when an unexpected failure occurs`);

src/core/server/saved_objects/service/lib/repository.ts

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -308,8 +308,12 @@ export class SavedObjectsRepository {
308308
if (id && overwrite) {
309309
// we will overwrite a multi-namespace saved object if it exists; if that happens, ensure we preserve its included namespaces
310310
// note: this check throws an error if the object is found but does not exist in this namespace
311-
const existingNamespaces = await this.preflightGetNamespaces(type, id, namespace);
312-
savedObjectNamespaces = initialNamespaces || existingNamespaces;
311+
savedObjectNamespaces = await this.preflightGetNamespaces(
312+
type,
313+
id,
314+
namespace,
315+
initialNamespaces
316+
);
313317
} else {
314318
savedObjectNamespaces = initialNamespaces || getSavedObjectNamespaces(namespace);
315319
}
@@ -455,8 +459,14 @@ export class SavedObjectsRepository {
455459
const indexFound = bulkGetResponse?.statusCode !== 404;
456460
const actualResult = indexFound ? bulkGetResponse?.body.docs[esRequestIndex] : undefined;
457461
const docFound = indexFound && actualResult?.found === true;
458-
// @ts-expect-error MultiGetHit._source is optional
459-
if (docFound && !this.rawDocExistsInNamespace(actualResult!, namespace)) {
462+
if (
463+
docFound &&
464+
!this.rawDocExistsInNamespaces(
465+
// @ts-expect-error MultiGetHit._source is optional
466+
actualResult!,
467+
initialNamespaces ?? [SavedObjectsUtils.namespaceIdToString(namespace)]
468+
)
469+
) {
460470
const { id, type } = object;
461471
return {
462472
tag: 'Left' as 'Left',
@@ -2140,12 +2150,18 @@ export class SavedObjectsRepository {
21402150
* @param type The type of the saved object.
21412151
* @param id The ID of the saved object.
21422152
* @param namespace The target namespace.
2153+
* @param initialNamespaces The target namespace(s) we intend to create the object in, if specified.
21432154
* @returns Array of namespaces that this saved object currently includes, or (if the object does not exist yet) the namespaces that a
21442155
* newly-created object will include. Value may be undefined if an existing saved object has no namespaces attribute; this should not
21452156
* happen in normal operations, but it is possible if the Elasticsearch document is manually modified.
21462157
* @throws Will throw an error if the saved object exists and it does not include the target namespace.
21472158
*/
2148-
private async preflightGetNamespaces(type: string, id: string, namespace?: string) {
2159+
private async preflightGetNamespaces(
2160+
type: string,
2161+
id: string,
2162+
namespace: string | undefined,
2163+
initialNamespaces?: string[]
2164+
) {
21492165
if (!this._registry.isMultiNamespace(type)) {
21502166
throw new Error(`Cannot make preflight get request for non-multi-namespace type '${type}'.`);
21512167
}
@@ -2160,17 +2176,19 @@ export class SavedObjectsRepository {
21602176
}
21612177
);
21622178

2179+
const namespaces = initialNamespaces ?? [SavedObjectsUtils.namespaceIdToString(namespace)];
2180+
21632181
const indexFound = statusCode !== 404;
21642182
if (indexFound && isFoundGetResponse(body)) {
2165-
if (!this.rawDocExistsInNamespace(body, namespace)) {
2183+
if (!this.rawDocExistsInNamespaces(body, namespaces)) {
21662184
throw SavedObjectsErrorHelpers.createConflictError(type, id);
21672185
}
2168-
return getSavedObjectNamespaces(namespace, body);
2186+
return initialNamespaces ?? getSavedObjectNamespaces(namespace, body);
21692187
} else if (isNotFoundFromUnsupportedServer({ statusCode, headers })) {
21702188
// checking if the 404 is from Elasticsearch
21712189
throw SavedObjectsErrorHelpers.createGenericNotFoundEsUnavailableError(type, id);
21722190
}
2173-
return getSavedObjectNamespaces(namespace);
2191+
return initialNamespaces ?? getSavedObjectNamespaces(namespace);
21742192
}
21752193

21762194
/**

0 commit comments

Comments
 (0)