Skip to content

Commit

Permalink
Move gatsby-source-directus to dedicated repo
Browse files Browse the repository at this point in the history
  • Loading branch information
rijkvanzanten committed Jul 9, 2022
0 parents commit a6afc58
Show file tree
Hide file tree
Showing 5 changed files with 470 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.DS_Store
node_modules
.vscode
263 changes: 263 additions & 0 deletions gatsby-node.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
const ms = require('ms');
const chalk = require('chalk');
const { Directus } = require('@directus/sdk');
const { sourceNodes, createSchemaCustomization } = require('gatsby-source-graphql/gatsby-node');
const { createRemoteFileNode } = require('gatsby-source-filesystem');

/**
* Validate plugin options
*/
exports.pluginOptionsSchema = ({ Joi }) => {
return Joi.object().keys({
url: Joi.string().required(),
auth: Joi.object()
.keys({
token: Joi.string(),
email: Joi.string(),
password: Joi.string(),
})
.with('email', 'password')
.with('password', 'email')
.xor('token', 'email'),
type: Joi.object()
.keys({
name: Joi.string(),
field: Joi.string(),
})
.optional(),
dev: Joi.object().keys({
refresh: [Joi.number(), Joi.string()],
}),
graphql: Joi.object(),
});
};

/**
* Gatsby source implementation.
*/
exports.sourceNodes = async (gatsbyOptions, pluginOptions) => {
await plugin.setOptions(pluginOptions);

const optionsSystem = plugin.getOptionsSystem();
const options = plugin.getOptions();

const createNode = gatsbyOptions.actions.createNode;

// Avoid type conflict with gatsby-source-graphql
gatsbyOptions.actions.createNode = (node) => {
if (node.internal.type === 'GraphQLSource') {
if (node.typeName === optionsSystem.typeName) node.internal.type = 'DirectusSystemGraphQLSource';
if (node.typeName === options.typeName) node.internal.type = 'DirectusGraphQLSource';
}

return createNode(node);
};

await sourceNodes(gatsbyOptions, optionsSystem);
await sourceNodes(gatsbyOptions, options);
};

exports.createSchemaCustomization = async (gatsby, pluginOptions) => {
await plugin.setOptions(pluginOptions);

await createSchemaCustomization(gatsby, plugin.getOptionsSystem());
await createSchemaCustomization(gatsby, plugin.getOptions());
};

/**
* Gatsby file implementation.
*/
exports.createResolvers = async ({ actions, cache, createNodeId, createResolvers, store, reporter }, pluginOptions) => {
await plugin.setOptions(pluginOptions);

const { createNode } = actions;

const { headers } = await plugin.getOptions();
const { Authorization } = await headers();

const fileResolver = {
imageFile: {
type: `File`,
async resolve(source) {
if (!source || !source.id) return null;

let filename_download = plugin.fileCache.get(source.id);

if (!filename_download) {
if (source.filename_download) filename_download = source.filename_download;
else ({ filename_download } = await plugin.directus.files.readOne(source.id));

plugin.fileCache.set(source.id, filename_download);
}

const nameParts = filename_download.split('.');
const ext = nameParts.length > 1 ? `.${nameParts.pop()}` : '';
const name = nameParts.join('.');

return createRemoteFileNode({
url: `${plugin.url}assets/${source.id}`,
store,
cache,
createNode,
createNodeId,
httpHeaders: { Authorization },
reporter,
ext,
name,
});
},
},
};

await createResolvers({
DirectusData_directus_files: fileResolver,
DirectusSystemData_directus_files: fileResolver,
});
};

