Skip to content

Commit

Permalink
feat(api-headless-cms): correct endpoint types (#2533)
Browse files Browse the repository at this point in the history
  • Loading branch information
brunozoric committed Jul 13, 2022
1 parent 405d92b commit 4fc4d5e
Show file tree
Hide file tree
Showing 9 changed files with 54 additions and 28 deletions.
4 changes: 2 additions & 2 deletions packages/api-headless-cms/src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,13 @@ export const createContextPlugin = () => {
}

context.cms = {
...((context.cms || {}) as any),
...(context.cms || {}),
type,
locale,
getLocale: () => systemLocale,
READ: type === "read",
PREVIEW: type === "preview",
MANAGE: type === "manage"
} as any;
};
});
};
5 changes: 3 additions & 2 deletions packages/api-headless-cms/src/crud/contentEntry.crud.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,11 @@ export const STATUS_UNPUBLISHED = "unpublished";
export const STATUS_CHANGES_REQUESTED = "changesRequested";
export const STATUS_REVIEW_REQUESTED = "reviewRequested";

type DefaultValue = boolean | number | string | null;
/**
* Used for some fields to convert their values.
*/
const convertDefaultValue = (field: CmsModelField, value: any): string | number | boolean => {
const convertDefaultValue = (field: CmsModelField, value: DefaultValue): DefaultValue => {
switch (field.type) {
case "boolean":
return Boolean(value);
Expand All @@ -75,7 +76,7 @@ const convertDefaultValue = (field: CmsModelField, value: any): string | number
return value;
}
};
const getDefaultValue = (field: CmsModelField): any => {
const getDefaultValue = (field: CmsModelField): (DefaultValue | DefaultValue[]) | undefined => {
const { settings, multipleValues } = field;
if (settings && settings.defaultValue !== undefined) {
return convertDefaultValue(field, settings.defaultValue);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,12 @@ interface ValidateArgs {
entry?: CmsEntry;
}

type PossibleValue = boolean | number | string | null | undefined;

const validateValue = async (
args: ValidateArgs,
fieldValidators: CmsModelFieldValidation[],
value: any
value: PossibleValue | PossibleValue[]
): Promise<string | null> => {
if (!fieldValidators) {
return null;
Expand Down
33 changes: 18 additions & 15 deletions packages/api-headless-cms/src/graphql/graphQLHandlerFactory.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { GraphQLSchema } from "graphql";
import { makeExecutableSchema } from "@graphql-tools/schema";
import { CmsContext } from "~/types";
import { ApiEndpoint, CmsContext } from "~/types";
import { I18NLocale } from "@webiny/api-i18n/types";
import { NotAuthorizedError, NotAuthorizedResponse } from "@webiny/api-security";
import { PluginCollection } from "@webiny/plugins/types";
Expand All @@ -17,9 +17,9 @@ interface SchemaCache {
key: string;
schema: GraphQLSchema;
}
interface Args {
interface GetSchemaParams {
context: CmsContext;
type: string;
type: ApiEndpoint;
locale: I18NLocale;
}

Expand Down Expand Up @@ -47,13 +47,13 @@ const respond = (http: HttpObject, result: unknown) => {
};
const schemaList = new Map<string, SchemaCache>();

const generateCacheKey = async (args: Args): Promise<string> => {
const generateCacheKey = async (args: GetSchemaParams): Promise<string> => {
const { context, locale, type } = args;
const lastModelChange = await context.cms.getModelLastChange();
return [locale.code, type, lastModelChange.toISOString()].join("#");
};

const generateSchema = async (args: Args): Promise<GraphQLSchema> => {
const generateSchema = async (args: GetSchemaParams): Promise<GraphQLSchema> => {
const { context } = args;

context.plugins.register(await buildSchemaPlugins(context));
Expand All @@ -77,16 +77,18 @@ const generateSchema = async (args: Args): Promise<GraphQLSchema> => {
});
};

// gets an existing schema or rewrites existing one or creates a completely new one
// depending on the schemaId created from type and locale parameters
const getSchema = async (args: Args): Promise<GraphQLSchema> => {
const { context, type, locale } = args;
/**
* Gets an existing schema or rewrites existing one or creates a completely new one
* depending on the schemaId created from type and locale parameters
*/
const getSchema = async (params: GetSchemaParams): Promise<GraphQLSchema> => {
const { context, type, locale } = params;
const tenantId = context.tenancy.getCurrentTenant().id;
const id = `${tenantId}#${type}#${locale.code}`;

const cacheKey = await generateCacheKey(args);
const cacheKey = await generateCacheKey(params);
if (!schemaList.has(id)) {
const schema = await generateSchema(args);
const schema = await generateSchema(params);

schemaList.set(id, {
key: cacheKey,
Expand All @@ -101,7 +103,7 @@ const getSchema = async (args: Args): Promise<GraphQLSchema> => {
if (cache.key === cacheKey) {
return cache.schema;
}
const schema = await generateSchema(args);
const schema = await generateSchema(params);
schemaList.set(id, {
key: cacheKey,
schema
Expand Down Expand Up @@ -133,7 +135,8 @@ export const graphQLHandlerFactory = ({ debug }: GraphQLHandlerFactoryParams): P
/**
* Possibly not a CMS request?
*/
if (!cms.type || !http || !http.request) {
const type = cms?.type;
if (!type || !http?.request) {
return next();
}

Expand Down Expand Up @@ -165,8 +168,8 @@ export const graphQLHandlerFactory = ({ debug }: GraphQLHandlerFactoryParams): P

const schema = await getSchema({
context,
locale: context.cms.getLocale(),
type: context.cms.type
locale: cms.getLocale(),
type
});

const body: GraphQLRequestBody | GraphQLRequestBody[] = JSON.parse(http.request.body);
Expand Down
17 changes: 15 additions & 2 deletions packages/api-headless-cms/src/graphql/schema/schemaPlugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,15 @@ export const generateSchemaPlugins = async (
): Promise<GraphQLSchemaPlugin<CmsContext>[]> => {
const { plugins, cms } = context;

/**
* If type does not exist, we are not generating schema plugins for models.
* It should not come to this point, but we check it anyways.
*/
const { type } = cms;
if (!type) {
return [];
}

// Structure plugins for faster access
const fieldTypePlugins: CmsFieldTypePlugins = plugins
.byType<CmsModelFieldToGraphQLPlugin>("cms-model-field-to-graphql")
Expand All @@ -25,7 +34,11 @@ export const generateSchemaPlugins = async (
const models = (await cms.listModels()).filter(model => model.isPrivate !== true);
context.security.enableAuthorization();

const schemas = getSchemaFromFieldPlugins({ models, fieldTypePlugins, type: cms.type });
const schemas = getSchemaFromFieldPlugins({
models,
fieldTypePlugins,
type
});

const newPlugins: GraphQLSchemaPlugin<CmsContext>[] = [];
for (const schema of schemas) {
Expand All @@ -35,7 +48,7 @@ export const generateSchemaPlugins = async (
models
.filter(model => model.fields.length > 0)
.forEach(model => {
switch (cms.type) {
switch (type) {
case "manage":
newPlugins.push(
new GraphQLSchemaPlugin({
Expand Down
7 changes: 6 additions & 1 deletion packages/api-headless-cms/src/parameters/path.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import WebinyError from "@webiny/error";
import { CmsParametersPlugin } from "~/plugins/CmsParametersPlugin";
import { ApiEndpoint } from "~/types";

const allowedEndpoints: ApiEndpoint[] = ["manage", "read", "preview"];

export const createPathParameterPlugin = () => {
return new CmsParametersPlugin(async context => {
Expand Down Expand Up @@ -27,10 +30,12 @@ export const createPathParameterPlugin = () => {
);
} else if (!locale) {
throw new WebinyError(`Missing context.http.request.path.parameters.key "locale".`);
} else if (allowedEndpoints.includes(type as ApiEndpoint) === false) {
throw new WebinyError(`Endpoint "${type}" not allowed!`);
}

return {
type,
type: type as ApiEndpoint,
locale
};
});
Expand Down
4 changes: 2 additions & 2 deletions packages/api-headless-cms/src/plugins/CmsParametersPlugin.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { Plugin } from "@webiny/plugins";
import { CmsContext } from "~/types";
import { ApiEndpoint, CmsContext } from "~/types";

/**
* Type can be null because it might be that Headless CMS context is loaded on a different Lambda where there is no GraphQL Schema generated.
*/
export type CmsParametersPluginResponseType = "read" | "manage" | "preview" | string | null;
export type CmsParametersPluginResponseType = ApiEndpoint | null;
export type CmsParametersPluginResponseLocale = string;

export interface CmsParametersPluginResponse {
Expand Down
2 changes: 1 addition & 1 deletion packages/api-headless-cms/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export interface HeadlessCms
/**
* API type
*/
type: ApiEndpoint;
type: ApiEndpoint | null;
/**
* Requested locale
*/
Expand Down
6 changes: 4 additions & 2 deletions packages/api-headless-cms/src/utils/permissions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export const checkPermissions = async <
check?: { rwd?: string; pw?: string }
): Promise<TPermission> => {
// Check if user is allowed to edit content in current language
const contentPermission: any = await context.security.getPermission("content.i18n");
const contentPermission = await context.security.getPermission("content.i18n");

if (!contentPermission) {
throw new NotAuthorizedError({
Expand All @@ -58,9 +58,11 @@ export const checkPermissions = async <
// We need to check this manually as CMS locale comes from the URL and not the default i18n app.
const code = context.cms.getLocale().code;

const locales: string[] = contentPermission.locales;

// IMPORTANT: If we have a `contentPermission`, and `locales` key is NOT SET - it means the user has access to all locales.
// However, if the the `locales` IS SET - check that it contains the required locale.
if (Array.isArray(contentPermission.locales) && !contentPermission.locales.includes(code)) {
if (Array.isArray(locales) && !locales.includes(code)) {
throw new NotAuthorizedError({
data: {
reason: `Not allowed to access content in "${code}."`
Expand Down

0 comments on commit 4fc4d5e

Please sign in to comment.