Skip to content

Commit 62445ce

Browse files
committed
feat(next-drupal): switch to use url generator plugins
1 parent 99bf2b6 commit 62445ce

File tree

5 files changed

+422
-104
lines changed

5 files changed

+422
-104
lines changed

jest.config.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ const config: Config.InitialOptions = {
1313
"/.cache/",
1414
"/drupal/",
1515
],
16+
globals: {
17+
"ts-jest": {
18+
isolatedModules: true,
19+
},
20+
},
1621
}
1722

1823
export default config

packages/next-drupal/src/client.ts

Lines changed: 115 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ import type {
2525
JsonApiResourceWithPath,
2626
PathAlias,
2727
PreviewOptions,
28-
GetResourcePreviewUrlOptions,
2928
JsonApiWithCacheOptions,
3029
JsonApiCreateResourceBody,
3130
JsonApiUpdateResourceBody,
@@ -105,6 +104,8 @@ export class DrupalClient {
105104

106105
private accessToken?: DrupalClientOptions["accessToken"]
107106

107+
private accessTokenScope?: DrupalClientOptions["accessTokenScope"]
108+
108109
private tokenExpiresOn?: number
109110

110111
private withAuth?: DrupalClientOptions["withAuth"]
@@ -538,15 +539,17 @@ export class DrupalClient {
538539
): Promise<T> {
539540
const type = typeof input === "string" ? input : input.jsonapi.resourceName
540541

541-
const previewData = context.previewData as { resourceVersion?: string }
542+
const previewData = context.previewData as {
543+
resourceVersion?: string
544+
}
542545

543546
options = {
544547
// Add support for revisions for node by default.
545548
// TODO: Make this required before stable?
546549
isVersionable: /^node--/.test(type),
547550
deserialize: true,
548551
pathPrefix: "/",
549-
withAuth: this.withAuth,
552+
withAuth: this.getAuthFromContextAndOptions(context, options),
550553
params: {},
551554
...options,
552555
}
@@ -556,7 +559,7 @@ export class DrupalClient {
556559
isVersionable: options.isVersionable,
557560
locale: context.locale,
558561
defaultLocale: context.defaultLocale,
559-
withAuth: context.preview || options?.withAuth,
562+
withAuth: options?.withAuth,
560563
params: {
561564
resourceVersion: previewData?.resourceVersion,
562565
...options?.params,
@@ -757,7 +760,6 @@ export class DrupalClient {
757760
JsonApiWithAuthOptions
758761
): Promise<T> {
759762
options = {
760-
withAuth: this.withAuth,
761763
deserialize: true,
762764
...options,
763765
}
@@ -766,7 +768,7 @@ export class DrupalClient {
766768
...options,
767769
locale: context.locale,
768770
defaultLocale: context.defaultLocale,
769-
withAuth: context.preview || options.withAuth,
771+
withAuth: this.getAuthFromContextAndOptions(context, options),
770772
})
771773
}
772774

@@ -928,18 +930,15 @@ export class DrupalClient {
928930
): Promise<DrupalTranslatedPath> {
929931
options = {
930932
pathPrefix: "/",
931-
withAuth: this.withAuth,
932933
...options,
933934
}
934935
const path = this.getPathFromContext(context, {
935936
pathPrefix: options.pathPrefix,
936937
})
937938

938-
const response = await this.translatePath(path, {
939-
withAuth: context.preview || options.withAuth,
939+
return await this.translatePath(path, {
940+
withAuth: this.getAuthFromContextAndOptions(context, options),
940941
})
941-
942-
return response
943942
}
944943

945944
getPathFromContext(
@@ -1045,84 +1044,57 @@ export class DrupalClient {
10451044
response?: NextApiResponse,
10461045
options?: PreviewOptions
10471046
) {
1048-
const { slug, resourceVersion, secret, locale, defaultLocale } =
1049-
request.query
1050-
1051-
if (secret !== this.previewSecret) {
1052-
return response
1053-
.status(401)
1054-
.json(options?.errorMessages.secret || "Invalid preview secret.")
1055-
}
1047+
const { slug, resourceVersion, plugin } = request.query
10561048

1057-
if (!slug) {
1058-
return response
1059-
.status(401)
1060-
.end(options?.errorMessages.slug || "Invalid slug.")
1061-
}
1062-
1063-
let _options: GetResourcePreviewUrlOptions = {
1064-
isVersionable: !!resourceVersion,
1065-
}
1049+
try {
1050+
// Always clear preview data to handle different scopes.
1051+
response.clearPreviewData()
1052+
1053+
// Validate the preview url.
1054+
const validateUrl = this.buildUrl("/next/preview-url")
1055+
const result = await this.fetch(validateUrl.toString(), {
1056+
method: "POST",
1057+
headers: {
1058+
"Content-Type": "application/json",
1059+
},
1060+
body: JSON.stringify(request.query),
1061+
})
10661062

1067-
if (locale && defaultLocale) {
1068-
// Fix for und locale.
1069-
const _locale = locale === "und" ? defaultLocale : locale
1063+
if (!result.ok) {
1064+
response.statusCode = result.status
10701065

1071-
_options = {
1072-
..._options,
1073-
locale: _locale as string,
1074-
defaultLocale: defaultLocale as string,
1066+
return response.json(await result.json())
10751067
}
1076-
}
10771068

1078-
const entity = await this.getResourceByPath(slug as string, {
1079-
withAuth: true,
1080-
..._options,
1081-
})
1082-
1083-
const missingEntityErrorMessage = `The entity with slug ${slug} coud not be found. If the entity exists on your Drupal site, make sure the proper permissions are configured so that Next.js can access it.`
1084-
const missingPathAliasErrorMessage = `The path alias is missing for entity with slug ${slug}.`
1085-
1086-
if (!entity) {
1087-
this.throwError(new Error(missingEntityErrorMessage))
1069+
const validationPayload = await result.json()
10881070

1089-
return response.status(404).end(missingEntityErrorMessage)
1090-
}
1091-
1092-
if (!entity?.path?.alias) {
1093-
this.throwError(new Error(missingPathAliasErrorMessage))
1094-
1095-
return response.status(404).end(missingPathAliasErrorMessage)
1096-
}
1097-
1098-
const url = entity.default_langcode
1099-
? entity?.path.alias
1100-
: `/${entity.path.langcode}${entity.path.alias}`
1071+
response.setPreviewData({
1072+
resourceVersion,
1073+
plugin,
1074+
...validationPayload,
1075+
})
11011076

1102-
if (!url) {
1103-
return response
1104-
.status(404)
1105-
.end(options?.errorMessages.slug || "Invalid slug")
1106-
}
1077+
// Fix issue with cookie.
1078+
// See https://github.com/vercel/next.js/discussions/32238.
1079+
// See https://github.com/vercel/next.js/blob/d895a50abbc8f91726daa2d7ebc22c58f58aabbb/packages/next/server/api-utils/node.ts#L504.
1080+
if (this.forceIframeSameSiteCookie) {
1081+
const previous = response.getHeader("Set-Cookie") as string[]
1082+
previous.forEach((cookie, index) => {
1083+
previous[index] = cookie.replace(
1084+
"SameSite=Lax",
1085+
"SameSite=None;Secure"
1086+
)
1087+
})
1088+
response.setHeader(`Set-Cookie`, previous)
1089+
}
11071090

1108-
response.setPreviewData({
1109-
resourceVersion,
1110-
})
1091+
// We can safely redirect to the slug since this has been validated on the server.
1092+
response.writeHead(307, { Location: slug })
11111093

1112-
// Fix issue with cookie.
1113-
// See https://github.com/vercel/next.js/discussions/32238.
1114-
// See https://github.com/vercel/next.js/blob/d895a50abbc8f91726daa2d7ebc22c58f58aabbb/packages/next/server/api-utils/node.ts#L504.
1115-
if (this.forceIframeSameSiteCookie) {
1116-
const previous = response.getHeader("Set-Cookie") as string[]
1117-
previous.forEach((cookie, index) => {
1118-
previous[index] = cookie.replace("SameSite=Lax", "SameSite=None;Secure")
1119-
})
1120-
response.setHeader(`Set-Cookie`, previous)
1094+
return response.end()
1095+
} catch (error) {
1096+
return response.status(422).end()
11211097
}
1122-
1123-
response.writeHead(307, { Location: url })
1124-
1125-
return response.end()
11261098
}
11271099

11281100
async getMenu<T = DrupalMenuLinkContent>(
@@ -1320,12 +1292,10 @@ export class DrupalClient {
13201292
return url
13211293
}
13221294

1323-
async getAccessToken(opts?: {
1324-
clientId: string
1325-
clientSecret: string
1326-
url?: string
1327-
}): Promise<AccessToken> {
1328-
if (this.accessToken) {
1295+
async getAccessToken(
1296+
opts?: DrupalClientAuthClientIdSecret
1297+
): Promise<AccessToken> {
1298+
if (this.accessToken && this.accessTokenScope === opts?.scope) {
13291299
return this.accessToken
13301300
}
13311301

@@ -1350,7 +1320,11 @@ export class DrupalClient {
13501320
const clientSecret = opts?.clientSecret || this._auth.clientSecret
13511321
const url = this.buildUrl(opts?.url || this._auth.url || DEFAULT_AUTH_URL)
13521322

1353-
if (this._token && Date.now() < this.tokenExpiresOn) {
1323+
if (
1324+
this.accessTokenScope === opts?.scope &&
1325+
this._token &&
1326+
Date.now() < this.tokenExpiresOn
1327+
) {
13541328
this._debug(`Using existing access token.`)
13551329
return this._token
13561330
}
@@ -1359,13 +1333,21 @@ export class DrupalClient {
13591333

13601334
const basic = Buffer.from(`${clientId}:${clientSecret}`).toString("base64")
13611335

1336+
let body = `grant_type=client_credentials`
1337+
1338+
if (opts?.scope) {
1339+
body = `${body}&scope=${opts.scope}`
1340+
1341+
this._debug(`Using scope: ${opts.scope}`)
1342+
}
1343+
13621344
const response = await fetch(url.toString(), {
13631345
method: "POST",
13641346
headers: {
13651347
Authorization: `Basic ${basic}`,
13661348
"Content-Type": "application/x-www-form-urlencoded",
13671349
},
1368-
body: `grant_type=client_credentials`,
1350+
body,
13691351
})
13701352

13711353
if (!response?.ok) {
@@ -1378,6 +1360,8 @@ export class DrupalClient {
13781360

13791361
this.token = result
13801362

1363+
this.accessTokenScope = opts?.scope
1364+
13811365
return result
13821366
}
13831367

@@ -1443,4 +1427,48 @@ export class DrupalClient {
14431427
throw new JsonApiErrors(errors, response.status)
14441428
}
14451429
}
1430+
1431+
private getAuthFromContextAndOptions(
1432+
context: GetStaticPropsContext,
1433+
options: JsonApiWithAuthOptions
1434+
) {
1435+
// If not in preview or withAuth is provided, use that.
1436+
if (!context.preview) {
1437+
// If we have provided an auth, use that.
1438+
if (typeof options?.withAuth !== "undefined") {
1439+
return options.withAuth
1440+
}
1441+
1442+
// Otherwise we fallback to the global auth.
1443+
return this.withAuth
1444+
}
1445+
1446+
// If no plugin is provided, return.
1447+
const plugin = context.previewData?.["plugin"]
1448+
if (!plugin) {
1449+
return null
1450+
}
1451+
1452+
let withAuth = this._auth
1453+
1454+
if (plugin === "simple_oauth") {
1455+
// If we are using a client id and secret auth, pass the scope.
1456+
if (isClientIdSecretAuth(withAuth) && context.previewData?.["scope"]) {
1457+
withAuth = {
1458+
...withAuth,
1459+
scope: context.previewData?.["scope"],
1460+
}
1461+
}
1462+
}
1463+
1464+
if (plugin === "jwt") {
1465+
const accessToken = context.previewData?.["access_token"]
1466+
1467+
if (accessToken) {
1468+
return `Bearer ${accessToken}`
1469+
}
1470+
}
1471+
1472+
return withAuth
1473+
}
14461474
}

packages/next-drupal/src/types.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,11 @@ export type DrupalClientOptions = {
135135
*/
136136
accessToken?: AccessToken
137137

138+
/**
139+
* The scope used for the current access token.
140+
*/
141+
accessTokenScope?: string
142+
138143
/**
139144
* If set to true, the preview cookie will be set with SameSite=None,Secure.
140145
*
@@ -162,6 +167,7 @@ export interface DrupalClientAuthClientIdSecret {
162167
clientId: string
163168
clientSecret: string
164169
url?: string
170+
scope?: string
165171
}
166172

167173
export type DrupalClientAuthAccessToken = AccessToken
@@ -340,6 +346,7 @@ export interface DrupalTranslatedPath {
340346
id: string
341347
uuid: string
342348
langcode?: string
349+
path?: string
343350
}
344351
label?: string
345352
jsonapi?: {

0 commit comments

Comments
 (0)