feat: Add GraphQL query cloudConfig to retrieve and mutation updateCloudConfig to update Cloud Config (#9947)
This commit is contained in:
@@ -7080,6 +7080,284 @@ describe('ParseGraphQLServer', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("Config Queries", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
// Setup initial config data
|
||||||
|
await Parse.Config.save(
|
||||||
|
{ publicParam: 'publicValue', privateParam: 'privateValue' },
|
||||||
|
{ privateParam: true },
|
||||||
|
{ useMasterKey: true }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return the config value for a specific parameter", async () => {
|
||||||
|
const query = gql`
|
||||||
|
query cloudConfig($paramName: String!) {
|
||||||
|
cloudConfig(paramName: $paramName) {
|
||||||
|
value
|
||||||
|
isMasterKeyOnly
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const result = await apolloClient.query({
|
||||||
|
query,
|
||||||
|
variables: { paramName: 'publicParam' },
|
||||||
|
context: {
|
||||||
|
headers: {
|
||||||
|
'X-Parse-Master-Key': 'test',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.errors).toBeUndefined();
|
||||||
|
expect(result.data.cloudConfig.value).toEqual('publicValue');
|
||||||
|
expect(result.data.cloudConfig.isMasterKeyOnly).toEqual(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return null for non-existent parameter", async () => {
|
||||||
|
const query = gql`
|
||||||
|
query cloudConfig($paramName: String!) {
|
||||||
|
cloudConfig(paramName: $paramName) {
|
||||||
|
value
|
||||||
|
isMasterKeyOnly
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const result = await apolloClient.query({
|
||||||
|
query,
|
||||||
|
variables: { paramName: 'nonExistentParam' },
|
||||||
|
context: {
|
||||||
|
headers: {
|
||||||
|
'X-Parse-Master-Key': 'test',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.errors).toBeUndefined();
|
||||||
|
expect(result.data.cloudConfig.value).toBeNull();
|
||||||
|
expect(result.data.cloudConfig.isMasterKeyOnly).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Config Mutations", () => {
|
||||||
|
it("should update a config value using mutation and retrieve it with query", async () => {
|
||||||
|
const mutation = gql`
|
||||||
|
mutation updateCloudConfig($input: UpdateCloudConfigInput!) {
|
||||||
|
updateCloudConfig(input: $input) {
|
||||||
|
clientMutationId
|
||||||
|
cloudConfig {
|
||||||
|
value
|
||||||
|
isMasterKeyOnly
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const query = gql`
|
||||||
|
query cloudConfig($paramName: String!) {
|
||||||
|
cloudConfig(paramName: $paramName) {
|
||||||
|
value
|
||||||
|
isMasterKeyOnly
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const mutationResult = await apolloClient.mutate({
|
||||||
|
mutation,
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
clientMutationId: 'test-mutation-id',
|
||||||
|
paramName: 'testParam',
|
||||||
|
value: 'testValue',
|
||||||
|
isMasterKeyOnly: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
context: {
|
||||||
|
headers: {
|
||||||
|
'X-Parse-Master-Key': 'test',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mutationResult.errors).toBeUndefined();
|
||||||
|
expect(mutationResult.data.updateCloudConfig.cloudConfig.value).toEqual('testValue');
|
||||||
|
expect(mutationResult.data.updateCloudConfig.cloudConfig.isMasterKeyOnly).toEqual(false);
|
||||||
|
|
||||||
|
const queryResult = await apolloClient.query({
|
||||||
|
query,
|
||||||
|
variables: { paramName: 'testParam' },
|
||||||
|
context: {
|
||||||
|
headers: {
|
||||||
|
'X-Parse-Master-Key': 'test',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(queryResult.errors).toBeUndefined();
|
||||||
|
expect(queryResult.data.cloudConfig.value).toEqual('testValue');
|
||||||
|
expect(queryResult.data.cloudConfig.isMasterKeyOnly).toEqual(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should update a config value with isMasterKeyOnly set to true", async () => {
|
||||||
|
const mutation = gql`
|
||||||
|
mutation updateCloudConfig($input: UpdateCloudConfigInput!) {
|
||||||
|
updateCloudConfig(input: $input) {
|
||||||
|
clientMutationId
|
||||||
|
cloudConfig {
|
||||||
|
value
|
||||||
|
isMasterKeyOnly
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const query = gql`
|
||||||
|
query cloudConfig($paramName: String!) {
|
||||||
|
cloudConfig(paramName: $paramName) {
|
||||||
|
value
|
||||||
|
isMasterKeyOnly
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const mutationResult = await apolloClient.mutate({
|
||||||
|
mutation,
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
clientMutationId: 'test-mutation-id-2',
|
||||||
|
paramName: 'privateTestParam',
|
||||||
|
value: 'privateValue',
|
||||||
|
isMasterKeyOnly: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
context: {
|
||||||
|
headers: {
|
||||||
|
'X-Parse-Master-Key': 'test',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mutationResult.errors).toBeUndefined();
|
||||||
|
expect(mutationResult.data.updateCloudConfig.cloudConfig.value).toEqual('privateValue');
|
||||||
|
expect(mutationResult.data.updateCloudConfig.cloudConfig.isMasterKeyOnly).toEqual(true);
|
||||||
|
|
||||||
|
const queryResult = await apolloClient.query({
|
||||||
|
query,
|
||||||
|
variables: { paramName: 'privateTestParam' },
|
||||||
|
context: {
|
||||||
|
headers: {
|
||||||
|
'X-Parse-Master-Key': 'test',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(queryResult.errors).toBeUndefined();
|
||||||
|
expect(queryResult.data.cloudConfig.value).toEqual('privateValue');
|
||||||
|
expect(queryResult.data.cloudConfig.isMasterKeyOnly).toEqual(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should update an existing config value", async () => {
|
||||||
|
await Parse.Config.save(
|
||||||
|
{ existingParam: 'initialValue' },
|
||||||
|
{},
|
||||||
|
{ useMasterKey: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
const mutation = gql`
|
||||||
|
mutation updateCloudConfig($input: UpdateCloudConfigInput!) {
|
||||||
|
updateCloudConfig(input: $input) {
|
||||||
|
clientMutationId
|
||||||
|
cloudConfig {
|
||||||
|
value
|
||||||
|
isMasterKeyOnly
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const query = gql`
|
||||||
|
query cloudConfig($paramName: String!) {
|
||||||
|
cloudConfig(paramName: $paramName) {
|
||||||
|
value
|
||||||
|
isMasterKeyOnly
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const mutationResult = await apolloClient.mutate({
|
||||||
|
mutation,
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
clientMutationId: 'test-mutation-id-3',
|
||||||
|
paramName: 'existingParam',
|
||||||
|
value: 'updatedValue',
|
||||||
|
isMasterKeyOnly: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
context: {
|
||||||
|
headers: {
|
||||||
|
'X-Parse-Master-Key': 'test',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mutationResult.errors).toBeUndefined();
|
||||||
|
expect(mutationResult.data.updateCloudConfig.cloudConfig.value).toEqual('updatedValue');
|
||||||
|
|
||||||
|
const queryResult = await apolloClient.query({
|
||||||
|
query,
|
||||||
|
variables: { paramName: 'existingParam' },
|
||||||
|
context: {
|
||||||
|
headers: {
|
||||||
|
'X-Parse-Master-Key': 'test',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(queryResult.errors).toBeUndefined();
|
||||||
|
expect(queryResult.data.cloudConfig.value).toEqual('updatedValue');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should require master key to update config", async () => {
|
||||||
|
const mutation = gql`
|
||||||
|
mutation updateCloudConfig($input: UpdateCloudConfigInput!) {
|
||||||
|
updateCloudConfig(input: $input) {
|
||||||
|
clientMutationId
|
||||||
|
cloudConfig {
|
||||||
|
value
|
||||||
|
isMasterKeyOnly
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await apolloClient.mutate({
|
||||||
|
mutation,
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
clientMutationId: 'test-mutation-id-4',
|
||||||
|
paramName: 'testParam',
|
||||||
|
value: 'testValue',
|
||||||
|
isMasterKeyOnly: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
context: {
|
||||||
|
headers: {
|
||||||
|
'X-Parse-Application-Id': 'test',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
fail('Should have thrown an error');
|
||||||
|
} catch (error) {
|
||||||
|
expect(error.graphQLErrors).toBeDefined();
|
||||||
|
expect(error.graphQLErrors[0].message).toContain('Permission denied');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
describe('Users Queries', () => {
|
describe('Users Queries', () => {
|
||||||
it('should return current logged user', async () => {
|
it('should return current logged user', async () => {
|
||||||
const userName = 'user1',
|
const userName = 'user1',
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ const RESERVED_GRAPHQL_TYPE_NAMES = [
|
|||||||
'DeleteClassPayload',
|
'DeleteClassPayload',
|
||||||
'PageInfo',
|
'PageInfo',
|
||||||
];
|
];
|
||||||
const RESERVED_GRAPHQL_QUERY_NAMES = ['health', 'viewer', 'class', 'classes'];
|
const RESERVED_GRAPHQL_QUERY_NAMES = ['health', 'viewer', 'class', 'classes', 'cloudConfig'];
|
||||||
const RESERVED_GRAPHQL_MUTATION_NAMES = [
|
const RESERVED_GRAPHQL_MUTATION_NAMES = [
|
||||||
'signUp',
|
'signUp',
|
||||||
'logIn',
|
'logIn',
|
||||||
@@ -59,6 +59,7 @@ const RESERVED_GRAPHQL_MUTATION_NAMES = [
|
|||||||
'createClass',
|
'createClass',
|
||||||
'updateClass',
|
'updateClass',
|
||||||
'deleteClass',
|
'deleteClass',
|
||||||
|
'updateCloudConfig',
|
||||||
];
|
];
|
||||||
|
|
||||||
class ParseGraphQLSchema {
|
class ParseGraphQLSchema {
|
||||||
@@ -118,6 +119,7 @@ class ParseGraphQLSchema {
|
|||||||
this.functionNamesString = functionNamesString;
|
this.functionNamesString = functionNamesString;
|
||||||
this.parseClassTypes = {};
|
this.parseClassTypes = {};
|
||||||
this.viewerType = null;
|
this.viewerType = null;
|
||||||
|
this.cloudConfigType = null;
|
||||||
this.graphQLAutoSchema = null;
|
this.graphQLAutoSchema = null;
|
||||||
this.graphQLSchema = null;
|
this.graphQLSchema = null;
|
||||||
this.graphQLTypes = [];
|
this.graphQLTypes = [];
|
||||||
|
|||||||
76
src/GraphQL/loaders/configMutations.js
Normal file
76
src/GraphQL/loaders/configMutations.js
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import { GraphQLNonNull, GraphQLString, GraphQLBoolean } from 'graphql';
|
||||||
|
import { mutationWithClientMutationId } from 'graphql-relay';
|
||||||
|
import Parse from 'parse/node';
|
||||||
|
import { createSanitizedError } from '../../Error';
|
||||||
|
import GlobalConfigRouter from '../../Routers/GlobalConfigRouter';
|
||||||
|
|
||||||
|
const globalConfigRouter = new GlobalConfigRouter();
|
||||||
|
|
||||||
|
const updateCloudConfig = async (context, paramName, value, isMasterKeyOnly = false) => {
|
||||||
|
const { config, auth } = context;
|
||||||
|
|
||||||
|
if (!auth.isMaster) {
|
||||||
|
throw createSanitizedError(
|
||||||
|
Parse.Error.OPERATION_FORBIDDEN,
|
||||||
|
'Master Key is required to update GlobalConfig.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await globalConfigRouter.updateGlobalConfig({
|
||||||
|
body: {
|
||||||
|
params: { [paramName]: value },
|
||||||
|
masterKeyOnly: { [paramName]: isMasterKeyOnly },
|
||||||
|
},
|
||||||
|
config,
|
||||||
|
auth,
|
||||||
|
context,
|
||||||
|
});
|
||||||
|
|
||||||
|
return { value, isMasterKeyOnly };
|
||||||
|
};
|
||||||
|
|
||||||
|
const load = parseGraphQLSchema => {
|
||||||
|
const updateCloudConfigMutation = mutationWithClientMutationId({
|
||||||
|
name: 'UpdateCloudConfig',
|
||||||
|
description: 'Updates the value of a specific parameter in GlobalConfig.',
|
||||||
|
inputFields: {
|
||||||
|
paramName: {
|
||||||
|
description: 'The name of the parameter to set.',
|
||||||
|
type: new GraphQLNonNull(GraphQLString),
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
description: 'The value to set for the parameter.',
|
||||||
|
type: new GraphQLNonNull(GraphQLString),
|
||||||
|
},
|
||||||
|
isMasterKeyOnly: {
|
||||||
|
description: 'Whether this parameter should only be accessible with master key.',
|
||||||
|
type: GraphQLBoolean,
|
||||||
|
defaultValue: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
outputFields: {
|
||||||
|
cloudConfig: {
|
||||||
|
description: 'The updated config value.',
|
||||||
|
type: new GraphQLNonNull(parseGraphQLSchema.cloudConfigType),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mutateAndGetPayload: async (args, context) => {
|
||||||
|
try {
|
||||||
|
const { paramName, value, isMasterKeyOnly } = args;
|
||||||
|
const result = await updateCloudConfig(context, paramName, value, isMasterKeyOnly);
|
||||||
|
return {
|
||||||
|
cloudConfig: result,
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
parseGraphQLSchema.handleError(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
parseGraphQLSchema.addGraphQLType(updateCloudConfigMutation.args.input.type.ofType, true, true);
|
||||||
|
parseGraphQLSchema.addGraphQLType(updateCloudConfigMutation.type, true, true);
|
||||||
|
parseGraphQLSchema.addGraphQLMutation('updateCloudConfig', updateCloudConfigMutation, true, true);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { load, updateCloudConfig };
|
||||||
|
|
||||||
61
src/GraphQL/loaders/configQueries.js
Normal file
61
src/GraphQL/loaders/configQueries.js
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import { GraphQLNonNull, GraphQLString, GraphQLBoolean, GraphQLObjectType } from 'graphql';
|
||||||
|
import Parse from 'parse/node';
|
||||||
|
import { createSanitizedError } from '../../Error';
|
||||||
|
|
||||||
|
const cloudConfig = async (context, paramName) => {
|
||||||
|
const { config, auth } = context;
|
||||||
|
|
||||||
|
if (!auth.isMaster) {
|
||||||
|
throw createSanitizedError(
|
||||||
|
Parse.Error.OPERATION_FORBIDDEN,
|
||||||
|
'Master Key is required to access GlobalConfig.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = await config.database.find('_GlobalConfig', { objectId: '1' }, { limit: 1 });
|
||||||
|
|
||||||
|
if (results.length !== 1) {
|
||||||
|
return { value: null, isMasterKeyOnly: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
const globalConfig = results[0];
|
||||||
|
const params = globalConfig.params || {};
|
||||||
|
const masterKeyOnly = globalConfig.masterKeyOnly || {};
|
||||||
|
|
||||||
|
if (params[paramName] !== undefined) {
|
||||||
|
return { value: params[paramName], isMasterKeyOnly: masterKeyOnly[paramName] ?? null };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { value: null, isMasterKeyOnly: null };
|
||||||
|
};
|
||||||
|
|
||||||
|
const load = (parseGraphQLSchema) => {
|
||||||
|
if (!parseGraphQLSchema.cloudConfigType) {
|
||||||
|
const cloudConfigType = new GraphQLObjectType({
|
||||||
|
name: 'ConfigValue',
|
||||||
|
fields: {
|
||||||
|
value: { type: GraphQLString },
|
||||||
|
isMasterKeyOnly: { type: GraphQLBoolean },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
parseGraphQLSchema.addGraphQLType(cloudConfigType, true, true);
|
||||||
|
parseGraphQLSchema.cloudConfigType = cloudConfigType;
|
||||||
|
}
|
||||||
|
|
||||||
|
parseGraphQLSchema.addGraphQLQuery('cloudConfig', {
|
||||||
|
description: 'Returns the value of a specific parameter from GlobalConfig.',
|
||||||
|
args: {
|
||||||
|
paramName: { type: new GraphQLNonNull(GraphQLString) },
|
||||||
|
},
|
||||||
|
type: new GraphQLNonNull(parseGraphQLSchema.cloudConfigType),
|
||||||
|
async resolve(_source, args, context) {
|
||||||
|
try {
|
||||||
|
return await cloudConfig(context, args.paramName);
|
||||||
|
} catch (e) {
|
||||||
|
parseGraphQLSchema.handleError(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}, false, true);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { load, cloudConfig };
|
||||||
@@ -2,12 +2,14 @@ import * as filesMutations from './filesMutations';
|
|||||||
import * as usersMutations from './usersMutations';
|
import * as usersMutations from './usersMutations';
|
||||||
import * as functionsMutations from './functionsMutations';
|
import * as functionsMutations from './functionsMutations';
|
||||||
import * as schemaMutations from './schemaMutations';
|
import * as schemaMutations from './schemaMutations';
|
||||||
|
import * as configMutations from './configMutations';
|
||||||
|
|
||||||
const load = parseGraphQLSchema => {
|
const load = parseGraphQLSchema => {
|
||||||
filesMutations.load(parseGraphQLSchema);
|
filesMutations.load(parseGraphQLSchema);
|
||||||
usersMutations.load(parseGraphQLSchema);
|
usersMutations.load(parseGraphQLSchema);
|
||||||
functionsMutations.load(parseGraphQLSchema);
|
functionsMutations.load(parseGraphQLSchema);
|
||||||
schemaMutations.load(parseGraphQLSchema);
|
schemaMutations.load(parseGraphQLSchema);
|
||||||
|
configMutations.load(parseGraphQLSchema);
|
||||||
};
|
};
|
||||||
|
|
||||||
export { load };
|
export { load };
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { GraphQLNonNull, GraphQLBoolean } from 'graphql';
|
import { GraphQLNonNull, GraphQLBoolean } from 'graphql';
|
||||||
import * as usersQueries from './usersQueries';
|
import * as usersQueries from './usersQueries';
|
||||||
import * as schemaQueries from './schemaQueries';
|
import * as schemaQueries from './schemaQueries';
|
||||||
|
import * as configQueries from './configQueries';
|
||||||
|
|
||||||
const load = parseGraphQLSchema => {
|
const load = parseGraphQLSchema => {
|
||||||
parseGraphQLSchema.addGraphQLQuery(
|
parseGraphQLSchema.addGraphQLQuery(
|
||||||
@@ -16,6 +17,7 @@ const load = parseGraphQLSchema => {
|
|||||||
|
|
||||||
usersQueries.load(parseGraphQLSchema);
|
usersQueries.load(parseGraphQLSchema);
|
||||||
schemaQueries.load(parseGraphQLSchema);
|
schemaQueries.load(parseGraphQLSchema);
|
||||||
|
configQueries.load(parseGraphQLSchema);
|
||||||
};
|
};
|
||||||
|
|
||||||
export { load };
|
export { load };
|
||||||
|
|||||||
Reference in New Issue
Block a user