diff --git a/package.json b/package.json index 9cf889e..0d9f149 100644 --- a/package.json +++ b/package.json @@ -19,13 +19,15 @@ "workspaces": [ "api", "web", - "cli" + "cli", + "schema" ], "scripts": { "build": "yarn tasks --task build", "release": "yarn build && lerna publish from-package", "sync": "yarn tasks --task sync --target", - "tasks": "./scripts/tasks.js" + "tasks": "./scripts/tasks.js", + "gs": "node ./schema/scripts/index.js" }, "dependencies": { "chokidar": "^3.5.3", diff --git a/schema/package.json b/schema/package.json new file mode 100644 index 0000000..bf300da --- /dev/null +++ b/schema/package.json @@ -0,0 +1,23 @@ +{ + "name": "@redwoodjs-stripe/schema", + "version": "0.0.0", + "description": "Yoga GraphQL plugin that builds sdl from Stripe's OpenAPI", + "keywords": [ + "stripe", + "redwoodjs", + "yoga-graphql", + "schema" + ], + "license": "MIT", + "author": "chrisvdm", + "main": "./dist/index.js", + "files": [ + "dist" + ], + "dependencies": { + "@envelop/core": "^4.0.0", + "@envelop/parser-cache": "^6.0.1", + "@envelop/validation-cache": "^6.0.1", + "graphql": "^16.7.1" + } +} diff --git a/schema/scripts/index.js b/schema/scripts/index.js new file mode 100644 index 0000000..ddf1394 --- /dev/null +++ b/schema/scripts/index.js @@ -0,0 +1,263 @@ +const fetch = require('node-fetch'); +const graphql = require("graphql") + +const main = async () => { + try { + // Fetch Stripe openAPI spec + const openAPIBlob = await fetch('https://raw.githubusercontent.com/stripe/openapi/master/openapi/spec3.json'); + const openAPI = JSON.parse(await openAPIBlob.text()); + + // Retrieve schema information + const openAPISchema = openAPI.components.schemas + + // Specify active (used) Stripe Objects + // TODO: Empty array leads to all objects being processed + // const activeTypes = ["checkout.session", "customer"] + + // Constructing types from Query (excluding resolvers for first iteration) + // https://graphql.org/graphql-js/constructing-types/ + + // Reference for development: + // `type Query { + // stripeCustomer(id: $id): StripeCustomer + // stripeCheckoutSession(id: $id): StripeCheckoutSession + // }` + + // 1. Build Query GraphQL Types + // 1st iteration: Build from scratch + // 2nd iteraction: Build from sdl + + // Define the Query type + // const queryType = new graphql.GraphQLObjectType({ + // name: "Query", + // fields: { + // stripeCustomer: { + // type: getGraphQLObjectType('customer', context), + // args: { + // id: { type: graphql.GraphQLID }, + // } + // }, + // }, + // }) + + // 2. Build out types defined in QueryType + // 1st iteration: Build first level properties + // 2nd iteration: Build recursively as required for QueryType + + // 3. Create new schema using Query as the root + // var schema = new graphql.GraphQLSchema({ query: queryType }) + + const toCamelCase = (str) => { + const words = str.split(/[-_.]/g) + if (words.length > 0) { + let newStr = words[0] + for (let i in words) { + if (i > 0) { + newStr += words[i].charAt(0).toUpperCase() + words[i].slice(1); + } + } + return newStr + } // end multi word if + return words[0] + } // end toCamelCase fn + + const capitalize = (str) => { + const words = str.split(/[-_.]/g) + let newStr = '' + for (let i in words) { + newStr += words[i].charAt(0).toUpperCase() + words[i].slice(1); + } + return newStr + } // end toCamelCase fn + + const getGraphQLReferenceType = (properties) => { + const ref = properties['$ref'] + const fieldName = ref.slice(ref.lastIndexOf('/') + 1) + return getGraphQLObjectType(openAPISchema[fieldName]) + } + + const getGraphQLUnionType = (schema) => { + const { name, properties: { anyOf } } = schema + console.log("==========UNION===========") + console.log(schema) + + const typeList = anyOf.map(t => { + return getPropertyGraphQLType({ + properties: {...t} + }) + }) + + console.log(typeList) + // const typy = new GraphQLUnionType({ + // name: name, + // types: [DogType, CatType], + // }) + // console.log(typy) + + console.log('-') + console.log('-') + console.log('-') + console.log('-') + console.log('-') + console.log('-') + + } + + // const getGraphQLExpandableType = (schema) => { + // return getPropertyGraphQLType(schema) + // } + + const getGraphQLListType = (schema) => { + const { name, isExpandable, properties: {items, description}} = schema + const itemSchema = { + name, + description, + isExpandable, + properties: items, + fromList: true + } + + console.log("List Item: ", name) + + itemType = getPropertyGraphQLType(itemSchema) + if (typeof itemType === "undefined") { + return + } + } + + const getGraphQLHashType = () => { + return new graphql.GraphQLScalarType({ + name: "stripeHash" + }) + } + + const getGraphQLEnumType = ({ name, properties }) => { + const enumValueObj = {} + properties.enum.forEach(value => { + const enumName = toCamelCase(value) + enumValueObj[enumName] = {value: value} + }) + + return new graphql.GraphQLEnumType({ + name: capitalize(`stripe_${name}_enum`), + description: properties.description, + values: enumValueObj, + }) + } + + const getGraphQLBasicType = (type) => { + switch (type) { + case 'string': + return graphql.GraphQLString + case 'integer': + return graphql.GraphQLInt + case 'boolean': + return graphql.GraphQLBoolean + case 'id': + return graphql.GraphQLID + } + } + + const getPropertyGraphQLType = (field) => { + const { properties, isExpandable } = field + // determine whether prop is scalar + const isRef = Object.hasOwn(properties, `\$ref`) + const isUnion = Object.hasOwn(properties, 'anyOf') + const isEnum = Object.hasOwn(properties, 'enum') + const isArray = properties.type === 'array' + const isHash = properties.type === 'object' && !Object.hasOwn(properties, 'properties') && !isExpandable + const isObject = !isHash && properties.type === 'object' + + if (!isRef && !isUnion && !isHash && !isObject && !isEnum && !isArray) { + return getGraphQLBasicType(properties.type) + } + + // if (isExpandable) { + // return getGraphQLExpandableType(field) + // } + + if (isObject) { + if (Object.hasOwn(properties, 'additionalProperties') && !Object.hasOwn(properties, 'properties')){ + const payload = { + ...properties, + properties: properties.additionalProperties + } + + return getGraphQLObjectType()} + return getGraphQLObjectType(properties) + } + + if (isRef) { + return getGraphQLReferenceType(properties) + } + + if (isEnum) { + return getGraphQLEnumType(field) + } + if (isArray) { + // return getGraphQLListType(field) + } + + if (isUnion) { + return getGraphQLUnionType(field) + } + + if (isHash) { + return getGraphQLHashType() + } + return undefined + } + + const getGraphQLObjectType = (schemaField) => { + const { properties, required, title, description } = schemaField + const expandable = schemaField['x-expandableFields'] + + // Check whether object exists + const typeName = capitalize(`stripe_${title}`) + if (seen.has(typeName)) { + // if object type exists return + return seen.get(typeName) + } + + let objectFieldsGraphQLType = {} + + // Goes through each property and return corresponding GraphQL Types + Object.keys(properties).forEach(name => { + const props = properties[name] + // const isRequired = required.includes(name) + + const propGraphQLType = getPropertyGraphQLType({ + name: name, + properties: props, + isExpandable: expandable.includes(name), + }) + + objectFieldsGraphQLType[name] = { + type: propGraphQLType + // type: isRequired ? new graphql.GraphQLNonNull(propGraphQLType) : propGraphQLType + } + + }); + + // Construct object type + const newType = new graphql.GraphQLObjectType({ + title: `Stripe${title}`, + name: capitalize(`Stripe_${title}`), + description: description, + fields: {...objectFieldsGraphQLType}, + }) + seen.set(typeName, newType) + return newType + } + + // Lookup for object types + const seen = new Map() + + getGraphQLObjectType(openAPISchema['checkout.session']) + + } catch (err) { + console.log('fetch error', err); + } +} + +main() \ No newline at end of file diff --git a/schema/src/index.js b/schema/src/index.js new file mode 100644 index 0000000..aefd1c7 --- /dev/null +++ b/schema/src/index.js @@ -0,0 +1,2 @@ +import * as GraphQLJS from 'graphql' +import { envelop, useEngine, useSchema } from '@envelop/core' \ No newline at end of file diff --git a/scripts/tasks.js b/scripts/tasks.js index 754eb43..6b9f0dd 100755 --- a/scripts/tasks.js +++ b/scripts/tasks.js @@ -25,6 +25,10 @@ const configs = { cli: { platform: 'node', format: 'cjs', + }, + schema: { + platform: 'node', + format: 'cjs', } } diff --git a/yarn.lock b/yarn.lock index 6ab81e2..0da7153 100644 --- a/yarn.lock +++ b/yarn.lock @@ -410,6 +410,52 @@ __metadata: languageName: node linkType: hard +"@envelop/core@npm:^4.0.0": + version: 4.0.0 + resolution: "@envelop/core@npm:4.0.0" + dependencies: + "@envelop/types": 4.0.0 + tslib: ^2.5.0 + checksum: 575039dfebeadaac3ea61fd700dcc806e26f04f045dc4457052b7cc1ca7c9c37bf9a98a8e4d377d20c69a4e7dc62c11b8884b70f2a218944736ee58c588d498b + languageName: node + linkType: hard + +"@envelop/parser-cache@npm:^6.0.1": + version: 6.0.1 + resolution: "@envelop/parser-cache@npm:6.0.1" + dependencies: + lru-cache: ^10.0.0 + tslib: ^2.5.0 + peerDependencies: + "@envelop/core": ^4.0.0 + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 + checksum: aa5bbc7f40161df045ef61fe62ba1cb187a5cb4c8456c186f81b1d465b008708276de01bde03d109e8c3ed4a9a9fabe3fb0914302e8c36f4556a9e784a012fd6 + languageName: node + linkType: hard + +"@envelop/types@npm:4.0.0": + version: 4.0.0 + resolution: "@envelop/types@npm:4.0.0" + dependencies: + tslib: ^2.5.0 + checksum: cd8bad3ef7d9ee6015e3befbd27a1fb66a560d1397111b0fe22d8a107c7509ab48f183752d514321e35e9c27e9e970e004579867d64105768fce69ccca411ac7 + languageName: node + linkType: hard + +"@envelop/validation-cache@npm:^6.0.1": + version: 6.0.1 + resolution: "@envelop/validation-cache@npm:6.0.1" + dependencies: + hash-it: ^6.0.0 + lru-cache: ^10.0.0 + tslib: ^2.5.0 + peerDependencies: + "@envelop/core": ^4.0.0 + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 + checksum: e9c5cdb2edaa8ccf6a730d862d5eb83e0af2772673882a2deb9907f2ef7318d38630ee7ad546ae3bf87648c782bc5b4c424b6cd0a7eac45c3b18c5e0a41f7c29 + languageName: node + linkType: hard + "@esbuild/android-arm64@npm:0.18.11": version: 0.18.11 resolution: "@esbuild/android-arm64@npm:0.18.11" @@ -1324,6 +1370,17 @@ __metadata: languageName: unknown linkType: soft +"@redwoodjs-stripe/schema@workspace:schema": + version: 0.0.0-use.local + resolution: "@redwoodjs-stripe/schema@workspace:schema" + dependencies: + "@envelop/core": ^4.0.0 + "@envelop/parser-cache": ^6.0.1 + "@envelop/validation-cache": ^6.0.1 + graphql: ^16.7.1 + languageName: unknown + linkType: soft + "@redwoodjs-stripe/web@workspace:web": version: 0.0.0-use.local resolution: "@redwoodjs-stripe/web@workspace:web" @@ -3494,6 +3551,13 @@ __metadata: languageName: node linkType: hard +"graphql@npm:^16.7.1": + version: 16.7.1 + resolution: "graphql@npm:16.7.1" + checksum: c924d8428daf0e96a5ea43e9bc3cd1b6802899907d284478ac8f705c8fd233a0a51eef915f7569fb5de8acb2e85b802ccc6c85c2b157ad805c1e9adba5a299bd + languageName: node + linkType: hard + "handlebars@npm:^4.7.7": version: 4.7.7 resolution: "handlebars@npm:4.7.7" @@ -3563,6 +3627,13 @@ __metadata: languageName: node linkType: hard +"hash-it@npm:^6.0.0": + version: 6.0.0 + resolution: "hash-it@npm:6.0.0" + checksum: 10ca948ff7902a752344c3975c1a5c76ba9e4aa0363032f347a7235cce98473dce60fe4399b05979596aedf649adaf5b3f792a55b9b64ad59ae686e43300c2d1 + languageName: node + linkType: hard + "hosted-git-info@npm:^2.1.4": version: 2.8.9 resolution: "hosted-git-info@npm:2.8.9" @@ -4930,6 +5001,13 @@ __metadata: languageName: node linkType: hard +"lru-cache@npm:^10.0.0, lru-cache@npm:^9.1.1 || ^10.0.0": + version: 10.0.0 + resolution: "lru-cache@npm:10.0.0" + checksum: 18f101675fe283bc09cda0ef1e3cc83781aeb8373b439f086f758d1d91b28730950db785999cd060d3c825a8571c03073e8c14512b6655af2188d623031baf50 + languageName: node + linkType: hard + "lru-cache@npm:^5.1.1": version: 5.1.1 resolution: "lru-cache@npm:5.1.1" @@ -4962,13 +5040,6 @@ __metadata: languageName: node linkType: hard -"lru-cache@npm:^9.1.1 || ^10.0.0": - version: 10.0.0 - resolution: "lru-cache@npm:10.0.0" - checksum: 18f101675fe283bc09cda0ef1e3cc83781aeb8373b439f086f758d1d91b28730950db785999cd060d3c825a8571c03073e8c14512b6655af2188d623031baf50 - languageName: node - linkType: hard - "make-dir@npm:3.1.0, make-dir@npm:^3.0.0": version: 3.1.0 resolution: "make-dir@npm:3.1.0" @@ -7185,6 +7256,13 @@ __metadata: languageName: node linkType: hard +"tslib@npm:^2.5.0": + version: 2.6.0 + resolution: "tslib@npm:2.6.0" + checksum: c01066038f950016a18106ddeca4649b4d76caa76ec5a31e2a26e10586a59fceb4ee45e96719bf6c715648e7c14085a81fee5c62f7e9ebee68e77a5396e5538f + languageName: node + linkType: hard + "tuf-js@npm:^1.1.3": version: 1.1.7 resolution: "tuf-js@npm:1.1.7"