Skip to content

Commit

Permalink
Merge branch 'feature/joins'
Browse files Browse the repository at this point in the history
  • Loading branch information
kmcaloon committed May 19, 2020
2 parents eb63223 + a6c31b8 commit d7c8f99
Show file tree
Hide file tree
Showing 4 changed files with 161 additions and 51 deletions.
46 changes: 42 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@ https://drive.google.com/file/d/1FVch2HbAWk1TEXph1katiNb1uuaBLSHf/view?usp=shari
- Replicates Gatsby's beloved patterns
- GROQ-based page queries with HMR
- GROQ-based static queries with live reloads
<<<<<<< HEAD
- Leverages GROQ's native functionality for advanced querying, node/document projections, joins (see [limitations](#limitations)), etc,
=======
- Leverages GROQ's native functionality for advanced querying, node/document projections, joins (see [notes on joins](#joins)), etc,
>>>>>>> feature/joins
- String interpolation ("fragments") within queries, much more flexible than GraphQL fragments
- GROQ explorer in browser during development at `locahost:8000/__groq` **(TO DO)**
- Optimized for incremental builds on Cloud and OSS **(TO DO)**
Expand All @@ -36,8 +40,14 @@ module.exports = {
resolve: 'gatsby-plugin-groq',
options: {
// Location of your project's fragments index file.
// Only required if you are implementing fragments.
fragmentsDir: './src/fragments'
// Only required if you are implementing fragments. Defaults to `./src/fragments`.
fragmentsDir: './src/fragments',
// Determines which field to match when using joins. Defaults to `id`.
referenceMatcher: 'id',
// If only using Sanity documents, change this to true to use default method
// of joining documents without having to append ._ref to the referencing field.
// Defaults to false.
autoRefs: false,
}
}
]
Expand All @@ -63,7 +73,11 @@ export function() {
```
5. For more flexibility and advanced usage check out [Fragments](#fragments)

<<<<<<< HEAD
**NOTE: If using joins or the gatsby-source-sanity plugin, please see [limitations](#limitations)**
=======
**NOTE: If using joins, please see [notes on joins](#joins)**
>>>>>>> feature/joins
## 🤔 What is This? <a name="introduction"></a>
Gatsby is an amazing tool that has helped advance modern web development in significant ways. While many love it for its magical frontend concoction of static generation and rehydration via React, easy routing, smart prefetching, image rendering, etc., one of the key areas where it stands out from other similar tools is its GraphQL data layer. This feature is a large part of why some developers love Gatsby and why others choose to go in another direction. Being able to source data from multiple APIs, files, etc. and compile them altogether into a queryable GraphQL layer is ***amazing***, but many developers simply don't enjoy working with GraphQL. This is where GROQ comes in.
Expand Down Expand Up @@ -144,9 +158,33 @@ To use GROQ fragments with this plugin, for now all fragments must be exported f

## 🤦 Limitations <a name="limitations"></a>
### Joins <a name="joins"></a>
Apart from references to assets such as images within Sanity datasets, the shorthand join syntax using the `->` operator is not supported. This is primarily related to the GROQ js client but may change in the future. Until then use the "Other Joins" syntaxes as documented [here](https://www.sanity.io/docs/groq-joins).
The ability to join multiple documents and retrieve their fields is a popular feature of GROQ. More testing needs to be done, but currently most join syntaxes are supported other than the `references()` function.

Here is an example needing to match a `_ref` found within an array of objects: https://groq.dev/oLIs5ABtnM0xrG9aSSJBYg
For Sanity users, if using the `->` operator you will need to append `._ref` to the field which contains the reference. So with a Sanity dataset, the usual
`referenceField->{ ... }` would instead look like this: `referenceField._ref->{ ... }`. Likewise for arrays: `arrayOfReferences[]._ref->{ ... }`. If you are only using Sanity data within your Gatsby project and would like to use the regular syntax, within the plugin options set `autoRefs: true` and you won't have to worry about appending the extra field.

### Other usage with gatsby-source-sanity
For every `_ref` field within your documents the source plugin injects the referenced document's GraphQL node id instead of its default `_id` value. This means that whenever you are trying to match `_ref` values to documents you need to use the `id` field instead of `_id`.

So instead of what you are used to:
```
{
...,
"document: *[ _id == ^._ref ] {
...
}[0]
}
```
You would use this:
```
{
...,
"document: *[ id == ^._ref ] {
...
}[0]
}
```
If you are using the source plugin and running issues into issues with your joins, if all else fails try double checking your queries and make sure you are matching every `_ref` to the actual node `id`.

### Usage with gatsby-source-sanity
For every `_ref` field within your documents, the source plugin injects the referenced document's GraphQL node id instead of its default `_id` value. This means that whenever you are trying to match `_ref` values to documents you need to use the `id` field instead of `_id`.
Expand Down
19 changes: 16 additions & 3 deletions gatsby-node.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,9 @@ const traverse = require( '@babel/traverse' ).default;
const { watch } = require( 'chokidar' );
const { runQuery } = require( './index' );
const { reporter } = require( './utils' );
const { groqDirectories } = require( './index' );

// TODO
const ROOT = path.resolve( __dirname, '../..' );
const GROQ_DIR = process.env.NODE_ENV === 'development' ? `${ROOT}/.cache/groq` : `${ROOT}/public/static/groq`;
const { GROQ_DIR, ROOT } = groqDirectories;


/**
Expand All @@ -27,6 +26,20 @@ exports.resolvableExtensions = async ( { graphql, actions, cache, getNodes, trac
}
fs.mkdirSync( GROQ_DIR );

// Cache options (because we need them on frontend)
// Probably a more sophisticated Gatsby way of doing this.
if( !! plugin ) {

fs.writeFileSync( `${GROQ_DIR}/options.json`, JSON.stringify( plugin ), err => {
if( err ) {
throw new Error( err );
}
} );

}



// Cache fragments.
const fragmentsDir = !! plugin.fragmentsDir ? path.join( ROOT, plugin.fragmentsDir ) : null;

Expand Down
145 changes: 102 additions & 43 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,17 @@
const groq = require( 'groq-js' );
const murmurhash = require( './murmur' );
const path = require( 'path' );
const { reporter } = require( './utils' );

const ROOT = path.resolve( __dirname, '../..' );
const GROQ_DIR = process.env.NODE_ENV === 'development' ? `${ROOT}/.cache/groq` : `${ROOT}/public/static/groq`;


/**
* Use directory settings throughout plugin.
*/
exports.groqDirectories = { ROOT, GROQ_DIR };


/**
* Hook to mimic Gatsby's static query.
Expand All @@ -16,6 +26,7 @@ exports.useGroqQuery = query => {

const hash = murmurhash( query );


try {
const result = require( `${process.env.GROQ_DIR}/${hash}.json` );
return result;
Expand Down Expand Up @@ -46,50 +57,11 @@ exports.runQuery = async ( rawQuery, dataset, options = {} ) => {
const hasFragment = query.includes( '${' );

if( hasFragment ) {

if( ! fragments || ! Object.keys( fragments ).length ) {
reporter.warn( 'Query contains fragments but no index provided.' );
return;
}

// For now we are just going through all fragments and running
// simple string replacement.
for( let [ name, value ] of Object.entries( fragments ) ) {

if( ! query.includes( name ) ) {
continue;
}

// Process string.
if( typeof value === 'string' ) {
const search = `\\$\\{(${name})\\}`;
const pattern = new RegExp( search, 'g' );
query = query.replace( pattern, value );
}
// Process function.
else if( typeof value === 'function' ) {

// const ast = parser.parse( query, {
// errorRecovery: true,
// plugins: [ 'jsx' ],
// sourceType: 'module',
// } );
//
// traverse( ast, {
// Identifier: function( path ) {
//
// if( path.node.name === name ) {
//
// }
// console.log( '=======', path.node.name );
// }
// } );

}

}
query = processFragments( query, fragments );
}

query = processJoins( query );

try {

const strippedQuery = query.replace( /`/g, '', );
Expand All @@ -103,7 +75,7 @@ exports.runQuery = async ( rawQuery, dataset, options = {} ) => {
catch( err ) {
console.error( file );
reporter.error( `${err}` );
reporter.error( query );
reporter.error( `Query: ${query}` );

return err;

Expand All @@ -112,3 +84,90 @@ exports.runQuery = async ( rawQuery, dataset, options = {} ) => {

}

/**
* Process joins.
*
* @param {string} query
* @return {string}
*/
function processJoins( query ) {

// We need to figure out a clean way to get plugin options...
let processedQuery = query;

if( processedQuery.includes( '->' ) ) {

const optionsDir = process.env.GROQ_DIR || GROQ_DIR;
const { autoRefs, referenceMatcher } = require( `${optionsDir}/options` );
const matchField = referenceMatcher || 'id';
const refOption = !! autoRefs ? '._ref' : '';

const search = `\\S+->\\w*`;
const regex = /\S+->\w*/gm;

for( let match of regex.exec( processedQuery ) ) {

let field = match.replace( '->', '' );
let replace = null;

// Single refs.
if( ! field.includes( '[]' ) ) {
replace = `*[ ${matchField} == ^.${field}${refOption} ][0]`;
}
// Arrays.
else {
replace = `*[ ${matchField} in ^.${field}${refOption} ]`;
}

processedQuery = processedQuery.replace( match, replace );

}

}

return processedQuery;

}

/**
* Process fragments.
*
* @param {string} query
* @param {object} fragments
* @return {string}
*/
function processFragments( query, fragments ) {

let processedQuery = query;

if( ! fragments || ! Object.keys( fragments ).length ) {
reporter.warn( 'Query contains fragments but no index provided.' );
return null;
}

// For now we are just going through all fragments and running
// simple string replacement.
for( let [ name, value ] of Object.entries( fragments ) ) {

if( ! processedQuery.includes( name ) ) {
continue;
}

// Process string.
if( typeof value === 'string' ) {
const search = `\\$\\{(${name})\\}`;
const pattern = new RegExp( search, 'g' );
processedQuery = processedQuery.replace( pattern, value );
}
// Process function.
// else if( typeof value === 'function' ) {
//
// }

}

return processedQuery;


}

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "gatsby-plugin-groq",
"description": "Gatsby plugin for using GROQ in place of GraphQL",
"version": "1.0.0-alpha.4",
"version": "1.0.0-alpha.5",
"author": "Kevin McAloon <[email protected]>",
"keywords": [
"gatsby"
Expand Down

0 comments on commit d7c8f99

Please sign in to comment.