0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-04-15 03:01:37 -05:00

Improved behaviour of default and all handlers

refs: https://github.com/TryGhost/Toolbox/issues/245

- .all methods are fallback serializers not to be run as well as a custom serializer
- The default serializer is also a fallback
- The "All" file with before and after are global hooks that _always_ get run as well as other serializers
- There's a lot of room for further improvement here especially with naming but this logic makes more sense
  for the usecases AND doesn't affect v2 & v3 etc. We can do another pass after 5.0
This commit is contained in:
Hannah Wolfe 2022-03-27 12:44:55 +01:00
parent de4044884b
commit 22b6f1af99
2 changed files with 297 additions and 104 deletions

View file

@ -67,6 +67,19 @@ module.exports.input = (apiConfig, apiSerializers, frame) => {
return sequence(tasks);
};
const getBestMatchSerializer = function (apiSerializers, docName, method) {
if (apiSerializers[docName] && apiSerializers[docName][method]) {
debug(`Calling ${docName}.${method}`);
return apiSerializers[docName][method].bind(apiSerializers[docName]);
} else if (apiSerializers[docName] && apiSerializers[docName].all) {
debug(`Calling ${docName}.all`);
return apiSerializers[docName].all.bind(apiSerializers[docName]);
}
debug(`Returning as-is`);
return false;
};
/**
* @description Shared output serialization handler.
*
@ -101,33 +114,19 @@ module.exports.output = (response = {}, apiConfig, apiSerializers, frame) => {
});
}
// CASE: custom serializer exists
if (apiSerializers[apiConfig.docName]) {
if (apiSerializers[apiConfig.docName].all) {
tasks.push(function serialiseCustomAll() {
return apiSerializers[apiConfig.docName].all(response, apiConfig, frame);
});
}
const customSerializer = getBestMatchSerializer(apiSerializers, apiConfig.docName, apiConfig.method);
const defaultSerializer = getBestMatchSerializer(apiSerializers, 'default', apiConfig.method);
if (apiSerializers[apiConfig.docName][apiConfig.method]) {
tasks.push(function serialiseCustomMethod() {
return apiSerializers[apiConfig.docName][apiConfig.method](response, apiConfig, frame);
});
}
// CASE: Fall back to default serializer
} else if (apiSerializers.default) {
if (apiSerializers.default.all) {
tasks.push(function serializeDefaultAll() {
return apiSerializers.default.all(response, apiConfig, frame);
});
}
if (apiSerializers.default[apiConfig.method]) {
tasks.push(function serializeDefaultMethod() {
return apiSerializers.default[apiConfig.method](response, apiConfig, frame);
});
}
if (customSerializer) {
// CASE: custom serializer exists
tasks.push(function doCustomSerializer() {
return customSerializer(response, apiConfig, frame);
});
} else if (defaultSerializer) {
// CASE: Fall back to default serializer
tasks.push(function doDefaultSerializer() {
return defaultSerializer(response, apiConfig, frame);
});
}
if (apiSerializers.all && apiSerializers.all.after) {

View file

@ -5,7 +5,7 @@ const sinon = require('sinon');
const shared = require('../../../../../core/server/api/shared');
describe('Unit: api/shared/serializers/handle', function () {
beforeEach(function () {
afterEach(function () {
sinon.restore();
});
@ -95,6 +95,17 @@ describe('Unit: api/shared/serializers/handle', function () {
});
describe('output', function () {
let apiSerializers,
response,
apiConfig,
frame;
beforeEach(function () {
response = [];
apiConfig = {docName: 'posts', method: 'add'};
frame = {};
});
it('no models passed', function () {
return shared.serializers.handle.output(null, {}, {}, {});
});
@ -115,105 +126,288 @@ describe('Unit: api/shared/serializers/handle', function () {
});
});
it('ensure custom api Serializers are called correctly', function () {
const apiSerializers = {
posts: {
add: sinon.stub().resolves()
},
users: {
add: sinon.stub().resolves()
}
};
describe('Specific serializers only', function () {
beforeEach(function () {
apiSerializers = {
posts: {
add: sinon.stub().resolves()
},
users: {
add: sinon.stub().resolves()
}
};
});
const response = [];
const apiConfig = {docName: 'posts', method: 'add'};
const frame = {};
it('correct custom serializer is called', function () {
return shared.serializers.handle.output(response, apiConfig, apiSerializers, frame)
.then(() => {
sinon.assert.calledOnceWithExactly(apiSerializers.posts.add, response, apiConfig, frame);
sinon.assert.notCalled(apiSerializers.users.add);
});
});
return shared.serializers.handle.output(response, apiConfig, apiSerializers, frame)
.then(() => {
sinon.assert.calledOnceWithExactly(apiSerializers.posts.add, response, apiConfig, frame);
sinon.assert.notCalled(apiSerializers.users.add);
});
it('no serializer called if there is no match', function () {
apiConfig = {docName: 'posts', method: 'idontexist'};
return shared.serializers.handle.output(response, apiConfig, apiSerializers, frame)
.then(() => {
sinon.assert.notCalled(apiSerializers.posts.add);
sinon.assert.notCalled(apiSerializers.users.add);
});
});
});
it('ensure "all" serializers are called correctly', function () {
const apiSerializers = {
all: {
after: sinon.stub().resolves(),
before: sinon.stub().resolves()
describe('Custom and global (all) serializers', function () {
beforeEach(function () {
apiSerializers = {
all: {
after: sinon.stub().resolves(),
before: sinon.stub().resolves()
},
default: {
add: sinon.stub().resolves(),
all: sinon.stub().resolves()
},
posts: {
add: sinon.stub().resolves(),
all: sinon.stub().resolves()
}
};
});
},
posts: {
add: sinon.stub().resolves(),
all: sinon.stub().resolves()
}
};
it('calls custom serializer if one exists', function () {
const stubsToCheck = [
apiSerializers.all.before,
apiSerializers.posts.add
];
const response = [];
const apiConfig = {docName: 'posts', method: 'add'};
const frame = {};
return shared.serializers.handle.output(response, apiConfig, apiSerializers, frame)
.then(() => {
stubsToCheck.forEach((stub) => {
sinon.assert.calledOnceWithExactly(stub, response, apiConfig, frame);
});
const stubsToCheck = [
apiSerializers.all.before,
apiSerializers.posts.add,
apiSerializers.posts.all
];
// After has a different call signature... is this a intentional?
sinon.assert.calledOnceWithExactly(apiSerializers.all.after, apiConfig, frame);
return shared.serializers.handle.output(response, apiConfig, apiSerializers, frame)
.then(() => {
stubsToCheck.forEach((stub) => {
sinon.assert.calledOnceWithExactly(stub, response, apiConfig, frame);
sinon.assert.callOrder(apiSerializers.all.before, apiSerializers.posts.add, apiSerializers.all.after);
sinon.assert.notCalled(apiSerializers.posts.all);
});
});
// After has a different call signature... is this a intentional?
sinon.assert.calledOnceWithExactly(apiSerializers.all.after, apiConfig, frame);
it('calls all serializer if custom one does not exist', function () {
apiConfig = {docName: 'posts', method: 'idontexist'};
sinon.assert.callOrder(apiSerializers.all.before, apiSerializers.posts.all, apiSerializers.posts.add, apiSerializers.all.after);
const stubsToCheck = [
apiSerializers.all.before,
apiSerializers.posts.all
];
sinon.assert.notCalled(apiSerializers.default.add);
sinon.assert.notCalled(apiSerializers.default.all);
});
return shared.serializers.handle.output(response, apiConfig, apiSerializers, frame)
.then(() => {
stubsToCheck.forEach((stub) => {
sinon.assert.calledOnceWithExactly(stub, response, apiConfig, frame);
});
// After has a different call signature... is this a intentional?
sinon.assert.calledOnceWithExactly(apiSerializers.all.after, apiConfig, frame);
sinon.assert.callOrder(apiSerializers.all.before, apiSerializers.posts.all, apiSerializers.all.after);
sinon.assert.notCalled(apiSerializers.posts.add);
});
});
});
it('correctly calls default serializer when no custom one is set', function () {
const apiSerializers = {
all: {
after: sinon.stub().resolves(),
before: sinon.stub().resolves()
describe('Custom, default and global (all) serializers with no custom fallback', function () {
beforeEach(function () {
apiSerializers = {
all: {
after: sinon.stub().resolves(),
before: sinon.stub().resolves()
},
default: {
add: sinon.stub().resolves(),
all: sinon.stub().resolves()
}
};
},
default: {
add: sinon.stub().resolves(),
all: sinon.stub().resolves()
const response = [];
const apiConfig = {docName: 'posts', method: 'add'};
const frame = {};
},
posts: {
add: sinon.stub().resolves()
}
};
});
const stubsToCheck = [
apiSerializers.all.before,
apiSerializers.default.all,
apiSerializers.default.add
];
it('uses best match serializer when custom match exists', function () {
const stubsToCheck = [
apiSerializers.all.before,
apiSerializers.posts.add
];
return shared.serializers.handle.output(response, apiConfig, apiSerializers, frame)
.then(() => {
stubsToCheck.forEach((stub) => {
sinon.assert.calledOnceWithExactly(stub, response, apiConfig, frame);
return shared.serializers.handle.output(response, apiConfig, apiSerializers, frame)
.then(() => {
stubsToCheck.forEach((stub) => {
sinon.assert.calledOnceWithExactly(stub, response, apiConfig, frame);
});
// After has a different call signature... is this a intentional?
sinon.assert.calledOnceWithExactly(apiSerializers.all.after, apiConfig, frame);
sinon.assert.callOrder(apiSerializers.all.before, apiSerializers.posts.add, apiSerializers.all.after);
sinon.assert.notCalled(apiSerializers.default.add);
sinon.assert.notCalled(apiSerializers.default.all);
});
});
// After has a different call signature... is this a intentional?
sinon.assert.calledOnceWithExactly(apiSerializers.all.after, apiConfig, frame);
it('uses nearest fallback serializer when custom match does not exist', function () {
apiConfig = {docName: 'posts', method: 'idontexist'};
sinon.assert.callOrder(apiSerializers.all.before, apiSerializers.default.all, apiSerializers.default.add, apiSerializers.all.after);
});
const stubsToCheck = [
apiSerializers.all.before,
apiSerializers.default.all
];
return shared.serializers.handle.output(response, apiConfig, apiSerializers, frame)
.then(() => {
stubsToCheck.forEach((stub) => {
sinon.assert.calledOnceWithExactly(stub, response, apiConfig, frame);
});
// After has a different call signature... is this a intentional?
sinon.assert.calledOnceWithExactly(apiSerializers.all.after, apiConfig, frame);
sinon.assert.callOrder(apiSerializers.all.before, apiSerializers.default.all, apiSerializers.all.after);
sinon.assert.notCalled(apiSerializers.posts.add);
sinon.assert.notCalled(apiSerializers.default.add);
});
});
});
describe('Custom, default and global (all) serializers with custom fallback', function () {
beforeEach(function () {
apiSerializers = {
all: {
after: sinon.stub().resolves(),
before: sinon.stub().resolves()
},
default: {
add: sinon.stub().resolves(),
all: sinon.stub().resolves()
},
posts: {
add: sinon.stub().resolves(),
all: sinon.stub().resolves()
}
};
});
it('uses best match serializer when custom match exists', function () {
const stubsToCheck = [
apiSerializers.all.before,
apiSerializers.posts.add
];
return shared.serializers.handle.output(response, apiConfig, apiSerializers, frame)
.then(() => {
stubsToCheck.forEach((stub) => {
sinon.assert.calledOnceWithExactly(stub, response, apiConfig, frame);
});
// After has a different call signature... is this a intentional?
sinon.assert.calledOnceWithExactly(apiSerializers.all.after, apiConfig, frame);
sinon.assert.callOrder(apiSerializers.all.before, apiSerializers.posts.add, apiSerializers.all.after);
sinon.assert.notCalled(apiSerializers.posts.all);
sinon.assert.notCalled(apiSerializers.default.add);
sinon.assert.notCalled(apiSerializers.default.all);
});
});
it('uses nearest fallback serializer when custom match does not exist', function () {
apiConfig = {docName: 'posts', method: 'idontexist'};
const stubsToCheck = [
apiSerializers.all.before,
apiSerializers.posts.all
];
return shared.serializers.handle.output(response, apiConfig, apiSerializers, frame)
.then(() => {
stubsToCheck.forEach((stub) => {
sinon.assert.calledOnceWithExactly(stub, response, apiConfig, frame);
});
// After has a different call signature... is this a intentional?
sinon.assert.calledOnceWithExactly(apiSerializers.all.after, apiConfig, frame);
sinon.assert.callOrder(apiSerializers.all.before, apiSerializers.posts.all, apiSerializers.all.after);
sinon.assert.notCalled(apiSerializers.posts.add);
sinon.assert.notCalled(apiSerializers.default.add);
sinon.assert.notCalled(apiSerializers.default.all);
});
});
});
describe('Default and global (all) serializers work together correctly', function () {
beforeEach(function () {
apiSerializers = {
all: {
after: sinon.stub().resolves(),
before: sinon.stub().resolves()
},
default: {
add: sinon.stub().resolves(),
all: sinon.stub().resolves()
}
};
});
it('correctly calls default serializer when no custom one is set', function () {
const stubsToCheck = [
apiSerializers.all.before,
apiSerializers.default.add
];
return shared.serializers.handle.output(response, apiConfig, apiSerializers, frame)
.then(() => {
stubsToCheck.forEach((stub) => {
sinon.assert.calledOnceWithExactly(stub, response, apiConfig, frame);
});
// After has a different call signature... is this a intentional?
sinon.assert.calledOnceWithExactly(apiSerializers.all.after, apiConfig, frame);
sinon.assert.callOrder(apiSerializers.all.before, apiSerializers.default.add, apiSerializers.all.after);
sinon.assert.notCalled(apiSerializers.default.all);
});
});
it('correctly uses fallback serializer when there is no default match', function () {
apiConfig = {docName: 'posts', method: 'idontexist'};
const stubsToCheck = [
apiSerializers.all.before,
apiSerializers.default.all
];
return shared.serializers.handle.output(response, apiConfig, apiSerializers, frame)
.then(() => {
stubsToCheck.forEach((stub) => {
sinon.assert.calledOnceWithExactly(stub, response, apiConfig, frame);
});
// After has a different call signature... is this a intentional?
sinon.assert.calledOnceWithExactly(apiSerializers.all.after, apiConfig, frame);
sinon.assert.callOrder(apiSerializers.all.before, apiSerializers.default.all, apiSerializers.all.after);
sinon.assert.notCalled(apiSerializers.default.add);
});
});
});
});
});