Skip to content

Commit

Permalink
Rectify the typings of the project and add Readme.md
Browse files Browse the repository at this point in the history
  • Loading branch information
Kundu authored and Kundu committed Apr 16, 2021
1 parent c71dec9 commit 0f1c884
Show file tree
Hide file tree
Showing 25 changed files with 1,219 additions and 2,530 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
dist
node_modules
transpiled
transpiled
worker
30 changes: 17 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# ʕ •́؈•̀) `workers-typescript-template`
`fringe-workers`

A batteries included template for kick starting a TypeScript Cloudflare worker project.
A experimental project to execute GraphQL queries/mutations on Cloudflare Edge.

## 🔋 Getting Started

Expand All @@ -9,29 +9,33 @@ This template is meant to be used with [Wrangler](https://github.com/cloudflare/
To generate using Wrangler, run this command:

```bash
wrangler generate my-ts-project https://github.com/EverlastingBugstopper/worker-typescript-template
wrangler generate my-ts-project https://github.com/bishwenduk029/fringe-workers
```

### 👩 💻 Developing
### ✏️ Concept

[`src/index.js`](./src/index.ts) calls the request handler in [`src/handler.ts`](./src/handler.ts), and will return the [request method](https://developer.mozilla.org/en-US/docs/Web/API/Request/method) for the given request.
The purpose of project is to execute any kind of DB/GraphQL queries/mutations on Cloudflare's amazing and secure EDGE network.

### 🧪 Testing
File system based routing - Any .graphql file inside graphql folder, becomes accessible at REST endpoint `/graphql/*`

This template comes with mocha tests which simply test that the request handler can handle each request method. `npm test` will run your tests.
For example query residing at `/graphql/space/index.graphql` is mapped to `/graphql/space`(Method = POST)

### ✏️ Formatting
### 👩 💻 Developing

This template uses [`prettier`](https://prettier.io/) to format the project. To invoke, run `npm run format`.
1.) Inside graphql folder add your queries/mutations in plain .graphql
2.) Provide the URL for your GraphQL API in wrangler.toml
3.) Now open any REST client of your choice (say POSTMAN)
4.) Start accessing your REST endpoints via POST method.

### 👀 Previewing and Publishing

For information on how to preview and publish your worker, please see the [Wrangler docs](https://developers.cloudflare.com/workers/tooling/wrangler/commands/#publish).

## 🤢 Issues
### Roadmap

If you run into issues with this specific project, please feel free to file an issue [here](https://github.com/cloudflare/workers-typescript-template/issues). If the problem is with Wrangler, please file an issue [here](https://github.com/cloudflare/wrangler/issues).
1.) Add support for normalized caching for GraphQL on Cloudflare KV ( in-progress )
2.) Add similar support for .sql files

## ⚠️ Caveats
## 🤢 Feedback

