Skip to content

Commit

Permalink
fix: ensure old meta field values are returned (#4161)
Browse files Browse the repository at this point in the history
  • Loading branch information
adrians5j authored Jun 11, 2024
1 parent c1849b3 commit 96f2f69
Show file tree
Hide file tree
Showing 15 changed files with 270 additions and 42 deletions.
13 changes: 13 additions & 0 deletions packages/api-headless-cms-ddb-es/src/definitions/entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,19 @@ export const createEntryEntity = (params: CreateEntryEntityParams): Entity<any>
firstPublishedBy: { type: "map" },
lastPublishedBy: { type: "map" },

/**
* Deprecated fields. 👇
*/
ownedBy: {
type: "map"
},
publishedOn: {
type: "string"
},

/**
* The rest. 👇
*/
modelId: {
type: "string"
},
Expand Down
13 changes: 13 additions & 0 deletions packages/api-headless-cms-ddb/src/definitions/entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,19 @@ export const createEntryEntity = (params: Params): Entity<any> => {
firstPublishedBy: { type: "map" },
lastPublishedBy: { type: "map" },

/**
* Deprecated fields. 👇
*/
ownedBy: {
type: "map"
},
publishedOn: {
type: "string"
},

/**
* The rest. 👇
*/
version: {
type: "number"
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import { SecurityIdentity } from "@webiny/api-security/types";
import { useTestModelHandler } from "~tests/testHelpers/useTestModelHandler";
import { getDocumentClient } from "@webiny/project-utils/testing/dynamodb";
import { PutCommand, QueryCommand, unmarshall } from "@webiny/aws-sdk/client-dynamodb";
import { CmsGraphQLSchemaPlugin } from "@webiny/api-headless-cms/plugins";

const identityA: SecurityIdentity = { id: "a", type: "admin", displayName: "A" };

jest.mock("~/graphql/getSchema/generateCacheId", () => {
return {
generateCacheId: () => Date.now()
};
});

describe("Content entries - Entry Meta Fields", () => {
const { manage: manageApiIdentityA, read: readApiIdentityA } = useTestModelHandler({
identity: identityA
});

beforeEach(async () => {
await manageApiIdentityA.setup();
});

test("deprecated 'publishedOn' and 'ownedBy' GraphQL fields should still return values", async () => {
const { data: testEntry } = await manageApiIdentityA.createTestEntry();

// Let's directly insert values for deprecated fields.
const client = getDocumentClient();

// Not pretty, but this test will be removed anyway, in 5.41.0.
if (process.env.WEBINY_STORAGE_OPS === "ddb") {
const { Items: testEntryDdbRecords } = await client.send(
new QueryCommand({
TableName: String(process.env.DB_TABLE),
KeyConditionExpression: "PK = :PK AND SK > :SK",
ExpressionAttributeValues: {
":PK": { S: `T#root#L#en-US#CMS#CME#CME#${testEntry.entryId}` },
":SK": { S: " " }
}
})
);

for (const testEntryDdbRecord of testEntryDdbRecords!) {
await client.send(
new PutCommand({
TableName: process.env.DB_TABLE,
Item: {
...unmarshall(testEntryDdbRecord),
publishedOn: "2021-01-01T00:00:00.000Z",
ownedBy: identityA
}
})
);
}
} else {
const { Items: testEntryDdbRecords } = await client.send(
new QueryCommand({
TableName: String(process.env.DB_TABLE),
KeyConditionExpression: "PK = :PK AND SK > :SK",
ExpressionAttributeValues: {
":PK": { S: `T#root#L#en-US#CMS#CME#${testEntry.entryId}` },
":SK": { S: " " }
}
})
);

for (const testEntryDdbRecord of testEntryDdbRecords!) {
await client.send(
new PutCommand({
TableName: process.env.DB_TABLE,
Item: {
...unmarshall(testEntryDdbRecord),
publishedOn: "2021-01-01T00:00:00.000Z",
ownedBy: identityA
}
})
);
}
}

// Ensure values are visible when data is fetched via GraphQL.
{
const { data: testEntryWithDeprecatedFields } = await manageApiIdentityA.getTestEntry({
revision: testEntry.id
});

expect(testEntryWithDeprecatedFields).toMatchObject({
publishedOn: "2021-01-01T00:00:00.000Z",
ownedBy: identityA
});
}

await manageApiIdentityA.publishTestEntry({ revision: testEntry.id });

{
const { data: testEntryWithDeprecatedFields } = await readApiIdentityA.getTestEntry({
where: { entryId: testEntry.entryId }
});

expect(testEntryWithDeprecatedFields).toMatchObject({
publishedOn: "2021-01-01T00:00:00.000Z",
ownedBy: identityA
});
}
});

test("deprecated 'publishedOn' and 'ownedBy' GraphQL fields should fall back to new fields if no value is present", async () => {
const { data: testEntry } = await manageApiIdentityA.createTestEntry();

const { data: publishedTestEntry } = await manageApiIdentityA.publishTestEntry({
revision: testEntry.id
});

expect(publishedTestEntry).toMatchObject({
publishedOn: null,
ownedBy: null
});

// Ensure values are visible when data is fetched via GraphQL.
const customGqlResolvers = new CmsGraphQLSchemaPlugin({
resolvers: {
TestEntry: {
publishedOn: entry => {
return entry.lastPublishedOn;
},
ownedBy: entry => {
return entry.createdBy;
}
}
}
});

customGqlResolvers.name = "cms-test-entry-meta-fields";

const { manage: manageApiWithGqlResolvers, read: readApiWithGqlResolvers } =
useTestModelHandler({
identity: identityA,
plugins: [customGqlResolvers]
});

const { data: testEntryWithDeprecatedFields } =
await manageApiWithGqlResolvers.getTestEntry({
revision: testEntry.id
});

expect(testEntryWithDeprecatedFields).toMatchObject({
publishedOn: publishedTestEntry.lastPublishedOn,
ownedBy: publishedTestEntry.createdBy
});

const { data: readTestEntryWithDeprecatedFields } =
await readApiWithGqlResolvers.getTestEntry({
where: {
entryId: testEntry.entryId
}
});

expect(readTestEntryWithDeprecatedFields).toMatchObject({
publishedOn: publishedTestEntry.lastPublishedOn,
ownedBy: publishedTestEntry.createdBy
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ export const fields = /* GraphQL */ `{
revisionFirstPublishedBy ${identityFields}
revisionLastPublishedBy ${identityFields}
publishedOn
ownedBy ${identityFields}
meta {
title
modelId
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ const data = /* GraphQL */ `
id
entryId
createdOn
publishedOn
ownedBy {
id
displayName
type
}
savedOn
title
slug
Expand Down
1 change: 1 addition & 0 deletions packages/api-headless-cms/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"@babel/core": "^7.22.8",
"@babel/preset-env": "^7.22.7",
"@webiny/api-wcp": "0.0.0",
"@webiny/aws-sdk": "0.0.0",
"@webiny/cli": "0.0.0",
"@webiny/project-utils": "0.0.0",
"apollo-graphql": "^0.9.5",
Expand Down
44 changes: 3 additions & 41 deletions packages/api-headless-cms/src/graphql/getSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@
import codeFrame from "code-frame";
import WebinyError from "@webiny/error";
import { generateSchema } from "./generateSchema";
import { ApiEndpoint, CmsContext, CmsModel } from "~/types";
import { ApiEndpoint, CmsContext } from "~/types";
import { Tenant } from "@webiny/api-tenancy/types";
import { I18NLocale } from "@webiny/api-i18n/types";
import { GraphQLSchema } from "graphql";
import crypto from "crypto";
import { generateCacheId } from "./getSchema/generateCacheId";
import { generateCacheKey } from "./getSchema/generateCacheKey";

interface SchemaCache {
key: string;
Expand All @@ -22,45 +23,6 @@ interface GetSchemaParams {

const schemaList = new Map<string, SchemaCache>();

/**
* Method generates cache ID based on:
* - tenant
* - endpoint type
* - locale
*/
type GenerateCacheIdParams = Pick<GetSchemaParams, "getTenant" | "getLocale" | "type">;
const generateCacheId = (params: GenerateCacheIdParams): string => {
const { getTenant, type, getLocale } = params;
return [`tenant:${getTenant().id}`, `endpoint:${type}`, `locale:${getLocale().code}`].join("#");
};
/**
* Method generates cache key based on last model change time.
* Or sets "unknown" - possible when no models in database.
*/
interface GenerateCacheKeyParams {
models: Pick<CmsModel, "modelId" | "singularApiName" | "pluralApiName" | "savedOn">[];
}
const generateCacheKey = async (params: GenerateCacheKeyParams): Promise<string> => {
const { models } = params;

const keys: string[] = [];
for (const model of models) {
const savedOn = model.savedOn;
const value =
// @ts-expect-error
savedOn instanceof Date || savedOn?.toISOString
? // @ts-expect-error
savedOn.toISOString()
: savedOn || "unknown";
keys.push(model.modelId, model.singularApiName, model.pluralApiName, value);
}
const key = keys.join("#");

const hash = crypto.createHash("sha1");
hash.update(key);
return hash.digest("hex");
};

/**
* Gets an existing schema or rewrites existing one or creates a completely new one
* depending on the schemaId created from type and locale parameters
Expand Down
14 changes: 14 additions & 0 deletions packages/api-headless-cms/src/graphql/getSchema/generateCacheId.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { ApiEndpoint } from "~/types";
import { Tenant } from "@webiny/api-tenancy/types";
import { I18NLocale } from "@webiny/api-i18n/types";

interface GenerateCacheIdParams {
type: ApiEndpoint;
getTenant: () => Tenant;
getLocale: () => I18NLocale;
}

export const generateCacheId = (params: GenerateCacheIdParams): string => {
const { getTenant, type, getLocale } = params;
return [`tenant:${getTenant().id}`, `endpoint:${type}`, `locale:${getLocale().code}`].join("#");
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { CmsModel } from "~/types";
import crypto from "crypto";

interface GenerateCacheKeyParams {
models: Pick<CmsModel, "modelId" | "singularApiName" | "pluralApiName" | "savedOn">[];
}

/**
* Method generates cache key based on last model change time.
* Or sets "unknown" - possible when no models in database.
*/
export const generateCacheKey = async (params: GenerateCacheKeyParams): Promise<string> => {
const { models } = params;

const keys: string[] = [];
for (const model of models) {
const savedOn = model.savedOn;
const value =
// @ts-expect-error
savedOn instanceof Date || savedOn?.toISOString
? // @ts-expect-error
savedOn.toISOString()
: savedOn || "unknown";
keys.push(model.modelId, model.singularApiName, model.pluralApiName, value);
}
const key = keys.join("#");

const hash = crypto.createHash("sha1");
hash.update(key);
return hash.digest("hex");
};
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export const createManageResolvers: CreateManageResolvers = ({
// These are extra fields we want to apply to field resolvers of "gqlType"
extraResolvers: {
/**
* Advanced Content Entry
* Advanced Content Organization
*/
wbyAco_location: async (entry: CmsEntry) => {
return entry.location || null;
Expand Down
14 changes: 14 additions & 0 deletions packages/api-headless-cms/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1571,6 +1571,20 @@ export interface CmsEntry<T = CmsEntryValues> {
*/
lastPublishedBy: CmsIdentity | null;

/**
* Deprecated fields. 👇
*/

/**
* @deprecated Will be removed with the 5.41.0 release. Use `createdBy` field instead.
*/
ownedBy?: CmsIdentity | null;

/**
* @deprecated Will be removed with the 5.41.0 release. Use `firstPublishedOn` or `lastPublishedOn` field instead.
*/
publishedOn?: string | null;

/**
* Model ID of the definition for the entry.
* @see CmsModel
Expand Down
1 change: 1 addition & 0 deletions packages/api-headless-cms/tsconfig.build.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
{ "path": "../api-i18n/tsconfig.build.json" },
{ "path": "../api-security/tsconfig.build.json" },
{ "path": "../api-tenancy/tsconfig.build.json" },
{ "path": "../aws-sdk/tsconfig.build.json" },
{ "path": "../error/tsconfig.build.json" },
{ "path": "../handler/tsconfig.build.json" },
{ "path": "../handler-aws/tsconfig.build.json" },
Expand Down
3 changes: 3 additions & 0 deletions packages/api-headless-cms/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
{ "path": "../api-i18n" },
{ "path": "../api-security" },
{ "path": "../api-tenancy" },
{ "path": "../aws-sdk" },
{ "path": "../error" },
{ "path": "../handler" },
{ "path": "../handler-aws" },
Expand Down Expand Up @@ -33,6 +34,8 @@
"@webiny/api-security": ["../api-security/src"],
"@webiny/api-tenancy/*": ["../api-tenancy/src/*"],
"@webiny/api-tenancy": ["../api-tenancy/src"],
"@webiny/aws-sdk/*": ["../aws-sdk/src/*"],
"@webiny/aws-sdk": ["../aws-sdk/src"],
"@webiny/error/*": ["../error/src/*"],
"@webiny/error": ["../error/src"],
"@webiny/handler/*": ["../handler/src/*"],
Expand Down
Loading

0 comments on commit 96f2f69

Please sign in to comment.