Skip to content

Commit

Permalink
Merge pull request #1701 from Shopify/remove-rest-from-remix-future-flag
Browse files Browse the repository at this point in the history
[Feature] Remove rest using a future flag
  • Loading branch information
byrichardpowell authored Nov 19, 2024
2 parents 57f2d80 + 5dddeae commit 1e45455
Show file tree
Hide file tree
Showing 60 changed files with 816 additions and 528 deletions.
9 changes: 9 additions & 0 deletions .changeset/eight-mails-hammer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@shopify/shopify-app-remix': minor
---

Added `removeRest` future flag.

When `removeRest` is `true`, the REST API will no longer be available. Please use the GraphQL API instead. See [Shopify is all-in on graphql](https://www.shopify.com/ca/partners/blog/all-in-on-graphql) for more information.

If your app doesn't use the REST API, you can safely set `removeRest` to `true` and be ready for a future major release. If your app does use the REST API, you should migrate to the GraphQL API and then set `removeRest` to `true`.
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ database.sqlite
packages/**/build
packages/**/*.tgz
tmp/
.vscode/
bundle/
.turbo
.rollup.cache
Expand Down
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"typescript.preferences.importModuleSpecifier": "relative",
}
6 changes: 4 additions & 2 deletions packages/apps/shopify-app-remix/docs/staticPages/admin.doc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,11 @@ const data: LandingTemplateSchema = {
{
type: 'Generic',
anchorLink: 'rest-api',
title: 'Using the REST API',
title: 'Using the REST API (Deprecated)',
sectionContent:
'Once a request is authenticated, `authenticate.admin` will return an `admin` object that contains a REST client that can interact with the [REST Admin API](/docs/api/admin-rest).' +
'**Shopify is [all-in on graphql](https://www.shopify.com/ca/partners/blog/all-in-on-graphql). In the next major release, the REST API will be removed from the `@shopify/shopify-app-remix` package.' +
'If the `removeRest` [future flag](/docs/api/shopify-app-remix/v3/guide-future-flags) is true, then the REST API will not be available.**' +
'\n\nOnce a request is authenticated, `authenticate.admin` will return an `admin` object that contains a REST client that can interact with the [REST Admin API](/docs/api/admin-rest).' +
'\n\nYou can also import a set of resource classes from the `@shopify/shopify-api` package, which is included in `@shopify/shopify-app-remix`.' +
'\n\nThese classes map to the individual REST endpoints, and will be returned under `admin.rest.resources`.',
codeblock: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ const data: LandingTemplateSchema = {
anchorLink: 'breaking-changes',
title: 'Breaking changes',
sectionContent:
'Similarly to unstable APIs, breaking changes will be introduced behind a future flag, but the prefix will be the next major version (e.g. `v3_`).' +
'Similarly to unstable APIs, breaking changes will be introduced behind a future flag.' +
'\n\nThis allows apps to prepare for the next major version ahead of time, and to gradually adopt the new APIs.' +
'\n\nWhen the next major version is released, the future flag will be removed, and the old code it changes will be removed. Apps that adopted the flag before then will continue to work the same way with no new changes.',
},
Expand All @@ -70,6 +70,21 @@ const data: LandingTemplateSchema = {
'\n\nLearn more about this [new embedded app auth strategy](https://shopify.dev/docs/api/shopify-app-remix#embedded-auth-strategy).',
isOptional: true,
},
{
name: 'removeRest',
value: '',
description:
'Methods for interacting with the admin REST API will not be returned\n\n' +
'This affects:\n\n' +
'* `authenticate.admin(request)`\n' +
'* `authenticate.webhook(request)`\n' +
'* `authenticate.flow(request)`\n' +
'* `authenticate.appProxy(request)`\n' +
'* `authenticate.fulfillmentService(request)`\n' +
'* `unauthenticated.admin(shop)`\n\n' +
'Learn more about this change by reading [all-in on graphql](https://www.shopify.com/ca/partners/blog/all-in-on-graphql).',
isOptional: true,
},
],
},
],
Expand Down
9 changes: 9 additions & 0 deletions packages/apps/shopify-app-remix/docs/upcoming_changes.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,12 @@ This package will automatically use token exchange, but that only works if [Shop
Before updating this package in your app, please ensure you've enabled managed installation.

For more details on how this works, please see the [new embedded authorization strategy](../README.md#new-embedded-authorization-strategy) section in the README.


## Removing the REST API

> [!NOTE]
> The `removeRest` future flag removed the REST API.
> If you've already enabled the flag, you don't need to follow these instructions.
The REST API will be removed from this package. Please use the GraphQL API instead. See [Shopify is all-in on graphql](https://www.shopify.com/ca/partners/blog/all-in-on-graphql) for more information.
Original file line number Diff line number Diff line change
@@ -1,122 +1,140 @@
import {Session} from '@shopify/shopify-api';

import {LATEST_API_VERSION} from '..';
import type {AdminApiContext} from '../clients';
import type {
AdminApiContextWithoutRest,
AdminApiContextWithRest,
} from '../clients';

import {mockExternalRequest} from './request-mock';
import {TEST_SHOP} from './const';
import {mockExternalRequest} from './request-mock';

const REQUEST_URL = `https://${TEST_SHOP}/admin/api/${LATEST_API_VERSION}/customers.json`;

export function expectAdminApiClient(
factory: () => Promise<{
admin: AdminApiContext;
admin: AdminApiContextWithRest;
adminWithoutRest?: AdminApiContextWithoutRest;
expectedSession: Session;
actualSession: Session;
}>,
) {
it('REST client can perform GET requests', async () => {
// GIVEN
const {admin} = await factory();
await mockExternalRequest({
request: new Request(REQUEST_URL),
response: new Response(JSON.stringify({customers: []})),
describe('when future.removeRest is falsey there is a REST client', () => {
it('can perform GET requests', async () => {
// GIVEN
const {admin} = await factory();
await mockExternalRequest({
request: new Request(REQUEST_URL),
response: new Response(JSON.stringify({customers: []})),
});

// WHEN
const response = await admin.rest.get({path: 'customers'});

// THEN
expect(response.status).toEqual(200);
expect(await response.json()).toEqual({customers: []});
});

// WHEN
const response = await admin.rest.get({path: 'customers'});

// THEN
expect(response.status).toEqual(200);
expect(await response.json()).toEqual({customers: []});
});

it('REST client can perform POST requests', async () => {
// GIVEN
const {admin} = await factory();
await mockExternalRequest({
request: new Request(REQUEST_URL, {method: 'POST'}),
response: new Response(JSON.stringify({customers: []})),
it('can perform POST requests', async () => {
// GIVEN
const {admin} = await factory();
await mockExternalRequest({
request: new Request(REQUEST_URL, {method: 'POST'}),
response: new Response(JSON.stringify({customers: []})),
});

// WHEN
const response = await admin.rest.post({
path: '/customers.json',
data: '',
});

// THEN
expect(response.status).toEqual(200);
expect(await response.json()).toEqual({customers: []});
});

// WHEN
const response = await admin.rest.post({
path: '/customers.json',
data: '',
it('can perform PUT requests', async () => {
// GIVEN
const {admin} = await factory();
await mockExternalRequest({
request: new Request(REQUEST_URL, {method: 'PUT'}),
response: new Response(JSON.stringify({customers: []})),
});

// WHEN
const response = await admin.rest.put({
path: '/customers.json',
data: '',
});

// THEN
expect(response.status).toEqual(200);
expect(await response.json()).toEqual({customers: []});
});

// THEN
expect(response.status).toEqual(200);
expect(await response.json()).toEqual({customers: []});
});
it('can perform DELETE requests', async () => {
// GIVEN
const {admin} = await factory();
await mockExternalRequest({
request: new Request(REQUEST_URL, {method: 'DELETE'}),
response: new Response(JSON.stringify({customers: []})),
});

it('REST client can perform PUT requests', async () => {
// GIVEN
const {admin} = await factory();
await mockExternalRequest({
request: new Request(REQUEST_URL, {method: 'PUT'}),
response: new Response(JSON.stringify({customers: []})),
});
// WHEN
const response = await admin.rest.delete({path: '/customers.json'});

// WHEN
const response = await admin.rest.put({
path: '/customers.json',
data: '',
// THEN
expect(response.status).toEqual(200);
expect(await response.json()).toEqual({customers: []});
});

// THEN
expect(response.status).toEqual(200);
expect(await response.json()).toEqual({customers: []});
});

it('REST client can perform DELETE requests', async () => {
// GIVEN
const {admin} = await factory();
await mockExternalRequest({
request: new Request(REQUEST_URL, {method: 'DELETE'}),
response: new Response(JSON.stringify({customers: []})),
});

// WHEN
const response = await admin.rest.delete({path: '/customers.json'});
describe('when future.removeRest is truthy', () => {
it('does not include a rest property on the admin object', async () => {
// GIVEN
const {adminWithoutRest} = await factory();

// THEN
expect(response.status).toEqual(200);
expect(await response.json()).toEqual({customers: []});
// THEN
expect(adminWithoutRest).not.toHaveProperty('rest');
});
});

it('GraphQL client can perform requests', async () => {
// GIVEN
const {admin, actualSession} = await factory();
await mockExternalRequest({
request: new Request(
`https://${TEST_SHOP}/admin/api/${LATEST_API_VERSION}/graphql.json`,
{
method: 'POST',
headers: {'X-Shopify-Access-Token': actualSession.accessToken!},
},
),
response: new Response(
JSON.stringify({data: {shop: {name: 'Test shop'}}}),
),
describe('Graphql client', () => {
it('can perform requests', async () => {
// GIVEN
const {admin, actualSession} = await factory();
await mockExternalRequest({
request: new Request(
`https://${TEST_SHOP}/admin/api/${LATEST_API_VERSION}/graphql.json`,
{
method: 'POST',
headers: {'X-Shopify-Access-Token': actualSession.accessToken!},
},
),
response: new Response(
JSON.stringify({data: {shop: {name: 'Test shop'}}}),
),
});

// WHEN
const response = await admin.graphql('{ shop { name } }');

// THEN
expect(response.status).toEqual(200);
expect(await response.json()).toEqual({
data: {shop: {name: 'Test shop'}},
headers: {'Content-Type': ['application/json']},
});
});

// WHEN
const response = await admin.graphql('{ shop { name } }');
it('returns a session object as part of the context', async () => {
// GIVEN
const {expectedSession, actualSession} = await factory();

// THEN
expect(response.status).toEqual(200);
expect(await response.json()).toEqual({
data: {shop: {name: 'Test shop'}},
headers: {'Content-Type': ['application/json']},
// THEN
expect(expectedSession).toEqual(actualSession);
});
});

it('returns a session object as part of the context', async () => {
// GIVEN
const {expectedSession, actualSession} = await factory();

// THEN
expect(expectedSession).toEqual(actualSession);
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,58 @@ import {setUpValidSession} from './setup-valid-session';
import {testConfig} from './test-config';

export async function setUpEmbeddedFlow() {
const shopify = shopifyApp(
testConfig({
future: {unstable_newEmbeddedAuthStrategy: false},
const shopify = shopifyApp({
...testConfig({
restResources,
}),
future: {
removeRest: false,
unstable_newEmbeddedAuthStrategy: false,
wip_optionalScopesApi: true,
},
});
const expectedSession = await setUpValidSession(shopify.sessionStorage);

const {token} = getJwt();
const request = new Request(
`${APP_URL}?embedded=1&shop=${TEST_SHOP}&host=${BASE64_HOST}&id_token=${token}`,
);

const result = await shopify.authenticate.admin(request);

return {
shopify,
expectedSession,
...result,
request,
};
}

export async function setUpEmbeddedFlowWithRemoveRestFlag() {
const shopify = shopifyApp({
...testConfig({
restResources,
}),
future: {
removeRest: true,
unstable_newEmbeddedAuthStrategy: false,
wip_optionalScopesApi: true,
},
});

const expectedSession = await setUpValidSession(shopify.sessionStorage);

const {token} = getJwt();
const request = new Request(
`${APP_URL}?embedded=1&shop=${TEST_SHOP}&host=${BASE64_HOST}&id_token=${token}`,
);

const result = await shopify.authenticate.admin(request);

return {
shopify,
expectedSession,
...(await shopify.authenticate.admin(request)),
...result,
request,
};
}
Loading

0 comments on commit 1e45455

Please sign in to comment.