Skip to content

Instantly share code, notes, and snippets.

@mshick
Last active September 24, 2021 21:59
Show Gist options
  • Save mshick/7ed492b8cb695c7dc23cc70ddf99ab48 to your computer and use it in GitHub Desktop.
Save mshick/7ed492b8cb695c7dc23cc70ddf99ab48 to your computer and use it in GitHub Desktop.
import {
FieldNode,
OperationDefinitionNode,
SelectionNode,
SelectionSetNode,
getVisitFn,
Kind,
ASTNode,
DocumentNode,
visit,
BREAK,
GraphQLResolveInfo,
} from "graphql";
import { Path } from "graphql/jsutils/Path";
import {
ProjectSchema,
ContentSchema,
Query,
Shape,
getRefShapeName,
splitMapping,
findMapping,
createSchemaPropertyAccessor,
} from "@takeshape/schema";
import { ASTKindToNode } from "graphql/language/ast";
import { Visitor, VisitFn } from "graphql/language/visitor";
import { isOperationDefinitionNode } from "../types";
import { BadDataError } from "@takeshape/errors";
export function getOperation(doc: DocumentNode): OperationDefinitionNode {
const operationDefinition = doc.definitions[0];
if (!isOperationDefinitionNode(operationDefinition)) {
throw new BadDataError("Invalid selectionSet");
}
return operationDefinition;
}
function hasName(node: SelectionNode, name: string): boolean {
return (
("name" in node && node.name.value === name) ||
("alias" in node && node.alias?.value === name)
);
}
export function pathToOperationPath(path: Path): string[] {
if (path.prev) {
const result = pathToOperationPath(path.prev);
return typeof path.key === "string" ? result.concat(path.key) : result;
}
return typeof path.key === "string" ? [path.key] : [];
}
export function followPath(
selection: { selectionSet?: SelectionSetNode },
path: string[],
i = 0
): SelectionNode | undefined {
if (selection.selectionSet) {
for (const node of selection.selectionSet.selections) {
if (hasName(node, path[i])) {
if (i === path.length - 1) {
return node;
}
return "selectionSet" in node
? followPath(node, path, i + 1)
: undefined;
}
}
}
}
export function getSelectionSetNames(node: FieldNode): string[] {
const result: string[] = [];
if (node.selectionSet) {
for (const selection of node.selectionSet.selections) {
if ("name" in selection && selection.name.value) {
result.push(selection.name.value);
}
}
}
return result;
}
export function getParentSelectionNames(
op: OperationDefinitionNode,
path: Path
): string[] {
const opPath = pathToOperationPath(path);
const parentOpPath = opPath.slice(0, opPath.length - 1);
const node = followPath(op, parentOpPath);
return node ? getSelectionSetNames(node as FieldNode) : [];
}
function isNode(maybeNode: any): maybeNode is ASTNode {
return Boolean(maybeNode && typeof maybeNode.kind === "string");
}
export interface SchemaInfo {
path: string[];
propertySchema?: ContentSchema;
shape?: Shape;
operationConfig?: Query;
operationType?: "query" | "mutation" | "subscription";
projectSchema: ProjectSchema;
}
type VisitFnParams = Parameters<VisitFn<any>>;
export interface SchemaInfoVisitor {
enter(
node: VisitFnParams[0],
key: VisitFnParams[1],
parent: VisitFnParams[2],
path: VisitFnParams[3],
ancestors: VisitFnParams[4]
): void;
leave(
node: VisitFnParams[0],
key: VisitFnParams[1],
parent: VisitFnParams[2],
path: VisitFnParams[3],
ancestors: VisitFnParams[4]
): void;
getInfo(): SchemaInfo;
}
interface ShapeStackItem {
shape: Shape;
path: string[];
}
// export function createSchemaInfoVisitor(projectSchema: ProjectSchema, shapeName: string): SchemaInfoVisitor {
// const createShapePropertyAccessor = createSchemaPropertyAccessor(projectSchema);
// let path: string[] = [];
// let propertySchema: ContentSchema | undefined;
// let shape: Shape | undefined = projectSchema.shapes[shapeName];
// const shapeStackItem: ShapeStackItem | undefined = shape && {
// shape,
// path: ['shapes', shape.name, 'schema', 'properties']
// };
// const shapeStack: ShapeStackItem[] = shapeStackItem ? [shapeStackItem] : [];
// return {
// // eslint-disable-next-line max-params
// enter(node: ASTNode, key, parent, path, ancestors) {
// console.log('==================================entering');
// if (node.kind === Kind.FIELD) {
// if (shapeStack.length) {
// console.log({key, parent, path, ancestors});
// // console.log(node);
// ({shape} = shapeStack[shapeStack.length - 1]);
// console.log(shape.name, node.name.value);
// const schemaPropertyAccessor = createShapePropertyAccessor(shape.schema);
// path = ['shapes', shape.name, 'schema', 'properties', node.name.value];
// propertySchema = schemaPropertyAccessor.getValue(node.name.value);
// if (propertySchema) {
// const shapeName = getRefShapeName(projectSchema, propertySchema?.items ?? propertySchema);
// if (shapeName) {
// // console.log('pushing-------', shapeName);
// shapeStack.push({shape: projectSchema.shapes[shapeName]});
// }
// }
// }
// }
// },
// leave(node: ASTNode, key): void {
// // console.log('>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>leaving');
// // console.log({key});
// // if (node.kind === Kind.FIELD) {
// // console.log(node);
// // console.log(node.name.value);
// // }
// if (node.kind === Kind.SELECTION_SET) {
// // console.log(node.selections[0].name);
// shapeStack.pop();
// }
// },
// getInfo(): SchemaInfo {
// return {
// projectSchema,
// path,
// propertySchema,
// shape
// };
// }
// };
// }
export function createSchemaInfoVisitor(
projectSchema: ProjectSchema,
shapeName: string
): SchemaInfoVisitor {
let path: string[] = [];
let propertySchema: ContentSchema | undefined;
let shape: Shape | undefined = projectSchema.shapes[shapeName];
const shapeStack: Shape[] = shape ? [shape] : [];
const createShapePropertyAccessor =
createSchemaPropertyAccessor(projectSchema);
return {
enter(node: ASTNode, key, parent, path, ancestors) {
// console.log('ENTERING-----------------');
// console.log('PARENT>>>');
// console.log(parent);
// console.log('ANCESTORS>>>>');
// console.log(ancestors[0]?.selections);
if (node.kind === Kind.FIELD) {
if (shapeStack.length) {
shape = shapeStack[shapeStack.length - 1];
// console.log({shapeName: shape.name});
const schemaPropertyAccessor = createShapePropertyAccessor(
shape.schema
);
path = [
"shapes",
shape.name,
"schema",
"properties",
node.name.value,
];
// console.log(path);
propertySchema = schemaPropertyAccessor.getValue(node.name.value);
if (propertySchema) {
const shapeName = getRefShapeName(
projectSchema,
propertySchema?.items ?? propertySchema
);
if (shapeName) {
shapeStack.push(projectSchema.shapes[shapeName]);
}
}
}
}
},
leave(node: ASTNode, key, parent, path, ancestors): void {
// console.log('----------------LEAVING');
// console.log('PARENT>>>');
// console.log(parent);
// console.log('ANCESTORS>>>>');
// console.log(ancestors[0]?.selections);
if (node.kind === Kind.SELECTION_SET) {
shapeStack.pop();
}
},
getInfo(): SchemaInfo {
return {
projectSchema,
path,
propertySchema,
shape,
};
},
};
}
export function visitWithSchemaInfo(
schemaInfoVisitor: SchemaInfoVisitor,
visitor: Visitor<ASTKindToNode>
): Visitor<ASTKindToNode> {
return {
// eslint-disable-next-line max-params
enter(node, key, parent, path, visitors) {
schemaInfoVisitor.enter(node, key, parent, path, visitors);
const fn = getVisitFn(visitor, node.kind, false);
if (fn) {
let result = fn.apply(visitor, arguments as any);
if (result !== undefined) {
schemaInfoVisitor.leave(node, key, parent, path, visitors);
if (isNode(result)) {
schemaInfoVisitor.enter(result, key, parent, path, visitors);
}
}
return result;
}
},
// eslint-disable-next-line max-params
leave(node, key, parent, path, visitors) {
const fn = getVisitFn(visitor, node.kind, true);
let result;
if (fn) {
result = fn.apply(visitor, arguments as any);
}
schemaInfoVisitor.leave(node, key, parent, path, visitors);
return result;
},
};
}
/**
* Transform the query selection set to match the remote schema
* @param info
* @param projectSchema
* @param serviceId
*/
export function transformSelectionSet(
info: Pick<GraphQLResolveInfo, "fieldNodes" | "returnType">,
projectSchema: ProjectSchema,
serviceId: string
): SelectionSetNode {
const shapeName = info.returnType.toString();
const schemaInfoVisitor = createSchemaInfoVisitor(projectSchema, shapeName);
const visitor = visitWithSchemaInfo(schemaInfoVisitor, {
Field: {
enter(node) {
const { path } = schemaInfoVisitor.getInfo();
console.log("///////////////////////////path");
console.log(path);
const { propertySchema } = schemaInfoVisitor.getInfo();
if (propertySchema) {
const mapping = findMapping(propertySchema, (mapping) =>
mapping.startsWith(serviceId)
);
if (mapping) {
return {
...node,
name: { ...node.name, value: splitMapping(mapping)[1] },
};
}
return null; // delete this node
}
},
},
});
const selectionSet = info.fieldNodes[0]?.selectionSet;
if (!selectionSet) {
throw new Error("Missing selectionSet");
}
return visit(selectionSet, visitor);
}
export const PARENT_FRAGMENT = "_parentFragment";
export function hasParentFragment(
selectionSet: SelectionSetNode
): string[] | undefined {
let path: string[] = [];
let result = false;
visit(selectionSet, {
Field: {
enter(node) {
path.push(node.alias ? node.alias.value : node.name.value);
},
leave() {
path.pop();
},
},
FragmentSpread: {
enter(node) {
if (node.name.value === PARENT_FRAGMENT) {
result = true;
return BREAK;
}
},
},
});
return result ? path : undefined;
}
function parentFragmentPredicate(selection: SelectionNode): boolean {
return (
selection.kind === Kind.FRAGMENT_SPREAD &&
selection.name.value === PARENT_FRAGMENT
);
}
function parentFragmentNotPredicate(selection: SelectionNode): boolean {
return !parentFragmentPredicate(selection);
}
export function inlineParentFragment(
selectionSet: SelectionSetNode,
parentSelectionSet: SelectionSetNode
): SelectionSetNode {
return visit(selectionSet, {
SelectionSet(node) {
for (const selection of node.selections) {
if (parentFragmentPredicate(selection)) {
return {
...node,
selections: [
...parentSelectionSet.selections,
...node.selections.filter(parentFragmentNotPredicate),
],
};
}
}
},
});
}
{
"projectId": "4c54da75-a240-4e50-8343-bd1e8eafcdbb",
"dataKey": "AQIDAHhcZWXOdlBglkxBhI23ElO/clC/kuw5ynjXDSqJgfTz8gFW5RUbr2gMJW3lg7GcBv3HAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMTQd9cbrxRTzTb3wuAgEQgDtorC3D/DOEoAguUH+McQKuH70cJWf7QnG1l0KQpmQDXwYY7z0+JYlEKyeXcqxOV+Lmp3JLmeesn6YbIQ==",
"version": 16,
"created": "2021-09-22T21:40:56.849Z",
"updated": "2021-09-24T18:42:24.966Z",
"defaultLocale": "en-us",
"locales": ["en-us"],
"apiVersion": "2",
"schemaVersion": "3.6.0",
"queries": {
"getShopifyProductDetails": {
"shape": "michaels-store-takeshape:Product",
"resolver": {
"id": "product",
"name": "graphql:query",
"service": "michaels-store-takeshape",
"options": {
"fieldName": "product",
"selectionSet": "{ ..._parentFragment metafields(first: 10) { edges { node { id namespace ownerType value description key } } } }"
},
"argsMapping": { "id": [["get", { "path": "args.id" }]] }
},
"description": "Returns Shopify Details",
"args": "ShopifyProductDetailsInput"
},
"getProduct": {
"shape": "Product",
"resolver": {
"name": "takeshape:get",
"service": "takeshape:local",
"options": { "model": "Product" }
},
"description": "Get a Product by ID",
"args": "TSGetArgs<Product>"
},
"getProductList": {
"shape": "PaginatedList<Product>",
"resolver": {
"name": "takeshape:list",
"service": "takeshape:local",
"options": { "model": "Product" }
},
"description": "Returns a list Product in natural order.",
"args": "TSListArgs<Product>"
}
},
"mutations": {
"updateProduct": {
"shape": "UpdateResult<Product>",
"resolver": {
"compose": [
{
"id": "createNewShopifyObject",
"if": "isEmpty(args.input.shopifyProductId) && !isEmpty(args.input.shopifyProduct)",
"argsMapping": {
"input.descriptionHtml": [
["get", { "path": "args.input.shopifyProduct.descriptionHtml" }]
],
"input.handle": [
["get", { "path": "args.input.shopifyProduct.handle" }]
],
"input.redirectNewHandle": [
[
"get",
{ "path": "args.input.shopifyProduct.redirectNewHandle" }
]
],
"input.productType": [
["get", { "path": "args.input.shopifyProduct.productType" }]
],
"input.tags": [
["get", { "path": "args.input.shopifyProduct.tags" }]
],
"input.templateSuffix": [
["get", { "path": "args.input.shopifyProduct.templateSuffix" }]
],
"input.giftCard": [
["get", { "path": "args.input.shopifyProduct.giftCard" }]
],
"input.giftCardTemplateSuffix": [
[
"get",
{ "path": "args.input.shopifyProduct.giftCardTemplateSuffix" }
]
],
"input.title": [
["get", { "path": "args.input.shopifyProduct.title" }]
],
"input.vendor": [
["get", { "path": "args.input.shopifyProduct.vendor" }]
],
"input.bodyHtml": [
["get", { "path": "args.input.shopifyProduct.bodyHtml" }]
]
},
"name": "graphql:mutation",
"service": "michaels-store-takeshape",
"options": {
"fieldName": "productCreate",
"selectionSet": "{ product { id } }"
}
},
{
"id": "editTakeshape",
"argsMapping": {
"input": [["get", { "path": "args.input" }]],
"input.shopifyProductId": [
["get", { "path": "steps.createNewShopifyObject.product.id" }],
["get", { "path": "args.input.shopifyProductId" }]
]
},
"name": "takeshape:update",
"service": "takeshape:local",
"options": { "model": "Product" }
},
{
"id": "updateExistingShopifyObject",
"if": "!isEmpty(args.input.shopifyProductId) && !isEmpty(args.input.shopifyProduct)",
"argsMapping": {
"input.descriptionHtml": [
["get", { "path": "args.input.shopifyProduct.descriptionHtml" }]
],
"input.handle": [
["get", { "path": "args.input.shopifyProduct.handle" }]
],
"input.redirectNewHandle": [
[
"get",
{ "path": "args.input.shopifyProduct.redirectNewHandle" }
]
],
"input.productType": [
["get", { "path": "args.input.shopifyProduct.productType" }]
],
"input.tags": [
["get", { "path": "args.input.shopifyProduct.tags" }]
],
"input.templateSuffix": [
["get", { "path": "args.input.shopifyProduct.templateSuffix" }]
],
"input.giftCard": [
["get", { "path": "args.input.shopifyProduct.giftCard" }]
],
"input.giftCardTemplateSuffix": [
[
"get",
{ "path": "args.input.shopifyProduct.giftCardTemplateSuffix" }
]
],
"input.title": [
["get", { "path": "args.input.shopifyProduct.title" }]
],
"input.vendor": [
["get", { "path": "args.input.shopifyProduct.vendor" }]
],
"input.bodyHtml": [
["get", { "path": "args.input.shopifyProduct.bodyHtml" }]
],
"input.id": [["get", { "path": "args.input.shopifyProductId" }]]
},
"name": "graphql:mutation",
"service": "michaels-store-takeshape",
"options": {
"fieldName": "productUpdate",
"selectionSet": "{ product { id } }"
}
}
],
"resultsMapping": {
"result": [["get", { "path": "steps.editTakeshape.result" }]]
}
},
"description": "Update Product. If the input has Shopify values and a Shopify ID, the Shopify product with that ID is updated.\nIf the input has Shopify values and no Shopify ID, a Shopify product is created.",
"args": "UpdateArgs<ProductInterface>"
},
"createProduct": {
"shape": "CreateResult<Product>",
"resolver": {
"compose": [
{
"id": "shopifyCreate",
"if": "!isEmpty(args.input.shopifyProduct)",
"argsMapping": {
"input.descriptionHtml": [
["get", { "path": "args.input.shopifyProduct.descriptionHtml" }]
],
"input.handle": [
["get", { "path": "args.input.shopifyProduct.handle" }]
],
"input.redirectNewHandle": [
[
"get",
{ "path": "args.input.shopifyProduct.redirectNewHandle" }
]
],
"input.productType": [
["get", { "path": "args.input.shopifyProduct.productType" }]
],
"input.tags": [
["get", { "path": "args.input.shopifyProduct.tags" }]
],
"input.templateSuffix": [
["get", { "path": "args.input.shopifyProduct.templateSuffix" }]
],
"input.giftCard": [
["get", { "path": "args.input.shopifyProduct.giftCard" }]
],
"input.giftCardTemplateSuffix": [
[
"get",
{ "path": "args.input.shopifyProduct.giftCardTemplateSuffix" }
]
],
"input.title": [
["get", { "path": "args.input.shopifyProduct.title" }]
],
"input.vendor": [
["get", { "path": "args.input.shopifyProduct.vendor" }]
],
"input.bodyHtml": [
["get", { "path": "args.input.shopifyProduct.bodyHtml" }]
]
},
"name": "graphql:mutation",
"service": "michaels-store-takeshape",
"options": {
"fieldName": "productCreate",
"selectionSet": "{ product { id } }"
}
},
{
"id": "creatingShopifyObject",
"if": "!isEmpty(args.input.shopifyProduct)",
"argsMapping": {
"input": [["get", { "path": "args.input" }]],
"input.shopifyProductId": [
["get", { "path": "steps.shopifyCreate.product.id" }]
]
},
"name": "takeshape:create",
"service": "takeshape:local",
"options": { "model": "Product" }
},
{
"id": "notCreatingShopifyObject",
"if": "isEmpty(args.input.shopifyProduct)",
"argsMapping": { "input": [["get", { "path": "args.input" }]] },
"name": "takeshape:create",
"service": "takeshape:local",
"options": { "model": "Product" }
}
],
"resultsMapping": {
"result": [
["get", { "path": "steps.creatingShopifyObject.result" }],
["get", { "path": "steps.notCreatingShopifyObject.result" }]
]
}
},
"description": "Create Product. If Shopify values are provided, a Shopify product is also created and the new product ID is saved.",
"args": "CreateArgs<ProductInterface>"
},
"duplicateProduct": {
"shape": "DuplicateResult<Product>",
"resolver": {
"name": "takeshape:duplicate",
"service": "takeshape:local",
"options": { "model": "Product" }
},
"description": "Duplicate Product",
"args": "DuplicateArgs<Product>"
},
"deleteProduct": {
"shape": "DeleteResult<Product>",
"resolver": {
"name": "takeshape:delete",
"service": "takeshape:local",
"options": { "model": "Product" }
},
"description": "Delete Product",
"args": "DeleteArgs<Product>"
}
},
"shapes": {
"ProductInterface": {
"id": "ProductInterface",
"name": "ProductInterface",
"title": "ProductInterface",
"description": "ProductInterface",
"schema": {
"type": "object",
"properties": {
"_id": { "type": "string" },
"_version": { "type": "number" },
"_status": { "type": "string", "@workflow": "default" },
"name": {
"title": "Name",
"type": "string",
"description": "Initialized with title from shopify"
},
"shopifyProductId": { "title": "Service ID", "type": "string" },
"shopifyProduct": { "@ref": "michaels-store-takeshape:ProductInput" }
},
"required": []
}
},
"ProductAttributes": {
"id": "attribute-id",
"name": "ProductAttributes",
"title": "ProductAttributes",
"schema": {
"type": "object",
"properties": {
"name": { "type": "string" },
"description": { "type": "string" },
"type": { "type": "string" },
"value": { "type": "string" }
}
}
},
"ShopifyProductDetailsInput": {
"id": "ShopifyProductDetailsInput",
"name": "ShopifyProductDetailsInput",
"title": "ShopifyProductDetailsInput",
"schema": {
"type": "object",
"properties": { "id": { "type": "string" } },
"required": ["id"]
}
},
"Character": {
"id": "Character",
"name": "Character",
"title": "Character",
"schema": {
"type": "object",
"properties": { "name": { "type": "string" } }
}
},
"Shopify_Product": {
"name": "Shopify_Product",
"title": "Shopify_Product",
"id": "Shopify_Product",
"description": "The Product resource lets you manage products in a merchant’s store. You can use [ProductVariants](https://shopify.dev/docs/admin-api/graphql/reference/products-and-collections/productvariant)\nto create or update different versions of the same product. You can also add or update product [Media](https://shopify.dev/docs/admin-api/graphql/reference/products-and-collections/media).\nProducts can be organized by grouping them into a [Collection](https://shopify.dev/docs/admin-api/graphql/reference/products-and-collections/collection).",
"schema": {
"type": "object",
"properties": {
"character": {
"$ref": "#/shapes/Character/schema",
"type": "object",
"title": "Character",
"@resolver": {
"name": "rest:post",
"service": "rick",
"options": { "path": "/character/1" }
}
},
"attributes": {
"items": { "$ref": "#/shapes/ProductAttributes/schema" },
"type": "array",
"@resolver": {
"resultsMapping": [
[
"expressionEval",
{
"expression": "map(compose(compose(zipObject(['description', 'id', 'name', 'namespace', 'type', 'value']), map(get('[1]'))), sortBy('[0]'), toPairs, get('node')), get('metafields.edges', steps[0]))"
}
]
],
"name": "graphql:query",
"service": "michaels-store-takeshape",
"options": {
"fieldName": "product",
"selectionSet": "{ metafields(first: 10) { edges { node { id namespace ownerType value description key } } } }"
},
"argsMapping": { "id": [["get", { "path": "source.id" }]] }
}
},
"availablePublicationCount": {
"type": "integer",
"@mapping": "michaels-store-takeshape:Product.availablePublicationCount",
"description": "The number of publications a resource is published to without feedback errors."
},
"bodyHtml": {
"type": "string",
"@mapping": "michaels-store-takeshape:Product.bodyHtml",
"@deprecationReason": "Use `descriptionHtml` instead",
"description": "The description of the product, complete with HTML formatting."
},
"collections": {
"@ref": "michaels-store-takeshape:CollectionConnection",
"@mapping": "michaels-store-takeshape:Product.collections",
"@args": {
"type": "object",
"properties": {
"first": {
"type": "integer",
"description": "Returns up to the first `n` elements from the list."
},
"after": {
"type": "string",
"description": "Returns the elements that come after the specified cursor."
},
"last": {
"type": "integer",
"description": "Returns up to the last `n` elements from the list."
},
"before": {
"type": "string",
"description": "Returns the elements that come before the specified cursor."
},
"reverse": {
"type": "boolean",
"description": "Reverse the order of the underlying list.",
"default": false
},
"sortKey": {
"description": "Sort the underlying list by the given key.",
"enum": ["TITLE", "UPDATED_AT", "ID", "RELEVANCE"],
"default": "ID"
},
"query": {
"type": "string",
"description": "Supported filter parameters:\n - `collection_type`\n - `publishable_status`\n - `published_status`\n - `title`\n - `updated_at`\n\nSee the detailed [search syntax](https://help.shopify.com/api/getting-started/search-syntax)\nfor more information about using filters."
}
}
},
"description": "A list of the collections that include the product."
},
"createdAt": {
"description": "The date and time ([ISO 8601 format](http://en.wikipedia.org/wiki/ISO_8601)) when the product was created.",
"@tag": "DateTime",
"@mapping": "michaels-store-takeshape:Product.createdAt"
},
"defaultCursor": {
"type": "string",
"@mapping": "michaels-store-takeshape:Product.defaultCursor",
"description": "A default cursor that returns the single next record, sorted ascending by ID."
},
"description": {
"type": "string",
"@mapping": "michaels-store-takeshape:Product.description",
"@args": {
"type": "object",
"properties": {
"truncateAt": {
"type": "integer",
"description": "Truncates string after the given length."
}
}
},
"description": "A stripped description of the product, single line with HTML tags removed."
},
"descriptionHtml": {
"description": "The description of the product, complete with HTML formatting.",
"@tag": "HTML",
"@mapping": "michaels-store-takeshape:Product.descriptionHtml"
},
"descriptionPlainSummary": {
"type": "string",
"@mapping": "michaels-store-takeshape:Product.descriptionPlainSummary",
"@deprecationReason": "Use `description` instead",
"description": "Stripped description of the product, single line with HTML tags removed.\nTruncated to 60 characters."
},
"featuredImage": {
"@ref": "michaels-store-takeshape:Image",
"@mapping": "michaels-store-takeshape:Product.featuredImage",
"description": "The featured image for the product."
},
"featuredMedia": {
"@ref": "michaels-store-takeshape:Media",
"@mapping": "michaels-store-takeshape:Product.featuredMedia",
"description": "The featured media for the product."
},
"feedback": {
"@ref": "michaels-store-takeshape:ResourceFeedback",
"@mapping": "michaels-store-takeshape:Product.feedback",
"description": "Information about the product that's provided through resource feedback."
},
"giftCardTemplateSuffix": {
"type": "string",
"@mapping": "michaels-store-takeshape:Product.giftCardTemplateSuffix",
"description": "The theme template used when viewing the gift card in a store."
},
"handle": {
"type": "string",
"@mapping": "michaels-store-takeshape:Product.handle",
"description": "A unique human-friendly string of the product's title."
},
"hasOnlyDefaultVariant": {
"type": "boolean",
"@mapping": "michaels-store-takeshape:Product.hasOnlyDefaultVariant",
"description": "Whether the product has only a single variant with the default option and value."
},
"hasOutOfStockVariants": {
"type": "boolean",
"@mapping": "michaels-store-takeshape:Product.hasOutOfStockVariants",
"description": "Whether the product has out of stock variants."
},
"id": {
"type": "string",
"@tag": "id",
"@mapping": "michaels-store-takeshape:Product.id",
"description": "A globally-unique identifier."
},
"images": {
"@ref": "michaels-store-takeshape:ImageConnection",
"@mapping": "michaels-store-takeshape:Product.images",
"@args": {
"type": "object",
"properties": {
"first": {
"type": "integer",
"description": "Returns up to the first `n` elements from the list."
},
"after": {
"type": "string",
"description": "Returns the elements that come after the specified cursor."
},
"last": {
"type": "integer",
"description": "Returns up to the last `n` elements from the list."
},
"before": {
"type": "string",
"description": "Returns the elements that come before the specified cursor."
},
"reverse": {
"type": "boolean",
"description": "Reverse the order of the underlying list.",
"default": false
},
"sortKey": {
"description": "Sort the underlying list by the given key.",
"enum": ["CREATED_AT", "POSITION", "ID", "RELEVANCE"],
"default": "POSITION"
},
"maxWidth": {
"type": "integer",
"description": "Image width in pixels between 1 and 2048. This argument is deprecated: Use `maxWidth` on `Image.transformedSrc` instead."
},
"maxHeight": {
"type": "integer",
"description": "Image height in pixels between 1 and 2048. This argument is deprecated: Use\n`maxHeight` on `Image.transformedSrc` instead."
},
"crop": {
"description": "Crops the image according to the specified region. This argument is\ndeprecated: Use `crop` on `Image.transformedSrc` instead.",
"enum": ["CENTER", "TOP", "BOTTOM", "LEFT", "RIGHT"]
},
"scale": {
"type": "integer",
"description": "Image size multiplier for high-resolution retina displays. Must be between 1\nand 3. This argument is deprecated: Use `scale` on `Image.transformedSrc` instead.",
"default": "1"
}
}
},
"description": "The images associated with the product."
},
"inCollection": {
"type": "boolean",
"@mapping": "michaels-store-takeshape:Product.inCollection",
"@args": {
"type": "object",
"properties": {
"id": {
"type": "string",
"@tag": "id",
"description": "The ID of the collection to check."
}
},
"required": ["id"]
},
"description": "Whether the product is in a given collection."
},
"isGiftCard": {
"type": "boolean",
"@mapping": "michaels-store-takeshape:Product.isGiftCard",
"description": "Whether the product is a gift card."
},
"legacyResourceId": {
"description": "The ID of the corresponding resource in the REST Admin API.",
"@tag": "UnsignedInt64",
"@mapping": "michaels-store-takeshape:Product.legacyResourceId"
},
"media": {
"@ref": "michaels-store-takeshape:MediaConnection",
"@mapping": "michaels-store-takeshape:Product.media",
"@args": {
"type": "object",
"properties": {
"first": {
"type": "integer",
"description": "Returns up to the first `n` elements from the list."
},
"after": {
"type": "string",
"description": "Returns the elements that come after the specified cursor."
},
"last": {
"type": "integer",
"description": "Returns up to the last `n` elements from the list."
},
"before": {
"type": "string",
"description": "Returns the elements that come before the specified cursor."
},
"reverse": {
"type": "boolean",
"description": "Reverse the order of the underlying list.",
"default": false
},
"sortKey": {
"description": "Sort the underlying list by the given key.",
"enum": ["POSITION", "ID", "RELEVANCE"],
"default": "POSITION"
}
}
},
"description": "The media associated with the product. This can include images, 3D models, or videos."
},
"mediaCount": {
"type": "integer",
"@mapping": "michaels-store-takeshape:Product.mediaCount",
"description": "Total count of media belonging to a product."
},
"metafield": {
"@ref": "michaels-store-takeshape:Metafield",
"@mapping": "michaels-store-takeshape:Product.metafield",
"@args": {
"type": "object",
"properties": {
"namespace": {
"type": "string",
"description": "The namespace for the metafield."
},
"key": {
"type": "string",
"description": "The key for the metafield."
}
},
"required": ["namespace", "key"]
},
"description": "Returns a metafield by namespace and key that belongs to the resource."
},
"metafields": {
"@ref": "michaels-store-takeshape:MetafieldConnection",
"@mapping": "michaels-store-takeshape:Product.metafields",
"@args": {
"type": "object",
"properties": {
"namespace": {
"type": "string",
"description": "The metafield namespace to filter by."
},
"first": {
"type": "integer",
"description": "Returns up to the first `n` elements from the list."
},
"after": {
"type": "string",
"description": "Returns the elements that come after the specified cursor."
},
"last": {
"type": "integer",
"description": "Returns up to the last `n` elements from the list."
},
"before": {
"type": "string",
"description": "Returns the elements that come before the specified cursor."
},
"reverse": {
"type": "boolean",
"description": "Reverse the order of the underlying list.",
"default": false
}
}
},
"description": "List of metafields that belong to the resource."
},
"onlineStorePreviewUrl": {
"description": "The online store preview URL.",
"@tag": "URL",
"@mapping": "michaels-store-takeshape:Product.onlineStorePreviewUrl"
},
"onlineStoreUrl": {
"description": "The online store URL for the product.\nA value of `null` indicates that the product is not published to the Online Store sales channel.",
"@tag": "URL",
"@mapping": "michaels-store-takeshape:Product.onlineStoreUrl"
},
"options": {
"type": "array",
"items": { "@ref": "michaels-store-takeshape:ProductOption" },
"@mapping": "michaels-store-takeshape:Product.options",
"@args": {
"type": "object",
"properties": {
"first": {
"type": "integer",
"description": "Truncate the array result to this size."
}
}
},
"description": "A list of product options. The limit is specified by Shop.resourceLimits.maxProductOptions."
},
"priceRange": {
"@ref": "michaels-store-takeshape:ProductPriceRange",
"@mapping": "michaels-store-takeshape:Product.priceRange",
"@deprecationReason": "Deprecated in API version 2020-10. Use `priceRangeV2` instead.",
"description": "The price range of the product."
},
"priceRangeV2": {
"@ref": "michaels-store-takeshape:ProductPriceRangeV2",
"@mapping": "michaels-store-takeshape:Product.priceRangeV2",
"description": "The price range of the product with prices formatted as decimals."
},
"privateMetafield": {
"@ref": "michaels-store-takeshape:PrivateMetafield",
"@mapping": "michaels-store-takeshape:Product.privateMetafield",
"@args": {
"type": "object",
"properties": {
"namespace": {
"type": "string",
"description": "The namespace for the private metafield."
},
"key": {
"type": "string",
"description": "The key for the private metafield."
}
},
"required": ["namespace", "key"]
},
"description": "Returns a private metafield by namespace and key that belongs to the resource."
},
"privateMetafields": {
"@ref": "michaels-store-takeshape:PrivateMetafieldConnection",
"@mapping": "michaels-store-takeshape:Product.privateMetafields",
"@args": {
"type": "object",
"properties": {
"namespace": {
"type": "string",
"description": "Filter the private metafields by namespace."
},
"first": {
"type": "integer",
"description": "Returns up to the first `n` elements from the list."
},
"after": {
"type": "string",
"description": "Returns the elements that come after the specified cursor."
},
"last": {
"type": "integer",
"description": "Returns up to the last `n` elements from the list."
},
"before": {
"type": "string",
"description": "Returns the elements that come before the specified cursor."
},
"reverse": {
"type": "boolean",
"description": "Reverse the order of the underlying list.",
"default": false
}
}
},
"description": "List of private metafields that belong to the resource."
},
"productPublications": {
"@ref": "michaels-store-takeshape:ProductPublicationConnection",
"@mapping": "michaels-store-takeshape:Product.productPublications",
"@deprecationReason": "Use `resourcePublications` instead",
"@args": {
"type": "object",
"properties": {
"first": {
"type": "integer",
"description": "Returns up to the first `n` elements from the list."
},
"after": {
"type": "string",
"description": "Returns the elements that come after the specified cursor."
},
"last": {
"type": "integer",
"description": "Returns up to the last `n` elements from the list."
},
"before": {
"type": "string",
"description": "Returns the elements that come before the specified cursor."
},
"reverse": {
"type": "boolean",
"description": "Reverse the order of the underlying list.",
"default": false
}
}
},
"description": "A list of the channels where the product is published."
},
"productType": {
"type": "string",
"@mapping": "michaels-store-takeshape:Product.productType",
"description": "The product type specified by the merchant."
},
"publicationCount": {
"type": "integer",
"@mapping": "michaels-store-takeshape:Product.publicationCount",
"@args": {
"type": "object",
"properties": {
"onlyPublished": {
"type": "boolean",
"description": "Include only the resource's publications that are published. If false, then\nreturn all the resource's publications including future publications.",
"default": true
}
}
},
"description": "The number of publications a resource is published on."
},
"publications": {
"@ref": "michaels-store-takeshape:ProductPublicationConnection",
"@mapping": "michaels-store-takeshape:Product.publications",
"@deprecationReason": "Use `resourcePublications` instead",
"@args": {
"type": "object",
"properties": {
"onlyPublished": {
"type": "boolean",
"description": "Return only the publications that are published. If false, then return all publications.",
"default": true
},
"first": {
"type": "integer",
"description": "Returns up to the first `n` elements from the list."
},
"after": {
"type": "string",
"description": "Returns the elements that come after the specified cursor."
},
"last": {
"type": "integer",
"description": "Returns up to the last `n` elements from the list."
},
"before": {
"type": "string",
"description": "Returns the elements that come before the specified cursor."
},
"reverse": {
"type": "boolean",
"description": "Reverse the order of the underlying list.",
"default": false
}
}
},
"description": "A list of the channels where the product is published."
},
"publishedAt": {
"description": "The date and time ([ISO 8601 format](http://en.wikipedia.org/wiki/ISO_8601))\nwhen the product was published to the Online Store.",
"@tag": "DateTime",
"@mapping": "michaels-store-takeshape:Product.publishedAt"
},
"publishedOnChannel": {
"type": "boolean",
"@mapping": "michaels-store-takeshape:Product.publishedOnChannel",
"@deprecationReason": "Use `publishedOnPublication` instead",
"@args": {
"type": "object",
"properties": {
"channelId": {
"type": "string",
"@tag": "id",
"description": "The ID of the channel to check."
}
},
"required": ["channelId"]
},
"description": "Check to see whether the resource is published to a given channel."
},
"publishedOnCurrentChannel": {
"type": "boolean",
"@mapping": "michaels-store-takeshape:Product.publishedOnCurrentChannel",
"@deprecationReason": "Use `publishedOnCurrentPublication` instead",
"description": "Check to see whether the resource is published to the calling app's channel."
},
"publishedOnCurrentPublication": {
"type": "boolean",
"@mapping": "michaels-store-takeshape:Product.publishedOnCurrentPublication",
"description": "Check to see whether the resource is published to the calling app's publication."
},
"publishedOnPublication": {
"type": "boolean",
"@mapping": "michaels-store-takeshape:Product.publishedOnPublication",
"@args": {
"type": "object",
"properties": {
"publicationId": {
"type": "string",
"@tag": "id",
"description": "The ID of the publication to check."
}
},
"required": ["publicationId"]
},
"description": "Check to see whether the resource is published to a given publication."
},
"resourcePublications": {
"@ref": "michaels-store-takeshape:ResourcePublicationConnection",
"@mapping": "michaels-store-takeshape:Product.resourcePublications",
"@args": {
"type": "object",
"properties": {
"onlyPublished": {
"type": "boolean",
"description": "Whether to return only the resources that are currently published. If false,\nthen also returns the resources that are scheduled to be published.",
"default": true
},
"first": {
"type": "integer",
"description": "Returns up to the first `n` elements from the list."
},
"after": {
"type": "string",
"description": "Returns the elements that come after the specified cursor."
},
"last": {
"type": "integer",
"description": "Returns up to the last `n` elements from the list."
},
"before": {
"type": "string",
"description": "Returns the elements that come before the specified cursor."
},
"reverse": {
"type": "boolean",
"description": "Reverse the order of the underlying list.",
"default": false
}
}
},
"description": "The list of resources that are published to a publication."
},
"resourcePublicationsV2": {
"@ref": "michaels-store-takeshape:ResourcePublicationV2Connection",
"@mapping": "michaels-store-takeshape:Product.resourcePublicationsV2",
"@args": {
"type": "object",
"properties": {
"onlyPublished": {
"type": "boolean",
"description": "Whether to return only the resources that are currently published. If false,\nthen also returns the resources that are scheduled or staged to be published.",
"default": true
},
"first": {
"type": "integer",
"description": "Returns up to the first `n` elements from the list."
},
"after": {
"type": "string",
"description": "Returns the elements that come after the specified cursor."
},
"last": {
"type": "integer",
"description": "Returns up to the last `n` elements from the list."
},
"before": {
"type": "string",
"description": "Returns the elements that come before the specified cursor."
},
"reverse": {
"type": "boolean",
"description": "Reverse the order of the underlying list.",
"default": false
}
}
},
"description": "The list of resources that are either published or staged to be published to a publication."
},
"seo": {
"@ref": "michaels-store-takeshape:SEO",
"@mapping": "michaels-store-takeshape:Product.seo",
"description": "SEO information of the product."
},
"status": {
"description": "The product status. This controls visibility across all channels.",
"enum": ["ACTIVE", "ARCHIVED", "DRAFT"],
"@mapping": "michaels-store-takeshape:Product.status"
},
"storefrontId": {
"description": "The storefront ID of the product.",
"@tag": "StorefrontID",
"@mapping": "michaels-store-takeshape:Product.storefrontId"
},
"tags": {
"type": "array",
"items": { "type": "string" },
"@mapping": "michaels-store-takeshape:Product.tags",
"description": "A comma separated list of tags associated with the product. Updating `tags` overwrites\nany existing tags that were previously added to the product. To add new tags without overwriting\nexisting tags, use the [tagsAdd](https://shopify.dev/docs/admin-api/graphql/reference/common-objects/tagsadd)\nmutation."
},
"templateSuffix": {
"type": "string",
"@mapping": "michaels-store-takeshape:Product.templateSuffix",
"description": "The theme template used when viewing the product in a store."
},
"title": {
"type": "string",
"@mapping": "michaels-store-takeshape:Product.title",
"description": "The title of the product."
},
"totalInventory": {
"type": "integer",
"@mapping": "michaels-store-takeshape:Product.totalInventory",
"description": "The quantity of inventory in stock."
},
"totalVariants": {
"type": "integer",
"@mapping": "michaels-store-takeshape:Product.totalVariants",
"description": "The number of variants that are associated with the product."
},
"tracksInventory": {
"type": "boolean",
"@mapping": "michaels-store-takeshape:Product.tracksInventory",
"description": "Whether inventory tracking has been enabled for the product."
},
"translations": {
"type": "array",
"items": {
"@ref": "michaels-store-takeshape:PublishedTranslation"
},
"@mapping": "michaels-store-takeshape:Product.translations",
"@args": {
"type": "object",
"properties": {
"locale": {
"type": "string",
"description": "Filters translations locale."
}
},
"required": ["locale"]
},
"description": "The translations associated with the resource."
},
"unpublishedChannels": {
"@ref": "michaels-store-takeshape:ChannelConnection",
"@mapping": "michaels-store-takeshape:Product.unpublishedChannels",
"@deprecationReason": "Use `unpublishedPublications` instead",
"@args": {
"type": "object",
"properties": {
"first": {
"type": "integer",
"description": "Returns up to the first `n` elements from the list."
},
"after": {
"type": "string",
"description": "Returns the elements that come after the specified cursor."
},
"last": {
"type": "integer",
"description": "Returns up to the last `n` elements from the list."
},
"before": {
"type": "string",
"description": "Returns the elements that come before the specified cursor."
},
"reverse": {
"type": "boolean",
"description": "Reverse the order of the underlying list.",
"default": false
}
}
},
"description": "The list of channels that the resource is not published to."
},
"unpublishedPublications": {
"@ref": "michaels-store-takeshape:PublicationConnection",
"@mapping": "michaels-store-takeshape:Product.unpublishedPublications",
"@args": {
"type": "object",
"properties": {
"first": {
"type": "integer",
"description": "Returns up to the first `n` elements from the list."
},
"after": {
"type": "string",
"description": "Returns the elements that come after the specified cursor."
},
"last": {
"type": "integer",
"description": "Returns up to the last `n` elements from the list."
},
"before": {
"type": "string",
"description": "Returns the elements that come before the specified cursor."
},
"reverse": {
"type": "boolean",
"description": "Reverse the order of the underlying list.",
"default": false
}
}
},
"description": "The list of publications that the resource is not published to."
},
"updatedAt": {
"description": "The date and time when the product was last modified.\nA product's `updatedAt` value can change for different reasons. For example, if an order\nis placed for a product that has inventory tracking set up, then the inventory adjustment\nis counted as an update.",
"@tag": "DateTime",
"@mapping": "michaels-store-takeshape:Product.updatedAt"
},
"variants": {
"@ref": "michaels-store-takeshape:ProductVariantConnection",
"@mapping": "michaels-store-takeshape:Product.variants",
"@args": {
"type": "object",
"properties": {
"first": {
"type": "integer",
"description": "Returns up to the first `n` elements from the list."
},
"after": {
"type": "string",
"description": "Returns the elements that come after the specified cursor."
},
"last": {
"type": "integer",
"description": "Returns up to the last `n` elements from the list."
},
"before": {
"type": "string",
"description": "Returns the elements that come before the specified cursor."
},
"reverse": {
"type": "boolean",
"description": "Reverse the order of the underlying list.",
"default": false
},
"sortKey": {
"description": "Sort the underlying list by the given key.",
"enum": [
"TITLE",
"NAME",
"SKU",
"INVENTORY_QUANTITY",
"INVENTORY_MANAGEMENT",
"INVENTORY_LEVELS_AVAILABLE",
"INVENTORY_POLICY",
"FULL_TITLE",
"POPULAR",
"POSITION",
"ID",
"RELEVANCE"
],
"default": "POSITION"
}
}
},
"description": "A list of variants associated with the product."
},
"vendor": {
"type": "string",
"@mapping": "michaels-store-takeshape:Product.vendor",
"description": "The name of the product's vendor."
}
},
"required": [
"availablePublicationCount",
"collections",
"createdAt",
"defaultCursor",
"description",
"descriptionHtml",
"descriptionPlainSummary",
"handle",
"hasOnlyDefaultVariant",
"hasOutOfStockVariants",
"id",
"images",
"inCollection",
"isGiftCard",
"legacyResourceId",
"media",
"mediaCount",
"metafields",
"options",
"priceRange",
"priceRangeV2",
"privateMetafields",
"productPublications",
"productType",
"publicationCount",
"publications",
"publishedOnChannel",
"publishedOnCurrentChannel",
"publishedOnCurrentPublication",
"publishedOnPublication",
"resourcePublications",
"resourcePublicationsV2",
"seo",
"status",
"storefrontId",
"tags",
"title",
"totalInventory",
"totalVariants",
"tracksInventory",
"translations",
"unpublishedChannels",
"unpublishedPublications",
"updatedAt",
"variants",
"vendor"
]
}
},
"Product": {
"name": "Product",
"id": "r_P4Zu5wr",
"title": "Product",
"workflow": "default",
"model": { "type": "multiple" },
"schema": {
"type": "object",
"properties": {
"name": {
"title": "Name",
"type": "string",
"description": "Initialized with title from shopify",
"@mapping": "takeshape:local:Product.UAPS1KwbxZ"
},
"shopifyProduct": {
"@ref": "michaels-store-takeshape:Product",
"@resolver": {
"name": "graphql:query",
"service": "michaels-store-takeshape",
"options": { "fieldName": "product" },
"argsMapping": {
"id": [["get", { "path": "source.shopifyProductId" }]]
}
},
"@tag": "pattern:service-object:1"
},
"shopifyProductId": {
"type": "string",
"@mapping": "takeshape:local:Product.shopifyProductId",
"title": "shopify product",
"minLength": 1,
"pattern": "(^gid://shopify/Product/\\d+$)"
},
"attributes": {
"items": { "$ref": "#/shapes/ProductAttributes/schema" },
"type": "array",
"@resolver": {
"resultsMapping": [
[
"expressionEval",
{
"expression": "map(compose(compose(zipObject(['description', 'id', 'name', 'namespace', 'type', 'value']), map(get('[1]'))), sortBy('[0]'), toPairs, get('node')), get('metafields.edges', steps[0]))"
}
]
],
"name": "graphql:query",
"service": "michaels-store-takeshape",
"options": {
"fieldName": "product",
"selectionSet": "{ metafields(first: 10) { edges { node { id namespace ownerType value description key } } } }"
},
"argsMapping": {
"id": [["get", { "path": "source.shopifyProductId" }]]
}
}
}
}
}
}
},
"workflows": {},
"forms": {
"Product": {
"default": {
"properties": {
"name": { "widget": "singleLineText" },
"shopifyProduct": {
"properties": {
"descriptionHtml": {
"widget": "serviceObjectProperty",
"provider": "shopify"
},
"handle": {
"widget": "serviceObjectProperty",
"provider": "shopify"
},
"redirectNewHandle": {
"widget": "serviceObjectProperty",
"provider": "shopify"
},
"productType": {
"widget": "serviceObjectProperty",
"provider": "shopify"
},
"tags": {
"widget": "serviceObjectProperty",
"provider": "shopify"
},
"templateSuffix": {
"widget": "serviceObjectProperty",
"provider": "shopify"
},
"giftCard": {
"widget": "serviceObjectProperty",
"provider": "shopify"
},
"giftCardTemplateSuffix": {
"widget": "serviceObjectProperty",
"provider": "shopify"
},
"title": {
"widget": "serviceObjectProperty",
"provider": "shopify"
},
"vendor": {
"widget": "serviceObjectProperty",
"provider": "shopify"
},
"bodyHtml": {
"widget": "serviceObjectProperty",
"provider": "shopify"
},
"images": {
"widget": "shopifyProductImages",
"provider": "shopify"
},
"publishedAt": {
"widget": "serviceObjectProperty",
"provider": "shopify"
},
"variants": {
"widget": "shopifyRelationshipSummary",
"provider": "shopify"
}
},
"widget": "shopify",
"wrapper": "shopifyServiceWrapper",
"order": [
"title",
"handle",
"descriptionHtml",
"bodyHtml",
"tags",
"vendor",
"publishedAt",
"variants",
"images",
"redirectNewHandle",
"productType",
"templateSuffix",
"giftCard",
"giftCardTemplateSuffix"
]
},
"shopifyProductId": {
"instructions": "Format: gid://shopify/Product/1111111111111",
"label": "product ID",
"widget": "serviceObjectId",
"provider": "shopify",
"serviceObjectType": "product",
"service": "michaels-store-takeshape"
}
},
"order": ["name", "shopifyProductId", "shopifyProduct"]
}
}
},
"services": {
"michaels-store-takeshape": {
"title": "SHOPIFY",
"namespace": "Shopify",
"provider": "shopify",
"serviceType": "graphql",
"authenticationType": "oauth2Bearer",
"authentication": "6/gkgSypmOWoOV6gK31wos2tknamMHXmG7nqftu0WeWOzAVTbGOdGignhyVm+w9TocoBehgctDxiyrToGTTI5w7TUVN5yItDJAd4DO+sY9DOy8kMcvr17jKCZvj0m32e+YKVmg24AT3OsOuX/O64wPZ1zv8JAOEMDGnHLEZEOc+BQp9zJlXdOf9PMAxkmjxsVe/S",
"options": {
"shop": "michaels-store-takeshape",
"endpoint": "https://michaels-store-takeshape.myshopify.com/admin/api/2020-07/graphql.json"
},
"id": "michaels-store-takeshape"
},
"rick": {
"id": "rick",
"provider": "generic",
"title": "Rick",
"namespace": "Rick",
"serviceType": "rest",
"authenticationType": "none",
"options": { "endpoint": "https://rickandmortyapi.com/api" }
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment