├── .gitignore ├── README.md ├── package.json ├── src ├── loaders.js ├── resolvers.js ├── schema.gql └── server.js └── test ├── babel-index.js ├── index.js └── sample.txt /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GraphQL and the file system 2 | 3 | Just having some fun using GraphQL for stuff. 4 | 5 | If we queried the filesystem using GraphQL, what would it look like? 6 | 7 | [ Work in pogress...] 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphql-fs", 3 | "version": "0.0.1", 4 | "description": "GraphQL for the filesystem", 5 | "main": "server.js", 6 | "scripts": { 7 | "test": "mocha test/babel-index.js" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/helfer/graphql-fs.git" 12 | }, 13 | "keywords": [ 14 | "GraphQL", 15 | "JavaScript", 16 | "FS" 17 | ], 18 | "author": "Jonas Helfer ", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/helfer/graphql-fs/issues" 22 | }, 23 | "homepage": "https://github.com/helfer/graphql-fs#readme", 24 | "dependencies": { 25 | "apollo-server": "github:apollostack/apollo-server", 26 | "fs-promise": "^0.5.0", 27 | "graphql": "github:apollostack/graphql-js#npm-dist" 28 | }, 29 | "devDependencies": { 30 | "babel": "^6.5.2", 31 | "babel-core": "^6.7.4", 32 | "babel-loader": "6.2.0", 33 | "babel-polyfill": "^6.7.4", 34 | "babel-preset-es2015": "^6.6.0", 35 | "babel-preset-stage-0": "^6.5.0", 36 | "babel-register": "^6.7.2", 37 | "chai": "^3.5.0", 38 | "fs": "0.0.2", 39 | "mocha": "^2.4.5" 40 | }, 41 | "babel": { 42 | "presets": [ 43 | "es2015", 44 | "stage-0" 45 | ] 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/loaders.js: -------------------------------------------------------------------------------- 1 | import fsp from 'fs-promise'; 2 | import path from 'path'; 3 | 4 | class FileLoader { 5 | 6 | constructor(baseDir){ 7 | // TODO: use baseDir 8 | this.baseDir = '/Users/helfer/workspace/helfer/graphql-fs'; 9 | } 10 | 11 | makeFullPath(partialPath){ 12 | return path.join( this.baseDir, partialPath); 13 | } 14 | 15 | openFile(filePath){ 16 | const fullPath = this.makeFullPath(filePath); 17 | return fsp.open(fullPath, 'r+') 18 | .then( (fd) => new File(fd, path.basename(fullPath), fullPath) ) 19 | .catch( (e) => { 20 | console.error('cannot open file', fullPath); 21 | throw e; 22 | return null; 23 | }); 24 | } 25 | 26 | openDir(dirPath){ 27 | const fullPath = this.makeFullPath(dirPath); 28 | return fsp.stat(fullPath) 29 | .then( (stats) => { 30 | if (!stats.isDirectory()){ 31 | console.error(`${fullPath} is not a directory`); 32 | return null; 33 | } 34 | return new Directory(fullPath); 35 | }) 36 | .catch( (err) => { 37 | console.error('could not find directory', fullPath); 38 | return null; 39 | }); 40 | 41 | } 42 | 43 | } 44 | 45 | class Directory { 46 | constructor( dirPath ){ 47 | this.path = dirPath; 48 | 49 | // figure out the name 50 | const pathFragments = dirPath.split('/').filter( x => x ); //nix only 51 | this.name = pathFragments[ pathFragments.length -1 ]; 52 | } 53 | 54 | 55 | // TODO: Memoize stuff. If you list the contents once, 56 | // they should be remembered. Any file should be opened 57 | // only once, any directory only listed once. 58 | 59 | // TODO: Simplify things by always requiring an absolute path. 60 | // also, don't allow .. anywhere. 61 | 62 | contents({includeFiles = true, includeDirs = true}) { 63 | // TODO: this assumes that everything is a file. Bad. 64 | // TODO: assumes a certain directory structure. not good. 65 | const fl = new FileLoader(); //ugh, make it a singleton 66 | return fsp.readdir(this.path) 67 | .then( (res) => { 68 | const promises = []; 69 | res.forEach( (fileName) => { 70 | const fullPath = `./${this.name}/${fileName}`; 71 | promises.push( 72 | fsp.stat(fullPath) 73 | .then( (res) => { 74 | if ( includeFiles && res.isFile()) { 75 | return fl.openFile(fullPath); 76 | } 77 | if ( includeDirs && res.isDirectory()) { 78 | return fl.openDir(fullPath); 79 | } 80 | return null; 81 | }) 82 | ); 83 | }); 84 | // TODO: I'd rather not use Promise.all, because 85 | // one failure shouldn't make everything else fail 86 | return Promise.all(promises) 87 | .then( (values) => { 88 | return values.filter( v => v !== null ); 89 | }); 90 | }) 91 | .catch( (err) => { 92 | console.error(err); 93 | console.error('cannot list dir contents'); 94 | }); 95 | } 96 | 97 | files(){ 98 | return this.contents({includeDirs: false}); 99 | } 100 | 101 | subdirectories(){ 102 | return this.contents({includeFiles: false}); 103 | } 104 | 105 | } 106 | 107 | 108 | class File { 109 | constructor(fd, name, fullPath){ 110 | this.fd = fd; 111 | this.name = name; 112 | this.path = fullPath; 113 | } 114 | 115 | read(){ 116 | // memoize this 117 | return fsp.readFile(this.fd); 118 | } 119 | 120 | stats(){ 121 | // memoize this 122 | return fsp.fstat(this.fd) 123 | //.then( (res) => { console.log(res); return res}) 124 | .catch( (err) => { 125 | console.error('error getting file stats'); 126 | return null; 127 | }); 128 | } 129 | } 130 | 131 | 132 | export { FileLoader, File } 133 | -------------------------------------------------------------------------------- /src/resolvers.js: -------------------------------------------------------------------------------- 1 | import { FileLoader } from './loaders.js'; 2 | export default { 3 | Directory: { 4 | files(dir){ 5 | return dir.files(); 6 | }, 7 | //TODO: this resolver threw an error, but I had no idea where 8 | // it came from. That's not good. 9 | subdirectories(dir){ 10 | return dir.subdirectories(); 11 | }, 12 | }, 13 | File: { 14 | content(file){ 15 | // return fs readfile 16 | return file.read(); 17 | }, 18 | 19 | size(file){ 20 | return file.stats().then( stats => stats.size ); 21 | }, 22 | }, 23 | Query: { 24 | file( obj, { path }){ 25 | const fl = new FileLoader(); 26 | return fl.openFile(path); 27 | // get file info and pass it to file 28 | }, 29 | dir(obj, { path }){ 30 | // get the directory info at that path and pass it to dir 31 | const fl = new FileLoader(); 32 | return fl.openDir(path); 33 | } 34 | }, 35 | /* Mutation: { 36 | createFile( obj, { dirPath, name, content }){ 37 | //fs create file, return file 38 | }, 39 | updateFile( obj, { filePath, newContent }){ 40 | //fs read file, return file 41 | }, 42 | deleteFile( obj, { filePath }){ 43 | //fs delete file, return success 44 | }, 45 | 46 | mkdir( obj, { firPath, name }){ 47 | // make directory and return it 48 | }, 49 | rmdir( obj, { dirPath }){ 50 | // remove directory and return it 51 | }, 52 | }*/ 53 | } 54 | -------------------------------------------------------------------------------- /src/schema.gql: -------------------------------------------------------------------------------- 1 | 2 | union FSNode = Directory | File 3 | 4 | type Directory { 5 | name: String 6 | path: String 7 | # parent: Directory 8 | # content: [File] 9 | subdirectories(regEx: String): [Directory] 10 | files(regEx: String): [File] 11 | # file(name: String!): File 12 | # subdirectory(name: String!): Directory 13 | } 14 | 15 | type File { 16 | name: String 17 | path: String 18 | # directory: Directory 19 | # createdAt: String 20 | # owner: String 21 | # group: String 22 | # permissions: PermissionSet 23 | content: String 24 | size: Int 25 | } 26 | 27 | #type PermissionSet { 28 | # user: Permission 29 | # group: Permission 30 | # other: Permission 31 | #} 32 | 33 | type Permission { 34 | read: Boolean 35 | write: Boolean 36 | execute: Boolean 37 | } 38 | 39 | type Query { 40 | file(path: String): File 41 | dir(path: String): Directory 42 | } 43 | 44 | #type Mutation { 45 | # createFile(dirPath: String!, name: String!, content: String!): File 46 | # updateFile(filePath: String!, newContent: String!): File 47 | # deleteFile(filePath: String!): Boolean 48 | 49 | # mkdir(dirPath: String!, name: String!): Directory 50 | # rmdir(dirPath: String!): Boolean 51 | #} 52 | 53 | schema { 54 | query: Query 55 | # mutation: Mutation 56 | } 57 | 58 | -------------------------------------------------------------------------------- /src/server.js: -------------------------------------------------------------------------------- 1 | 2 | // The server entry code 3 | -------------------------------------------------------------------------------- /test/babel-index.js: -------------------------------------------------------------------------------- 1 | // This file cannot be written with ECMAScript 2015 because it has to load 2 | // the Babel require hook to enable ECMAScript 2015 features! 3 | require('babel-core/register'); 4 | require('babel-polyfill'); 5 | 6 | // The tests, however, can and should be written with ECMAScript 2015. 7 | require('./index.js'); 8 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | // Include the tests here... 2 | import { generateSchema } from '../../../apollo/apollo-server/src'; 3 | import resolveFunctions from '../src/resolvers.js'; 4 | import { assert } from 'chai'; 5 | import { graphql } from '../../../apollo/apollo-server/node_modules/graphql'; 6 | 7 | import { readFileSync } from 'fs'; 8 | const typeDefinition = readFileSync('./src/schema.gql'); 9 | 10 | 11 | describe('Reading files', () => { 12 | it('Can read a simple file', (done) => { 13 | const schema = generateSchema(typeDefinition, resolveFunctions); 14 | const query = '{ file(path: "./test/sample.txt"){ content } }'; 15 | const expected = { data: { file: { content: "Hello world!\n" } } }; 16 | graphql(schema, query).then( (res) => { 17 | assert.deepEqual(res, expected); 18 | done(); 19 | }); 20 | }); 21 | 22 | it('Can get file size', (done) => { 23 | const schema = generateSchema(typeDefinition, resolveFunctions); 24 | const query = '{ file(path: "./test/sample.txt"){ size } }'; 25 | const expected = { data: { file: { size: 13 } } }; 26 | graphql(schema, query).then( (res) => { 27 | assert.deepEqual(res, expected); 28 | done(); 29 | }); 30 | }); 31 | 32 | it('Can get name and path', (done) => { 33 | const schema = generateSchema(typeDefinition, resolveFunctions); 34 | const query = '{ file(path: "./test/sample.txt"){ name, path } }'; 35 | const path = '/Users/helfer/workspace/helfer/graphql-fs/test/sample.txt'; 36 | const expected = { data: { file: { name: 'sample.txt', path: path } } }; 37 | graphql(schema, query).then( (res) => { 38 | assert.deepEqual(res, expected); 39 | done(); 40 | }); 41 | }); 42 | }); 43 | 44 | describe('Reading directories', () => { 45 | it('Can find a directory and get the path and name', (done) => { 46 | const schema = generateSchema(typeDefinition, resolveFunctions); 47 | const query = '{ dir(path: "./test/"){ path name} }'; 48 | const path = '/Users/helfer/workspace/helfer/graphql-fs/test/'; 49 | const expected = { data: { dir: { path: path, name: 'test' } } }; 50 | graphql(schema, query).then( (res) => { 51 | assert.deepEqual(res, expected); 52 | done(); 53 | }); 54 | }); 55 | 56 | it('Can find a directory and list its contents', (done) => { 57 | const schema = generateSchema(typeDefinition, resolveFunctions); 58 | const query = '{ dir(path: "./test/"){ files { name }, subdirectories { name } } }'; 59 | const path = '/Users/helfer/workspace/helfer/graphql-fs/test/'; 60 | const expected = { data: 61 | { dir: 62 | { 63 | files: [ 64 | { name: 'babel-index.js' }, 65 | { name: 'index.js' }, 66 | { name: 'sample.txt' }, 67 | ], 68 | subdirectories: [ 69 | { name: 'uga' }, 70 | ] 71 | }, 72 | } 73 | }; 74 | graphql(schema, query).then( (res) => { 75 | assert.deepEqual(res, expected); 76 | done(); 77 | }); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /test/sample.txt: -------------------------------------------------------------------------------- 1 | Hello world! 2 | --------------------------------------------------------------------------------