class Plugin {
constructor() {
// eslint-disable-next-line no-undef
this.fileCache = new Map();
this.directus = null;
this.options = null;
this.urlGraphqlSystem = '';
this.urlGraphql = '';
this.url = '';
this.refreshInterval = 0;
this.authPromise = null;
}

async setOptions(options) {
const { url, dev, auth } = options;

if (isEmpty(url)) error('"url" must be defined');

if (this.directus) return this.authPromise;

const hasAuth = !!auth;
const hasToken = !isEmpty(auth?.token);
const hasEmail = !isEmpty(auth?.email);
const hasPassword = !isEmpty(auth?.password);
const hasCredentials = hasEmail && hasPassword;

if (hasAuth) {
if (!hasToken && !hasCredentials) error('"auth.token" or ("auth.email" and "auth.password") must be defined');
} else warning('no "auth" option were defined. Resources will be fetched with public role');

try {
const baseUrl = new URL(url);

baseUrl.pathname = '/';
this.url = baseUrl.toString();

baseUrl.pathname = '/graphql';
this.urlGraphql = baseUrl.toString();

baseUrl.pathname = '/graphql/system';
this.urlGraphqlSystem = baseUrl.toString();
} catch (err) {
error('"url" should be a valid URL');
}

try {
this.directus = new Directus(this.url);

if (hasToken) this.authPromise = await this.directus.auth.static(auth.token);

if (hasCredentials)
this.authPromise = await this.directus.auth.login({ email: auth?.email, password: auth?.password });
} catch (err) {
error(`authentication failed with: ${err.message}\nAre credentials valid?`);
}

this.refreshInterval = typeof dev?.refresh === 'string' ? ms(dev.refresh) / 1000 : dev?.refresh || 15;

if (Number.isNaN(this.refreshInterval))
error('"dev.refresh" should be a number in seconds or a string with ms format, i.e. 5s, 5m, 5h, ...');

this.options = options;

return this.authPromise;
}

getOptions() {
const internalOptions = ['url', 'dev', 'auth', 'type'];
const gatsbyPluginOptions = Object.fromEntries(
Object.entries(this.options).flatMap(([key, value]) => (internalOptions.includes(key) ? [] : [[key, value]]))
);

return {
...this.options.graphql,
...gatsbyPluginOptions,
url: this.urlGraphql,
typeName: this.options?.type?.name || 'DirectusData',
fieldName: this.options?.type?.field || 'directus',
headers: this.headers.bind(this),
};
}

getOptionsSystem() {
const options = this.getOptions();

return {
...options,
url: this.urlGraphqlSystem,
typeName: this.options?.type?.system_name || 'DirectusSystemData',
fieldName: this.options?.type?.system_field || 'directus_system',
};
}

async headers() {
let headers = {};
if (typeof this.options?.headers === 'object') {
Object.assign(headers, this.options.headers || {});
} else if (typeof this.options?.headers === 'function') {
Object.assign(headers, (await this.options.headers()) || {});
}

if (this.directus.auth.token) {
Object.assign(headers, {
Authorization: `Bearer ${this.directus.auth.token}`,
});
}

return headers;
}
}

class Log {
static log(level, message) {
let color = level === 'error' ? 'red' : level === 'warning' ? 'yellow' : 'white';

// eslint-disable-next-line no-console
console.log(chalk.cyan('gatsby-source-directus'), ':', chalk[color](message));
}
static error(message) {
Log.log('error', message);
}
static warning(message) {
Log.log('error', message);
}
}

function isEmpty(value) {
if (value?.constructor === String) return value.length === 0;

return true;
}

function error(message) {
Log.error(message);

const error = new Error(`gatsby-source-directus: ${message}`);
error.stack = undefined;

throw error;
}

function warning(message) {
Log.warning(message);
}

const plugin = new Plugin();
1 change: 1 addition & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
//noop
23 changes: 23 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"name": "@directus/gatsby-source-directus",
"version": "9.14.1",
"description": "Source plugin for pulling data into Gatsby from a Directus API.",
"author": "João Biondo <[email protected]>",
"license": "MIT",
"keywords": [
"gatsby",
"gatsby-plugin",
"directus"
],
"dependencies": {
"@directus/sdk": "9.14.1",
"gatsby-source-filesystem": "4.13.0",
"gatsby-source-graphql": "4.13.0",
"ms": "2.1.3"
},
"repository": "directus/gatsby-source-directus",
"bugs": {
"url": "https://github.com/directus/directus/issues"
},
"gitHead": "24621f3934dc77eb23441331040ed13c676ceffd"
}
Loading

0 comments on commit a6afc58

Please sign in to comment.