diff --git a/src/lib/types/definition.js b/src/lib/types/definition.js index 9a27599..ade70ce 100644 --- a/src/lib/types/definition.js +++ b/src/lib/types/definition.js @@ -281,8 +281,17 @@ export class SchemaDefinition { // Merge all properties from s into t, joining arrays and objects for (let skey of Object.keys(s)) { const tkey = skey.toLowerCase(); - if (Array.isArray(t[tkey]) && Array.isArray(s[skey])) t[tkey].push(...s[skey]); + + // If source is an array... + if (Array.isArray(s[skey])) { + // ...and target is an array, merge them... + if (Array.isArray(t[tkey])) t[tkey].push(...s[skey]); + // ...otherwise, make target an array + else t[tkey] = [...s[skey]]; + } + // If source is a primitive value, copy it else if (s[skey] !== Object(s[skey])) t[tkey] = s[skey]; + // Finally, if source is neither an array nor primitive, merge it else t[tkey] = merge(t[tkey] ?? {}, s[skey]); } @@ -297,7 +306,7 @@ export class SchemaDefinition { // Coerce the mixed value, using only namespaced attributes for this extension target[name] = attribute.coerce(mixedSource, direction, basepath, [Object.keys(filter ?? {}) .filter(k => k.startsWith(`${name}:`)) - .reduce((res, key) => (((res[key.replace(`${name}:`, "")] = filter[key]) || true) && res), {}) + .reduce((res, key) => Object.assign(res, {[key.replace(`${name}:`, "")]: filter[key]}), {}) ]); } catch (ex) { // Rethrow exception with added context diff --git a/test/hooks/schemas.js b/test/hooks/schemas.js index d44c04d..3d93fed 100644 --- a/test/hooks/schemas.js +++ b/test/hooks/schemas.js @@ -107,10 +107,34 @@ export default { } }); + // https://github.com/scimmyjs/scimmy/issues/12 + it("should coerce complex multi-value attributes in schema extensions", async () => { + const {constructor = {}} = await fixtures; + const subAttribute = new Attribute("string", "name"); + const attribute = new Attribute("complex", "agencies", {multiValued: true}, [subAttribute]); + const extension = new SchemaDefinition("Extension", "urn:ietf:params:scim:schemas:Extension", "", [attribute]); + const source = {...constructor, [extension.id]: {[attribute.name]: [{[subAttribute.name]: "value"}]}}; + + try { + // Add the extension to the target + TargetSchema.extend(extension); + + // Construct an instance to test against, and get actual value for comparison + const instance = new TargetSchema(source); + const actual = JSON.parse(JSON.stringify(instance[extension.id][attribute.name])); + + assert.deepStrictEqual(actual, source[extension.id][attribute.name], + "Schema instance did not coerce complex multi-value attributes from schema extension"); + } finally { + // Remove the extension so it doesn't interfere later + TargetSchema.truncate("urn:ietf:params:scim:schemas:Extension"); + } + }); + it("should expect errors in extension schema coercion to be rethrown as SCIMErrors", async () => { + const {constructor = {}} = await fixtures; const attributes = [new Attribute("string", "testValue")]; const extension = new SchemaDefinition("Extension", "urn:ietf:params:scim:schemas:Extension", "", attributes); - const {constructor = {}} = await fixtures; const source = {...constructor, [`${extension.id}:testValue`]: "a string"}; try { @@ -145,8 +169,8 @@ export default { ]; // Get the extension and the source data ready - const extension = new SchemaDefinition("Extension", "urn:ietf:params:scim:schemas:Extension", "", attributes); const {constructor = {}} = await fixtures; + const extension = new SchemaDefinition("Extension", "urn:ietf:params:scim:schemas:Extension", "", attributes); const source = { ...constructor, [`${extension.id}:testValue.stringValue`]: "a string",