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:
parent
de4044884b
commit
22b6f1af99
2 changed files with 297 additions and 104 deletions
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Add table
Reference in a new issue