Loading src/DrupalState.ts +17 −9 Original line number Diff line number Diff line Loading @@ -231,15 +231,17 @@ class DrupalState { * Fetches data using our fetch method. * @param endpoint the assembled JSON:API endpoint * @param res response object * @param anon make the request anonymously if true * @returns data fetched from JSON:API endpoint */ async fetchData( endpoint: string, res: ServerResponse | boolean = false res: ServerResponse | boolean = false, anon = false ): Promise<TJsonApiBody | void> { let requestInit = {}; let authHeader = ''; if (this.clientId && this.clientSecret) { if (this.clientId && this.clientSecret && !anon) { const headers = new Headers(); authHeader = await this.getAuthHeader(); headers.append('Authorization', authHeader); Loading Loading @@ -288,6 +290,7 @@ class DrupalState { * @param options.res - response object * @param options.params - user provided JSON:API parameter string or DrupalJsonApiParams object * @param options.refresh - a boolean value. If true, ignore local state. * @param options.anon - a boolean value. If true, send the request without the authentication header if valid credentials exist. * @returns a promise containing deserialized JSON:API data for the requested * object * Loading @@ -308,6 +311,7 @@ class DrupalState { res, params, refresh = false, anon = false, query = false, }: GetObjectByPathParams): Promise<PartialState<State> | void> { if (query) { Loading @@ -325,7 +329,7 @@ class DrupalState { // TODO - abstract helper method to assemble requestInit and authHeader let requestInit = {}; let authHeader = ''; if (this.clientId && this.clientSecret) { if (this.clientId && this.clientSecret && !anon) { const headers = new Headers(); authHeader = await this.getAuthHeader(); headers.append('Authorization', authHeader); Loading Loading @@ -394,10 +398,11 @@ class DrupalState { * @remarks The query option was experimental and is now deprecated * @param options.objectName - Name of object to fetch * @param options.id - id of a specific resource * @param options.res response object * @param options.params user provided JSON:API parameter string or DrupalJsonApiParams object * @param options.all a boolean value. If true, fetch all objects in a collection. * @param options.refresh a boolean value. If true, ignore local state. * @param options.res - response object * @param options.params - user provided JSON:API parameter string or DrupalJsonApiParams object * @param options.all - a boolean value. If true, fetch all objects in a collection. * @param options.refresh - a boolean value. If true, ignore local state. * @param options.anon - a boolean value. If true, send the request without the authentication header if valid credentials exist. * @returns a promise containing deserialized JSON:API data for the requested * object * Loading @@ -420,6 +425,7 @@ class DrupalState { params, all = false, refresh = false, anon = false, query = false, }: GetObjectParams): Promise<PartialState<State> | void> { if (query) { Loading Loading @@ -512,7 +518,8 @@ class DrupalState { const resourceData = (await this.fetchData( endpoint, res res, anon )) as keyedResources; const objectResourceState = state[resourceKey]; Loading Loading @@ -572,7 +579,8 @@ class DrupalState { const collectionData = (await this.fetchData( endpoint, res res, anon )) as keyedResources; const fetchedCollectionState = {} as CollectionState; Loading src/__tests__/drupalState.test.ts +39 −11 Original line number Diff line number Diff line Loading @@ -348,7 +348,7 @@ describe('drupalState', () => { expect(fetchMock).toBeCalledTimes(2); }); test('Re-fetch object if it exists in local storage but refresh is set to true', async () => { test('Re-fetch collection object if it exists in local state but refresh is set to true', async () => { const store: DrupalState = new DrupalState({ apiBase: 'https://dev-ds-demo.pantheonsite.io', apiPrefix: 'jsonapi', Loading Loading @@ -425,15 +425,8 @@ describe('drupalState', () => { clientSecret: 'mysecret', debug: true, }); const getAuthHeaderSpy = jest.spyOn(store, 'getAuthHeader'); store.setState({ dsApiIndex: indexResponse.links }); fetchMock.mock( 'https://dev-ds-demo.pantheonsite.io/en/jsonapi/node/recipe/33386d32-a87c-44b9-b66b-3dd0bfc38dca', { status: 200, body: recipesResourceData1, }, { overwriteRoutes: true } ); fetchMock.mock( { url: 'https://dev-ds-demo.pantheonsite.io/oauth/token', Loading @@ -447,8 +440,13 @@ describe('drupalState', () => { body: tokenResponse, } ); expect(await store['getAuthHeader']()).toEqual( `${tokenResponse.token_type} ${tokenResponse.access_token}` fetchMock.mock( 'https://dev-ds-demo.pantheonsite.io/en/jsonapi/node/recipe/33386d32-a87c-44b9-b66b-3dd0bfc38dca', { status: 200, body: recipesResourceData1, }, { overwriteRoutes: true } ); expect( await store.getObject({ Loading @@ -456,9 +454,39 @@ describe('drupalState', () => { id: '33386d32-a87c-44b9-b66b-3dd0bfc38dca', }) ).toEqual(recipesResourceObject1); expect(getAuthHeaderSpy).toHaveBeenCalledTimes(1); expect(fetchMock).toBeCalledTimes(2); }); test('should fetch resource anonymously', async () => { const store: DrupalState = new DrupalState({ apiBase: 'https://dev-ds-demo.pantheonsite.io', apiPrefix: 'jsonapi', clientId: '9adc9c69-fa3b-4c21-9cef-fbd345d1a269', clientSecret: 'mysecret', debug: true, }); const getAuthHeaderSpy = jest.spyOn(store, 'getAuthHeader'); store.setState({ dsApiIndex: indexResponse.links }); fetchMock.mock( 'https://dev-ds-demo.pantheonsite.io/en/jsonapi/node/recipe/33386d32-a87c-44b9-b66b-3dd0bfc38dca', { status: 200, body: recipesResourceData1, }, { overwriteRoutes: true } ); expect(getAuthHeaderSpy).toHaveBeenCalledTimes(0); expect( await store.getObject({ objectName: 'node--recipe', id: '33386d32-a87c-44b9-b66b-3dd0bfc38dca', anon: true, }) ).toEqual(recipesResourceObject1); expect(fetchMock).toBeCalledTimes(1); }); test('A locale is honored if specified', async () => { const store: DrupalState = new DrupalState({ apiBase: 'https://demo-decoupled-bridge.lndo.site', Loading src/types/types.ts +46 −12 Original line number Diff line number Diff line Loading @@ -117,29 +117,63 @@ export interface jsonapiLinkObject { __typename: string; } interface SharedParams { /** * Describes get object parameters. * The name of the object in Drupal * @example 'node--article' */ export interface GetObjectParams { objectName: string; id?: string; /** * If included, the server response is passed to allow DrupalState to set headers * among other things. * @see {@link https://nodejs.org/docs/latest-v16.x/api/http.html#class-httpserverresponse} */ res?: ServerResponse | boolean; /** * A string of Drupal JSON:API parameters. * @example 'include=field_media_image, * Or an instance of DrupalJsonApi Params. * @see {@link https://www.npmjs.com/package/drupal-jsonapi-params} */ params?: string | DrupalJsonApiParams; /** * If true, data will be fetched from Drupal regardless of its existence in state */ refresh?: boolean; /** * If true and valid credentials are passed in when creating the instance of * DrupalState, the request will be made anonymously */ anon?: boolean; /** * @deprecated since 4.0.0 */ query?: string | boolean; } /** * Describes get object parameters. */ export interface GetObjectParams extends SharedParams { /** * The id of the object in Drupal */ id?: string; /** * If true, DrupalState fetches all pages of an object if there is more than one. */ all?: boolean; refresh?: boolean; } /** * Describes get object by Path alias. */ export interface GetObjectByPathParams { objectName: string; export interface GetObjectByPathParams extends SharedParams { /** * The path to the object that Decoupled Router resolves to * @see {@link https://www.drupal.org/project/decoupled_router} * @example '/recipes/fiery-chili-sauce' */ path: string; res?: ServerResponse | boolean; params?: string | DrupalJsonApiParams; query?: string | boolean; refresh?: boolean; } /** Loading web/src/pages/en/getting-objects.md +7 −0 Original line number Diff line number Diff line Loading @@ -70,6 +70,13 @@ const allArticlesFromApi = await store.getObject({ objectName: 'node--ds_example', all: true, }); // If your DrupalState store has valid credentials, all requests are authorized by default // To make a request anonymous, use the `anon` option and set it to true const anonRequest = await store.getObject({ objectName: 'node--recipe', anon: true, }); ``` To better understand the advantages of Drupal State, below we will compare the Loading Loading
src/DrupalState.ts +17 −9 Original line number Diff line number Diff line Loading @@ -231,15 +231,17 @@ class DrupalState { * Fetches data using our fetch method. * @param endpoint the assembled JSON:API endpoint * @param res response object * @param anon make the request anonymously if true * @returns data fetched from JSON:API endpoint */ async fetchData( endpoint: string, res: ServerResponse | boolean = false res: ServerResponse | boolean = false, anon = false ): Promise<TJsonApiBody | void> { let requestInit = {}; let authHeader = ''; if (this.clientId && this.clientSecret) { if (this.clientId && this.clientSecret && !anon) { const headers = new Headers(); authHeader = await this.getAuthHeader(); headers.append('Authorization', authHeader); Loading Loading @@ -288,6 +290,7 @@ class DrupalState { * @param options.res - response object * @param options.params - user provided JSON:API parameter string or DrupalJsonApiParams object * @param options.refresh - a boolean value. If true, ignore local state. * @param options.anon - a boolean value. If true, send the request without the authentication header if valid credentials exist. * @returns a promise containing deserialized JSON:API data for the requested * object * Loading @@ -308,6 +311,7 @@ class DrupalState { res, params, refresh = false, anon = false, query = false, }: GetObjectByPathParams): Promise<PartialState<State> | void> { if (query) { Loading @@ -325,7 +329,7 @@ class DrupalState { // TODO - abstract helper method to assemble requestInit and authHeader let requestInit = {}; let authHeader = ''; if (this.clientId && this.clientSecret) { if (this.clientId && this.clientSecret && !anon) { const headers = new Headers(); authHeader = await this.getAuthHeader(); headers.append('Authorization', authHeader); Loading Loading @@ -394,10 +398,11 @@ class DrupalState { * @remarks The query option was experimental and is now deprecated * @param options.objectName - Name of object to fetch * @param options.id - id of a specific resource * @param options.res response object * @param options.params user provided JSON:API parameter string or DrupalJsonApiParams object * @param options.all a boolean value. If true, fetch all objects in a collection. * @param options.refresh a boolean value. If true, ignore local state. * @param options.res - response object * @param options.params - user provided JSON:API parameter string or DrupalJsonApiParams object * @param options.all - a boolean value. If true, fetch all objects in a collection. * @param options.refresh - a boolean value. If true, ignore local state. * @param options.anon - a boolean value. If true, send the request without the authentication header if valid credentials exist. * @returns a promise containing deserialized JSON:API data for the requested * object * Loading @@ -420,6 +425,7 @@ class DrupalState { params, all = false, refresh = false, anon = false, query = false, }: GetObjectParams): Promise<PartialState<State> | void> { if (query) { Loading Loading @@ -512,7 +518,8 @@ class DrupalState { const resourceData = (await this.fetchData( endpoint, res res, anon )) as keyedResources; const objectResourceState = state[resourceKey]; Loading Loading @@ -572,7 +579,8 @@ class DrupalState { const collectionData = (await this.fetchData( endpoint, res res, anon )) as keyedResources; const fetchedCollectionState = {} as CollectionState; Loading
src/__tests__/drupalState.test.ts +39 −11 Original line number Diff line number Diff line Loading @@ -348,7 +348,7 @@ describe('drupalState', () => { expect(fetchMock).toBeCalledTimes(2); }); test('Re-fetch object if it exists in local storage but refresh is set to true', async () => { test('Re-fetch collection object if it exists in local state but refresh is set to true', async () => { const store: DrupalState = new DrupalState({ apiBase: 'https://dev-ds-demo.pantheonsite.io', apiPrefix: 'jsonapi', Loading Loading @@ -425,15 +425,8 @@ describe('drupalState', () => { clientSecret: 'mysecret', debug: true, }); const getAuthHeaderSpy = jest.spyOn(store, 'getAuthHeader'); store.setState({ dsApiIndex: indexResponse.links }); fetchMock.mock( 'https://dev-ds-demo.pantheonsite.io/en/jsonapi/node/recipe/33386d32-a87c-44b9-b66b-3dd0bfc38dca', { status: 200, body: recipesResourceData1, }, { overwriteRoutes: true } ); fetchMock.mock( { url: 'https://dev-ds-demo.pantheonsite.io/oauth/token', Loading @@ -447,8 +440,13 @@ describe('drupalState', () => { body: tokenResponse, } ); expect(await store['getAuthHeader']()).toEqual( `${tokenResponse.token_type} ${tokenResponse.access_token}` fetchMock.mock( 'https://dev-ds-demo.pantheonsite.io/en/jsonapi/node/recipe/33386d32-a87c-44b9-b66b-3dd0bfc38dca', { status: 200, body: recipesResourceData1, }, { overwriteRoutes: true } ); expect( await store.getObject({ Loading @@ -456,9 +454,39 @@ describe('drupalState', () => { id: '33386d32-a87c-44b9-b66b-3dd0bfc38dca', }) ).toEqual(recipesResourceObject1); expect(getAuthHeaderSpy).toHaveBeenCalledTimes(1); expect(fetchMock).toBeCalledTimes(2); }); test('should fetch resource anonymously', async () => { const store: DrupalState = new DrupalState({ apiBase: 'https://dev-ds-demo.pantheonsite.io', apiPrefix: 'jsonapi', clientId: '9adc9c69-fa3b-4c21-9cef-fbd345d1a269', clientSecret: 'mysecret', debug: true, }); const getAuthHeaderSpy = jest.spyOn(store, 'getAuthHeader'); store.setState({ dsApiIndex: indexResponse.links }); fetchMock.mock( 'https://dev-ds-demo.pantheonsite.io/en/jsonapi/node/recipe/33386d32-a87c-44b9-b66b-3dd0bfc38dca', { status: 200, body: recipesResourceData1, }, { overwriteRoutes: true } ); expect(getAuthHeaderSpy).toHaveBeenCalledTimes(0); expect( await store.getObject({ objectName: 'node--recipe', id: '33386d32-a87c-44b9-b66b-3dd0bfc38dca', anon: true, }) ).toEqual(recipesResourceObject1); expect(fetchMock).toBeCalledTimes(1); }); test('A locale is honored if specified', async () => { const store: DrupalState = new DrupalState({ apiBase: 'https://demo-decoupled-bridge.lndo.site', Loading
src/types/types.ts +46 −12 Original line number Diff line number Diff line Loading @@ -117,29 +117,63 @@ export interface jsonapiLinkObject { __typename: string; } interface SharedParams { /** * Describes get object parameters. * The name of the object in Drupal * @example 'node--article' */ export interface GetObjectParams { objectName: string; id?: string; /** * If included, the server response is passed to allow DrupalState to set headers * among other things. * @see {@link https://nodejs.org/docs/latest-v16.x/api/http.html#class-httpserverresponse} */ res?: ServerResponse | boolean; /** * A string of Drupal JSON:API parameters. * @example 'include=field_media_image, * Or an instance of DrupalJsonApi Params. * @see {@link https://www.npmjs.com/package/drupal-jsonapi-params} */ params?: string | DrupalJsonApiParams; /** * If true, data will be fetched from Drupal regardless of its existence in state */ refresh?: boolean; /** * If true and valid credentials are passed in when creating the instance of * DrupalState, the request will be made anonymously */ anon?: boolean; /** * @deprecated since 4.0.0 */ query?: string | boolean; } /** * Describes get object parameters. */ export interface GetObjectParams extends SharedParams { /** * The id of the object in Drupal */ id?: string; /** * If true, DrupalState fetches all pages of an object if there is more than one. */ all?: boolean; refresh?: boolean; } /** * Describes get object by Path alias. */ export interface GetObjectByPathParams { objectName: string; export interface GetObjectByPathParams extends SharedParams { /** * The path to the object that Decoupled Router resolves to * @see {@link https://www.drupal.org/project/decoupled_router} * @example '/recipes/fiery-chili-sauce' */ path: string; res?: ServerResponse | boolean; params?: string | DrupalJsonApiParams; query?: string | boolean; refresh?: boolean; } /** Loading
web/src/pages/en/getting-objects.md +7 −0 Original line number Diff line number Diff line Loading @@ -70,6 +70,13 @@ const allArticlesFromApi = await store.getObject({ objectName: 'node--ds_example', all: true, }); // If your DrupalState store has valid credentials, all requests are authorized by default // To make a request anonymous, use the `anon` option and set it to true const anonRequest = await store.getObject({ objectName: 'node--recipe', anon: true, }); ``` To better understand the advantages of Drupal State, below we will compare the Loading