The `service-worker-mock` used by the tests is not a perfect representation of the Cloudflare Workers runtime. It is a general approximation. We recommend that you test end to end with `wrangler dev` in addition to a [staging environment](https://developers.cloudflare.com/workers/tooling/wrangler/configuration/environments/) to test things before deploying.
If you have any feedback or enhancement ideas please feel free to share them [here](https://github.com/bishwenduk029/fringe-workers/issues).
19 changes: 9 additions & 10 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,23 +1,25 @@
{
"name": "pname",
"name": "fringe-workers",
"version": "1.0.0",
"description": "Cloudflare worker TypeScript template",
"description": "Cloudflare worker for running GraphQL queries on Cloudflare EDGE",
"main": "index.js",
"scripts": {
"build": "webpack",
"dev": "NODE_ENV=development npm run build",
"format": "prettier --write '**/*.{ts,js,css,json,md}'",
"test:clean": "rimraf ./transpiled/src ./transpiled/test",
"test": "npm run test:clean && mocha --require source-map-support/register --recursive test",
"transpile": "tsc --project ./test"
"format": "prettier --write '**/*.{ts,js,css,json,md}'"
},
"author": "author",
"license": "MIT OR Apache-2.0",
"devDependencies": {
"@cloudflare/workers-types": "^2.0.0",
"@types/chai": "^4.2.11",
"@types/mocha": "^7.0.2",
"@types/node": "^14.14.41",
"@types/webpack": "^5.28.0",
"@types/webpack-env": "^1.16.0",
"babel-loader": "^8.2.2",
"chai": "^4.2.0",
"graphql-tag": "^2.11.0",
"mocha": "^8.0.1",
"prettier": "^2.0.5",
"rimraf": "^3.0.2",
Expand All @@ -28,9 +30,6 @@
"webpack-cli": "^3.3.12"
},
"dependencies": {
"babel-loader": "^8.2.2",
"graphql": "^15.5.0",
"graphql-request": "^3.4.0",
"graphql-tag": "^2.11.0"
"graphql": "^15.5.0"
}
}
221 changes: 221 additions & 0 deletions src/cache/denormalize.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
import * as GraphQL from 'graphql'
import {
DenormalizationResult,
FieldNodeWithSelectionSet,
Variables,
ResponseObject,
ResponseObject2,
ResponseObjectArray,
RootFields,
ResolveType,
} from './types'
import {
expandFragments,
getDocumentDefinitions,
fieldNameWithArguments,
shouldIncludeField,
defaultResolveType,
} from './functions'
import { NormKey } from './norm-map'
import { Cache } from 'cache'

type Mutable<T> = { -readonly [P in keyof T]: T[P] } // Remove readonly

type MutableResponseObject = Mutable<ResponseObject>
// eslint-disable-next-line functional/prefer-readonly-type
type MutableResponseObjectArray = Array<MutableResponseObject>
type ParentResponseObjectOrArray =
| Mutable<ResponseObject2>
| ResponseObjectArray
type ParentResponseKey = string | number | undefined
type StackWorkItem = readonly [
FieldNodeWithSelectionSet,
NormKey | ReadonlyArray<NormKey>,
ParentResponseObjectOrArray,
ParentResponseKey,
NormKey,
string,
]

/**
* Creates a graphql response by denormalizing.
* @param query The graphql query document
* @param variables The graphql query variables
* @param normMap The map of normalized objects
* @param resolveType Function get get typeName from an object
*/
export async function denormalize(
query: GraphQL.DocumentNode,
variables: Variables | undefined,
normMap: Cache,
resolveType: ResolveType = defaultResolveType,
): Promise<DenormalizationResult> {
const [fragmentMap, rootFieldNode] = getDocumentDefinitions(query.definitions)

const stack: Array<StackWorkItem> = []
const response = {}
const usedFieldsMap: {
// eslint-disable-next-line
[key: string]: Set<string>
} = {}
stack.push([
rootFieldNode,
'ROOT_QUERY',
response,
undefined,
'ROOT_QUERY',
'ROOT_QUERY',
])
while (stack.length > 0) {
const [
fieldNode,
idOrIdArray,
parentObjectOrArray,
parentResponseKey,
parentNormKey,
fieldNameInParent,
] = stack.pop()!

// The stack has work items, depending on the work item we have four different cases to handle:
// field + id + parentObject = denormalize(ID) => [responseObject, workitems] and parentObject[field] = responseObject
// field + id + parentArray = denormalize(ID) => [responseObject, workitems] and parentArray.push(responseObject)
// field + idArray + parentObject = stack.push(workItemsFrom(idArray)) and parentObject[field] = new Array()
// field + idArray + parentArray = stack.push(workItemsFrom(idArray)) and parentArray.push(new Array())

let responseObjectOrNewParentArray:
| MutableResponseObject
| MutableResponseObjectArray
| null

if (idOrIdArray === null) {
responseObjectOrNewParentArray = null
} else if (!Array.isArray(idOrIdArray)) {
const key: NormKey = idOrIdArray as NormKey

const normObj = await normMap.get(key)

// Does not exist in normalized map. We can't fully resolve query
if (!normObj) {
return {
data: undefined,
fields: { [parentNormKey]: new Set([fieldNameInParent]) },
}
}

let usedFields = usedFieldsMap[key]
if (usedFields === undefined) {
usedFields = new Set()
usedFieldsMap[key] = usedFields
}

// If we've been here before we need to use the previously created response object
if (Array.isArray(parentObjectOrArray)) {
responseObjectOrNewParentArray =
(parentObjectOrArray as MutableResponseObjectArray)[
parentResponseKey as number
] || Object.create(null)
} else {
responseObjectOrNewParentArray =
(parentObjectOrArray as MutableResponseObject)[
parentResponseKey as string
] || Object.create(null)
}

// Expand fragments and loop all fields
const expandedSelections = expandFragments(
resolveType,
normObj,
fieldNode.selectionSet.selections,
fragmentMap,
)
for (const field of expandedSelections) {
// Check if this field should be skipped according to @skip and @include directives
const include = field.directives
? shouldIncludeField(field.directives, variables)
: true
if (include) {
// Build key according to any arguments
const fieldName =
field.arguments && field.arguments.length > 0
? fieldNameWithArguments(field, variables)
: field.name.value
// Add this to used fields
usedFields.add(fieldName)
const normObjValue = normObj[fieldName]
if (normObjValue !== null && field.selectionSet) {
// Put a work-item on the stack to build this field and set it on the response object
stack.push([
field as FieldNodeWithSelectionSet,
normObjValue as any,
responseObjectOrNewParentArray as
| MutableResponseObject
| MutableResponseObjectArray,
(field.alias && field.alias.value) || field.name.value,
key,
fieldName,
])
} else {
// This field is a primitive (not a array or object)
if (normObjValue !== undefined) {
;(responseObjectOrNewParentArray as MutableResponseObject)[
(field.alias && field.alias.value) || field.name.value
] = normObjValue
} else {
return {
data: undefined,
fields: { [key]: new Set([fieldName]) },
}
}
}
}
}
} else {
const idArray: ReadonlyArray<NormKey> = idOrIdArray
responseObjectOrNewParentArray =
(parentObjectOrArray as MutableResponseObject)[
parentResponseKey as string
] || []
for (let i = 0; i < idArray.length; i++) {
const idArrayItem = idArray[i]
stack.push([
fieldNode,
idArrayItem,
responseObjectOrNewParentArray as
| MutableResponseObject
| MutableResponseObjectArray,
i,
parentNormKey,
fieldNameInParent,
])
}
}

// Add to the parent, either field or an array
if (Array.isArray(parentObjectOrArray)) {
const parentArray: MutableResponseObjectArray = parentObjectOrArray
parentArray[
parentResponseKey as number
] = responseObjectOrNewParentArray as
| MutableResponseObject
| MutableResponseObjectArray
} else {
const parentObject: MutableResponseObject = parentObjectOrArray
parentObject[
parentResponseKey ||
(fieldNode.alias && fieldNode.alias.value) ||
fieldNode.name.value
] = responseObjectOrNewParentArray
}
}

interface GraphQLResponse {
readonly data: RootFields
}

const data = (response as GraphQLResponse).data

return {
data: data,
fields: usedFieldsMap,
}
}
Loading

0 comments on commit 0f1c884

Please sign in to comment.