Skip to content

Commit 96f2f69

Browse files
authored
fix: ensure old meta field values are returned (#4161)
1 parent c1849b3 commit 96f2f69

File tree

15 files changed

+270
-42
lines changed

15 files changed

+270
-42
lines changed

packages/api-headless-cms-ddb-es/src/definitions/entry.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,19 @@ export const createEntryEntity = (params: CreateEntryEntityParams): Entity<any>
6767
firstPublishedBy: { type: "map" },
6868
lastPublishedBy: { type: "map" },
6969

70+
/**
71+
* Deprecated fields. 👇
72+
*/
73+
ownedBy: {
74+
type: "map"
75+
},
76+
publishedOn: {
77+
type: "string"
78+
},
79+
80+
/**
81+
* The rest. 👇
82+
*/
7083
modelId: {
7184
type: "string"
7285
},

packages/api-headless-cms-ddb/src/definitions/entry.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,19 @@ export const createEntryEntity = (params: Params): Entity<any> => {
8080
firstPublishedBy: { type: "map" },
8181
lastPublishedBy: { type: "map" },
8282

83+
/**
84+
* Deprecated fields. 👇
85+
*/
86+
ownedBy: {
87+
type: "map"
88+
},
89+
publishedOn: {
90+
type: "string"
91+
},
92+
93+
/**
94+
* The rest. 👇
95+
*/
8396
version: {
8497
type: "number"
8598
},
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
import { SecurityIdentity } from "@webiny/api-security/types";
2+
import { useTestModelHandler } from "~tests/testHelpers/useTestModelHandler";
3+
import { getDocumentClient } from "@webiny/project-utils/testing/dynamodb";
4+
import { PutCommand, QueryCommand, unmarshall } from "@webiny/aws-sdk/client-dynamodb";
5+
import { CmsGraphQLSchemaPlugin } from "@webiny/api-headless-cms/plugins";
6+
7+
const identityA: SecurityIdentity = { id: "a", type: "admin", displayName: "A" };
8+
9+
jest.mock("~/graphql/getSchema/generateCacheId", () => {
10+
return {
11+
generateCacheId: () => Date.now()
12+
};
13+
});
14+
15+
describe("Content entries - Entry Meta Fields", () => {
16+
const { manage: manageApiIdentityA, read: readApiIdentityA } = useTestModelHandler({
17+
identity: identityA
18+
});
19+
20+
beforeEach(async () => {
21+
await manageApiIdentityA.setup();
22+
});
23+
24+
test("deprecated 'publishedOn' and 'ownedBy' GraphQL fields should still return values", async () => {
25+
const { data: testEntry } = await manageApiIdentityA.createTestEntry();
26+
27+
// Let's directly insert values for deprecated fields.
28+
const client = getDocumentClient();
29+
30+
// Not pretty, but this test will be removed anyway, in 5.41.0.
31+
if (process.env.WEBINY_STORAGE_OPS === "ddb") {
32+
const { Items: testEntryDdbRecords } = await client.send(
33+
new QueryCommand({
34+
TableName: String(process.env.DB_TABLE),
35+
KeyConditionExpression: "PK = :PK AND SK > :SK",
36+
ExpressionAttributeValues: {
37+
":PK": { S: `T#root#L#en-US#CMS#CME#CME#${testEntry.entryId}` },
38+
":SK": { S: " " }
39+
}
40+
})
41+
);
42+
43+
for (const testEntryDdbRecord of testEntryDdbRecords!) {
44+
await client.send(
45+
new PutCommand({
46+
TableName: process.env.DB_TABLE,
47+
Item: {
48+
...unmarshall(testEntryDdbRecord),
49+
publishedOn: "2021-01-01T00:00:00.000Z",
50+
ownedBy: identityA
51+
}
52+
})
53+
);
54+
}
55+
} else {
56+
const { Items: testEntryDdbRecords } = await client.send(
57+
new QueryCommand({
58+
TableName: String(process.env.DB_TABLE),
59+
KeyConditionExpression: "PK = :PK AND SK > :SK",
60+
ExpressionAttributeValues: {
61+
":PK": { S: `T#root#L#en-US#CMS#CME#${testEntry.entryId}` },
62+
":SK": { S: " " }
63+
}
64+
})
65+
);
66+
67+
for (const testEntryDdbRecord of testEntryDdbRecords!) {
68+
await client.send(
69+
new PutCommand({
70+
TableName: process.env.DB_TABLE,
71+
Item: {
72+
...unmarshall(testEntryDdbRecord),
73+
publishedOn: "2021-01-01T00:00:00.000Z",
74+
ownedBy: identityA
75+
}
76+
})
77+
);
78+
}
79+
}
80+
81+
// Ensure values are visible when data is fetched via GraphQL.
82+
{
83+
const { data: testEntryWithDeprecatedFields } = await manageApiIdentityA.getTestEntry({
84+
revision: testEntry.id
85+
});
86+
87+
expect(testEntryWithDeprecatedFields).toMatchObject({
88+
publishedOn: "2021-01-01T00:00:00.000Z",
89+
ownedBy: identityA
90+
});
91+
}
92+
93+
await manageApiIdentityA.publishTestEntry({ revision: testEntry.id });
94+
95+
{
96+
const { data: testEntryWithDeprecatedFields } = await readApiIdentityA.getTestEntry({
97+
where: { entryId: testEntry.entryId }
98+
});
99+
100+
expect(testEntryWithDeprecatedFields).toMatchObject({
101+
publishedOn: "2021-01-01T00:00:00.000Z",
102+
ownedBy: identityA
103+
});
104+
}
105+
});
106+
107+
test("deprecated 'publishedOn' and 'ownedBy' GraphQL fields should fall back to new fields if no value is present", async () => {
108+
const { data: testEntry } = await manageApiIdentityA.createTestEntry();
109+
110+
const { data: publishedTestEntry } = await manageApiIdentityA.publishTestEntry({
111+
revision: testEntry.id
112+
});
113+
114+
expect(publishedTestEntry).toMatchObject({
115+
publishedOn: null,
116+
ownedBy: null
117+
});
118+
119+
// Ensure values are visible when data is fetched via GraphQL.
120+
const customGqlResolvers = new CmsGraphQLSchemaPlugin({
121+
resolvers: {
122+
TestEntry: {
123+
publishedOn: entry => {
124+
return entry.lastPublishedOn;
125+
},
126+
ownedBy: entry => {
127+
return entry.createdBy;
128+
}
129+
}
130+
}
131+
});
132+
133+
customGqlResolvers.name = "cms-test-entry-meta-fields";
134+
135+
const { manage: manageApiWithGqlResolvers, read: readApiWithGqlResolvers } =
136+
useTestModelHandler({
137+
identity: identityA,
138+
plugins: [customGqlResolvers]
139+
});
140+
141+
const { data: testEntryWithDeprecatedFields } =
142+
await manageApiWithGqlResolvers.getTestEntry({
143+
revision: testEntry.id
144+
});
145+
146+
expect(testEntryWithDeprecatedFields).toMatchObject({
147+
publishedOn: publishedTestEntry.lastPublishedOn,
148+
ownedBy: publishedTestEntry.createdBy
149+
});
150+
151+
const { data: readTestEntryWithDeprecatedFields } =
152+
await readApiWithGqlResolvers.getTestEntry({
153+
where: {
154+
entryId: testEntry.entryId
155+
}
156+
});
157+
158+
expect(readTestEntryWithDeprecatedFields).toMatchObject({
159+
publishedOn: publishedTestEntry.lastPublishedOn,
160+
ownedBy: publishedTestEntry.createdBy
161+
});
162+
});
163+
});

packages/api-headless-cms/__tests__/testHelpers/useTestModelHandler/manageGql.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ export const fields = /* GraphQL */ `{
3838
revisionFirstPublishedBy ${identityFields}
3939
revisionLastPublishedBy ${identityFields}
4040
41+
publishedOn
42+
ownedBy ${identityFields}
43+
4144
meta {
4245
title
4346
modelId

packages/api-headless-cms/__tests__/testHelpers/useTestModelHandler/readGql.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@ const data = /* GraphQL */ `
33
id
44
entryId
55
createdOn
6+
publishedOn
7+
ownedBy {
8+
id
9+
displayName
10+
type
11+
}
612
savedOn
713
title
814
slug

packages/api-headless-cms/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
"@babel/core": "^7.22.8",
5353
"@babel/preset-env": "^7.22.7",
5454
"@webiny/api-wcp": "0.0.0",
55+
"@webiny/aws-sdk": "0.0.0",
5556
"@webiny/cli": "0.0.0",
5657
"@webiny/project-utils": "0.0.0",
5758
"apollo-graphql": "^0.9.5",

packages/api-headless-cms/src/graphql/getSchema.ts

Lines changed: 3 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@
22
import codeFrame from "code-frame";
33
import WebinyError from "@webiny/error";
44
import { generateSchema } from "./generateSchema";
5-
import { ApiEndpoint, CmsContext, CmsModel } from "~/types";
5+
import { ApiEndpoint, CmsContext } from "~/types";
66
import { Tenant } from "@webiny/api-tenancy/types";
77
import { I18NLocale } from "@webiny/api-i18n/types";
88
import { GraphQLSchema } from "graphql";
9-
import crypto from "crypto";
9+
import { generateCacheId } from "./getSchema/generateCacheId";
10+
import { generateCacheKey } from "./getSchema/generateCacheKey";
1011

1112
interface SchemaCache {
1213
key: string;
@@ -22,45 +23,6 @@ interface GetSchemaParams {
2223

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

25-
/**
26-
* Method generates cache ID based on:
27-
* - tenant
28-
* - endpoint type
29-
* - locale
30-
*/
31-
type GenerateCacheIdParams = Pick<GetSchemaParams, "getTenant" | "getLocale" | "type">;
32-
const generateCacheId = (params: GenerateCacheIdParams): string => {
33-
const { getTenant, type, getLocale } = params;
34-
return [`tenant:${getTenant().id}`, `endpoint:${type}`, `locale:${getLocale().code}`].join("#");
35-
};
36-
/**
37-
* Method generates cache key based on last model change time.
38-
* Or sets "unknown" - possible when no models in database.
39-
*/
40-
interface GenerateCacheKeyParams {
41-
models: Pick<CmsModel, "modelId" | "singularApiName" | "pluralApiName" | "savedOn">[];
42-
}
43-
const generateCacheKey = async (params: GenerateCacheKeyParams): Promise<string> => {
44-
const { models } = params;
45-
46-
const keys: string[] = [];
47-
for (const model of models) {
48-
const savedOn = model.savedOn;
49-
const value =
50-
// @ts-expect-error
51-
savedOn instanceof Date || savedOn?.toISOString
52-
? // @ts-expect-error
53-
savedOn.toISOString()
54-
: savedOn || "unknown";
55-
keys.push(model.modelId, model.singularApiName, model.pluralApiName, value);
56-
}
57-
const key = keys.join("#");
58-
59-
const hash = crypto.createHash("sha1");
60-
hash.update(key);
61-
return hash.digest("hex");
62-
};
63-
6426
/**
6527
* Gets an existing schema or rewrites existing one or creates a completely new one
6628
* depending on the schemaId created from type and locale parameters
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { ApiEndpoint } from "~/types";
2+
import { Tenant } from "@webiny/api-tenancy/types";
3+
import { I18NLocale } from "@webiny/api-i18n/types";
4+
5+
interface GenerateCacheIdParams {
6+
type: ApiEndpoint;
7+
getTenant: () => Tenant;
8+
getLocale: () => I18NLocale;
9+
}
10+
11+
export const generateCacheId = (params: GenerateCacheIdParams): string => {
12+
const { getTenant, type, getLocale } = params;
13+
return [`tenant:${getTenant().id}`, `endpoint:${type}`, `locale:${getLocale().code}`].join("#");
14+
};
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { CmsModel } from "~/types";
2+
import crypto from "crypto";
3+
4+
interface GenerateCacheKeyParams {
5+
models: Pick<CmsModel, "modelId" | "singularApiName" | "pluralApiName" | "savedOn">[];
6+
}
7+
8+
/**
9+
* Method generates cache key based on last model change time.
10+
* Or sets "unknown" - possible when no models in database.
11+
*/
12+
export const generateCacheKey = async (params: GenerateCacheKeyParams): Promise<string> => {
13+
const { models } = params;
14+
15+
const keys: string[] = [];
16+
for (const model of models) {
17+
const savedOn = model.savedOn;
18+
const value =
19+
// @ts-expect-error
20+
savedOn instanceof Date || savedOn?.toISOString
21+
? // @ts-expect-error
22+
savedOn.toISOString()
23+
: savedOn || "unknown";
24+
keys.push(model.modelId, model.singularApiName, model.pluralApiName, value);
25+
}
26+
const key = keys.join("#");
27+
28+
const hash = crypto.createHash("sha1");
29+
hash.update(key);
30+
return hash.digest("hex");
31+
};

packages/api-headless-cms/src/graphql/schema/createManageResolvers.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ export const createManageResolvers: CreateManageResolvers = ({
5656
// These are extra fields we want to apply to field resolvers of "gqlType"
5757
extraResolvers: {
5858
/**
59-
* Advanced Content Entry
59+
* Advanced Content Organization
6060
*/
6161
wbyAco_location: async (entry: CmsEntry) => {
6262
return entry.location || null;

0 commit comments

Comments
 (0)