From 24d33f06863f22c62056bd2e4290a86237127cdb Mon Sep 17 00:00:00 2001 From: Spartak Date: Mon, 25 May 2020 11:12:00 +0300 Subject: [PATCH] Nested fragment fields with arguments are here + refactoring + readme --- README.md | 57 +++++- package.json | 5 +- src/orm/fragment.js | 73 ++++++-- src/orm/hasura.js | 58 ++++-- src/orm/table.js | 381 +++++++++++++++----------------------- src/utils/builders.js | 170 +++++++++++++---- tests/aggregate.js | 8 +- tests/fragment.js | 97 +++++++--- tests/nested-arguments.js | 97 ++++++++++ 9 files changed, 616 insertions(+), 330 deletions(-) create mode 100644 tests/nested-arguments.js diff --git a/README.md b/README.md index 9a3c088..0328f3c 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![Node.js Package](https://github.com/mrspartak/hasura-om/workflows/Node.js%20Package/badge.svg)](https://nodei.co/npm/hasura-om/) [![Coverage Status](https://coveralls.io/repos/github/mrspartak/hasura-om/badge.svg?branch=master)](https://coveralls.io/github/mrspartak/hasura-om?branch=master) [![XO code style](https://img.shields.io/badge/code_style-XO-5ed9c7.svg)](https://github.com/xojs/xo) - +[![Codacy Badge](https://app.codacy.com/project/badge/Grade/5489a27a08094fd6b34f54ce32b0dcef)](https://www.codacy.com/manual/assorium/hasura-om?utm_source=github.com&utm_medium=referral&utm_content=mrspartak/hasura-om&utm_campaign=Badge_Grade) This library provides an object way to interact with Hasura from backend. Main focus is on fragments, queries are autogenerated. Base fragments (base - all table fields/pk - primary keys) are autogenerated, then you can extend them or create new. @@ -26,12 +26,13 @@ If you know a better way to solve this problem, you are welcome to issues or ema - [x] Fragment extending (v0.0.7) - [x] Subscriptions (v0.0.10) - [x] Aggregate queries (v0.0.12) -- [ ] Nested queries +- [x] Nested queries - [ ] Hasura class extend EventEmitter - [ ] Refactor query builder code - [ ] Docs +- [ ] Support directives -# Simple example +## Simple example ```javascript const { Hasura } = require('hasura-om') @@ -184,6 +185,7 @@ let unsub = om.subscribe({ }) ``` +## Fragments The only control you have is fragments. So this library provides base fragments with all table fields without relations. Of course you need them, so you have many ways to do so. ```javascript @@ -260,12 +262,57 @@ let baseUserFragment = orm.table('user').fragment('base') let basePostFragment = orm.table('post').fragment('base') orm.table('user').createFragment('some_unique_name', [ - baseUserFragment.gqlFields(), + baseUserFragment, { key: 'posts', values: [ - basePostFragment.gqlFields(), + basePostFragment, ] } ]) +``` + +## Nested fields arguments +This is really hard to implement and I hope current implementation works fine. I will investigate it later, when I will have troubles or get any issues. +```javascript + +const fragment2 = new Fragment({ + table: 'test', + name: 'with_nested_args', + fields: [ + 'id', + [ + 'logo', + ['url'], + { + _table: 'images', + limit: 'logo_limit', + offset: 'logo_offset', + where: 'logo_where', + order_by: 'logo_order_by', + distinct_on: 'logo_distinct_on', + }, + ], + ], +}); + +/* +This will generate such fragment + +Fragment with_nested_args_fragment_test on test { + id + logo (limit: $logo_limit, ...) { + url + } +} +*/ + +await orm.query({ + test: { + fragment: 'with_nested_args', + variables: { + 'logo_limit': 1 + } + } +}) ``` \ No newline at end of file diff --git a/package.json b/package.json index 6a95daf..3a9c86f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hasura-om", - "version": "0.0.6", + "version": "0.0.15", "description": "Hasura Fragment focused ORM library", "author": "Spartak (https://spartak.io/)", "keywords": [ @@ -40,7 +40,8 @@ "no-return-assign": 0, "camelcase": 0, "block-scoped-var": 0, - "no-sequences": 0 + "no-sequences": 0, + "no-negated-condition": 0 } }, "ava": { diff --git a/src/orm/fragment.js b/src/orm/fragment.js index c70e528..c038ce4 100644 --- a/src/orm/fragment.js +++ b/src/orm/fragment.js @@ -1,7 +1,9 @@ -const {fieldsToGql} = require('../utils/builders'); +const {fieldsToGql, template} = require('../utils/builders'); class Fragment { constructor(parameters) { + this._isFragment = true; + const defaultParameters = { name: 'base', table: null, @@ -14,28 +16,71 @@ class Fragment { throw new Error('table is required'); } - if (Object.keys(this.params.fields).length === 0) { + if (Object.keys(this.rawFields()).length === 0 || this.rawFields() === '') { throw new Error('fields are required'); } - this._gqlFields = fieldsToGql(this.params.fields); + // Builded fields part of fragment + const {fields, fragmentOperationArgument} = fieldsToGql(this.rawFields()); + this._fields = fields; + this._arguments = fragmentOperationArgument; + + // Templates to create parts local parts of fragment + this._fragmentTemplate = template` + fragment ${'fragmentName'} on ${'table'} { + ${'fields'} + } + `; + this._nameTemplate = template`${'name'}_fragment_${'table'}`; } - gqlFields() { - return this._gqlFields; + /* + Fields passed to Fragment constructor + */ + rawFields() { + return this.params.fields; } - build() { - const fragmentName = `${this.params.name}_fragment_${this.params.table}`; - const fields = this.gqlFields(); + /* + Generated fields to string + */ + fields() { + return this._fields; + } + + /* + Generated arguments if fragment contains them + */ + arguments() { + return this._arguments; + } + /* + Generated Fragment name + */ + name() { + return this._nameTemplate({ + name: this.params.name, + table: this.params.table, + }); + } + + /* + Generated Fragment + */ + fragment() { + return this._fragmentTemplate({ + fragmentName: this.name(), + table: this.params.table, + fields: this.fields(), + }); + } + + build() { return { - name: fragmentName, - raw: ` - fragment ${fragmentName} on ${this.params.table} { - ${fields} - } - `, + name: this.name(), + raw: this.fragment(), + arguments: this.arguments(), }; } } diff --git a/src/orm/hasura.js b/src/orm/hasura.js index c5f4ebf..a485991 100644 --- a/src/orm/hasura.js +++ b/src/orm/hasura.js @@ -158,6 +158,8 @@ class Hasura { return [error]; } + // Console.log(query); + settings = __.mergeDeep({}, this.params.query, settings); const [err, response] = await this.$gql.run({ @@ -180,8 +182,8 @@ class Hasura { buildQuery(parameters, queryType = 'query') { const tables = Object.keys(parameters); - const queryName = []; - const queryVariables = []; + const operationName = []; + const operationArguments = []; const queryFields = []; const queryFragments = {}; const queryFlatSetting = []; @@ -194,9 +196,12 @@ class Hasura { queryType, }); + variables = Object.assign({}, parameters[tableName].variables); + builts.forEach((built) => { - queryName.push(built.query.name); - queryVariables.push(...built.query.variables); + operationName.push(built.query.name); + operationArguments.push(...built.query.arguments); + queryFields.push(built.query.fields); if (built.query.fragment) { queryFragments[built.query.fragment.fragmentName] = built.query.fragment.fragment; @@ -205,17 +210,37 @@ class Hasura { queryFlatSetting.push(built.query.flatSetting); variables = Object.assign({}, variables, built.variables); }); - - /* QueryName.push(built.query.name); - queryVariables.push(...built.query.variables); - queryFields.push(built.query.fields); - queryFragments[built.query.fragment.fragmentName] = built.query.fragment.fragment; - variables = Object.assign({}, variables, built.variables); */ }); + /* + //!queryFragments[] + fragment pk_fragment_user on user { + id + } + + //!queryType - query | subscription + //!operationName - [S_s_user, S_a_user] + //!operationArguments - [$user_where: user_bool_exp] + query S_s_user_S_a_user($user_where: user_bool_exp) { + + //!queryFields - [user , user_aggregate] + user(where: $user_where) { + ...pk_fragment_user + } + user_aggregate { + aggregate { + count + sum { + id + money + } + } + } + } + */ const query = ` ${Object.values(queryFragments).join('\n')} - ${queryType} ${queryName.join('_')} ${queryVariables.length > 0 ? '(' + queryVariables.join(', ') + ')' : ''} { + ${queryType} ${operationName.join('_')} ${operationArguments.length > 0 ? '(' + operationArguments.join(', ') + ')' : ''} { ${queryFields.join('\n')} } `; @@ -278,8 +303,8 @@ class Hasura { buildMutation(parameters) { const tables = Object.keys(parameters); - const queryName = []; - const queryVariables = []; + const operationName = []; + const operationArguments = []; const queryFields = []; const queryFragments = {}; const queryFlatSetting = []; @@ -292,8 +317,9 @@ class Hasura { }); builts.forEach((built) => { - queryName.push(built.query.name); - queryVariables.push(...built.query.variables); + operationName.push(built.query.name); + operationArguments.push(...built.query.arguments); + queryFields.push(built.query.fields); queryFragments[built.query.fragment.fragmentName] = built.query.fragment.fragment; queryFlatSetting.push(built.query.flatSetting); @@ -303,7 +329,7 @@ class Hasura { const query = ` ${Object.values(queryFragments).join('\n')} - mutation ${queryName.join('_')} (${queryVariables.join(', ')}) { + mutation ${operationName.join('_')} (${operationArguments.join(', ')}) { ${queryFields.join('\n')} } `; diff --git a/src/orm/table.js b/src/orm/table.js index 16fe175..9518558 100644 --- a/src/orm/table.js +++ b/src/orm/table.js @@ -1,6 +1,6 @@ const Fragment = require('./fragment'); const Field = require('./field'); -const {fieldsToGql} = require('../utils/builders'); +const {fieldsToGql, template} = require('../utils/builders'); class Table { constructor(parameters) { @@ -17,6 +17,20 @@ class Table { this.fields = {}; this.fragments = {}; + + this.queryNameTemplate = template`${'literal'}_${'type'}_${this.params.name}`; + this.queryTemplate = template` + ${this.params.name}${'table_postfix'} ${'arguments'} { + ${'fields'} + } + `; + this.mutationTeamplate = template` + ${'table_prefix'}${this.params.name} ${'arguments'} { + returning { + ${'fields'} + } + } + `; } init() { @@ -116,69 +130,30 @@ class Table { } build_select(parameters) { - const {fields, fragment, fragmentName} = this.getFieldsFromParams(parameters); - - const variables = {}; - const query_variables = []; - const query_field_variables = []; - - const predifinedVariables = { - where: { - type(name) { - return `${name}_bool_exp`; - }, - }, - limit: { - type() { - return `Int`; - }, - }, - offset: { - type() { - return `Int`; - }, - }, - order_by: { - type(name) { - return `[${name}_order_by!]`; - }, - }, - distinct_on: { - type(name) { - return `[${name}_select_column!]`; - }, - }, - }; - - Object.keys(predifinedVariables).forEach((varName) => { - const varOptions = predifinedVariables[varName]; - - const varKey = `${this.params.name}_${varName}`; - - if (parameters[varName]) { - variables[varKey] = parameters[varName]; - query_field_variables.push(`${varName}: $${varKey}`); - query_variables.push(`$${varKey}: ${varOptions.type(this.params.name)}`); - } - }); + const {fields, fragment, fragmentName, fragmentOperationArguments} = this.getFieldsFromParams(parameters); // Building for query or subscription const queryLiteral = parameters.queryType === 'query' ? 'Q' : 'S'; - const flatKey = `${this.params.name}.select`; - const flatSetting = {}; - flatSetting[flatKey] = `${this.params.name}`; + var {variables, query_arguments, operation_arguments} = this.buildArguments(['where', 'limit', 'offset', 'order_by', 'distinct_on'], parameters, 's'); + + const flatSetting = { + [`${this.params.name}.select`]: `${this.params.name}`, + }; return { query: { - name: `${queryLiteral}_${this.params.name}`, - variables: query_variables, + name: this.queryNameTemplate({ + literal: queryLiteral, + type: 's', + }), + arguments: [...operation_arguments, ...fragmentOperationArguments], flatSetting, - fields: ` - ${this.params.name} ${query_field_variables.length > 0 ? '(' + query_field_variables.join(', ') + ')' : ''} { - ${fields} - } - `, + fields: this.queryTemplate({ + table_postfix: '', + arguments: query_arguments.length > 0 ? '(' + query_arguments.join(', ') + ')' : '', + fields, + }), fragment: { fragment, fragmentName, @@ -189,69 +164,30 @@ class Table { } build_aggregate(parameters) { - const {fields} = this.getFieldsFromAggregate(parameters); - - const variables = {}; - const query_variables = []; - const query_field_variables = []; - - const predifinedVariables = { - where: { - type(name) { - return `${name}_bool_exp`; - }, - }, - limit: { - type() { - return `Int`; - }, - }, - offset: { - type() { - return `Int`; - }, - }, - order_by: { - type(name) { - return `[${name}_order_by!]`; - }, - }, - distinct_on: { - type(name) { - return `[${name}_select_column!]`; - }, - }, - }; - - Object.keys(predifinedVariables).forEach((varName) => { - const varOptions = predifinedVariables[varName]; - - const varKey = `a_${this.params.name}_${varName}`; - - if (parameters[varName]) { - variables[varKey] = parameters[varName]; - query_field_variables.push(`${varName}: $${varKey}`); - query_variables.push(`$${varKey}: ${varOptions.type(this.params.name)}`); - } - }); + const {fields, fragmentOperationArguments} = this.getFieldsFromAggregate(parameters); // Building for query or subscription const queryLiteral = parameters.queryType === 'query' ? 'Q' : 'S'; - const flatKey = `${this.params.name}.aggregate`; - const flatSetting = {}; - flatSetting[flatKey] = `${this.params.name}_aggregate.aggregate`; + var {variables, query_arguments, operation_arguments} = this.buildArguments(['where', 'limit', 'offset', 'order_by', 'distinct_on'], parameters, 'a'); + + const flatSetting = { + [`${this.params.name}.aggregate`]: `${this.params.name}_aggregate.aggregate`, + }; return { query: { - name: `${queryLiteral}_${this.params.name}`, - variables: query_variables, + name: this.queryNameTemplate({ + literal: queryLiteral, + type: 'a', + }), + arguments: [...operation_arguments, ...fragmentOperationArguments], flatSetting, - fields: ` - ${this.params.name}_aggregate ${query_field_variables.length > 0 ? '(' + query_field_variables.join(', ') + ')' : ''} { - ${fields} - } - `, + fields: this.queryTemplate({ + table_postfix: '_aggregate', + arguments: query_arguments.length > 0 ? '(' + query_arguments.join(', ') + ')' : '', + fields, + }), }, variables, }; @@ -275,53 +211,24 @@ class Table { } build_insert(parameters) { - const {fields, fragment, fragmentName} = this.getFieldsFromParams(parameters); + const {fields, fragment, fragmentName, fragmentOperationArguments} = this.getFieldsFromParams(parameters); - const variables = {}; - const query_variables = []; - const query_field_variables = []; + var {variables, query_arguments, operation_arguments} = this.buildArguments(['objects', 'on_conflict'], parameters, 'i'); - const predifinedVariables = { - objects: { - type(name) { - return `[${name}_insert_input!]!`; - }, - }, - on_conflict: { - type(name) { - return `${name}_on_conflict`; - }, - }, + const flatSetting = { + [`${this.params.name}.insert`]: `insert_${this.params.name}.returning`, }; - Object.keys(predifinedVariables).forEach((varName) => { - const varOptions = predifinedVariables[varName]; - - const varKey = `i_${this.params.name}_${varName}`; - - if (parameters[varName]) { - variables[varKey] = parameters[varName]; - query_field_variables.push(`${varName}: $${varKey}`); - query_variables.push(`$${varKey}: ${varOptions.type(this.params.name)}`); - } - }); - - const flatKey = `${this.params.name}.insert`; - const flatSetting = {}; - flatSetting[flatKey] = `insert_${this.params.name}.returning`; - return { query: { - name: `I_${this.params.name}`, + name: this.queryNameTemplate({literal: 'M', type: 'i'}), flatSetting, - variables: query_variables, - fields: ` - insert_${this.params.name}(${query_field_variables.join(', ')}) { - returning { - ${fields} - } - } - `, + arguments: [...operation_arguments, ...fragmentOperationArguments], + fields: this.mutationTeamplate({ + table_prefix: 'insert_', + arguments: `(${query_arguments.join(', ')})`, + fields, + }), fragment: { fragment, fragmentName, @@ -332,58 +239,24 @@ class Table { } build_update(parameters) { - const {fields, fragment, fragmentName} = this.getFieldsFromParams(parameters); + const {fields, fragment, fragmentName, fragmentOperationArguments} = this.getFieldsFromParams(parameters); - const variables = {}; - const query_variables = []; - const query_field_variables = []; + var {variables, query_arguments, operation_arguments} = this.buildArguments(['where', '_set', '_inc'], parameters, 'u'); - const predifinedVariables = { - where: { - type(name) { - return `${name}_bool_exp!`; - }, - }, - _set: { - type(name) { - return `${name}_set_input`; - }, - }, - _inc: { - type(name) { - return `${name}_inc_input`; - }, - }, + const flatSetting = { + [`${this.params.name}.update`]: `update_${this.params.name}.returning`, }; - Object.keys(predifinedVariables).forEach((varName) => { - const varOptions = predifinedVariables[varName]; - - const varKey = `u_${this.params.name}_${varName}`; - - if (parameters[varName]) { - variables[varKey] = parameters[varName]; - query_field_variables.push(`${varName}: $${varKey}`); - query_variables.push(`$${varKey}: ${varOptions.type(this.params.name)}`); - } - }); - - const flatKey = `${this.params.name}.update`; - const flatSetting = {}; - flatSetting[flatKey] = `update_${this.params.name}.returning`; - return { query: { - name: `U_${this.params.name}`, + name: this.queryNameTemplate({literal: 'M', type: 'u'}), flatSetting, - variables: query_variables, - fields: ` - update_${this.params.name}(${query_field_variables.join(', ')}) { - returning { - ${fields} - } - } - `, + arguments: [...operation_arguments, ...fragmentOperationArguments], + fields: this.mutationTeamplate({ + table_prefix: 'update_', + arguments: `(${query_arguments.join(', ')})`, + fields, + }), fragment: { fragment, fragmentName, @@ -394,48 +267,24 @@ class Table { } build_delete(parameters) { - const {fields, fragment, fragmentName} = this.getFieldsFromParams(parameters); + const {fields, fragment, fragmentName, fragmentOperationArguments} = this.getFieldsFromParams(parameters); - const variables = {}; - const query_variables = []; - const query_field_variables = []; + var {variables, query_arguments, operation_arguments} = this.buildArguments(['where'], parameters, 'd'); - const predifinedVariables = { - where: { - type(name) { - return `${name}_bool_exp!`; - }, - }, + const flatSetting = { + [`${this.params.name}.delete`]: `delete_${this.params.name}.returning`, }; - Object.keys(predifinedVariables).forEach((varName) => { - const varOptions = predifinedVariables[varName]; - - const varKey = `d_${this.params.name}_${varName}`; - - if (parameters[varName]) { - variables[varKey] = parameters[varName]; - query_field_variables.push(`${varName}: $${varKey}`); - query_variables.push(`$${varKey}: ${varOptions.type(this.params.name)}`); - } - }); - - const flatKey = `${this.params.name}.delete`; - const flatSetting = {}; - flatSetting[flatKey] = `delete_${this.params.name}.returning`; - return { query: { - name: `D_${this.params.name}`, + name: this.queryNameTemplate({literal: 'M', type: 'd'}), flatSetting, - variables: query_variables, - fields: ` - delete_${this.params.name}(${query_field_variables.join(', ')}) { - returning { - ${fields} - } - } - `, + arguments: [...operation_arguments, ...fragmentOperationArguments], + fields: this.mutationTeamplate({ + table_prefix: 'delete_', + arguments: `(${query_arguments.join(', ')})`, + fields, + }), fragment: { fragment, fragmentName, @@ -446,9 +295,17 @@ class Table { } getFieldsFromParams(parameters) { + let fields = ''; let fragment = ''; let fragmentName = ''; - let fields = parameters.fields ? fieldsToGql(parameters.fields) : false; + let fragmentOperationArguments = []; + const gqlFields = parameters.fields ? fieldsToGql(parameters.fields) : false; + + if (gqlFields) { + fields = gqlFields.fields; + fragmentOperationArguments = gqlFields.fragmentOperationArgument; + } + if (!fields) { let fInstance = null; if (typeof parameters.fragment === 'string') { @@ -467,17 +324,26 @@ class Table { fragment = fragmentObject.raw; fragmentName = fragmentObject.name; fields = `...${fragmentObject.name}`; + fragmentOperationArguments = fragmentObject.arguments; } if (!fields) { throw new Error('no returning fields specified'); } - return {fragment, fragmentName, fields}; + return {fragment, fragmentName, fields, fragmentOperationArguments}; } getFieldsFromAggregate(parameters) { - let fields = parameters.fields ? fieldsToGql(parameters.fields) : false; + let fields = ''; + let fragmentOperationArguments = []; + const gqlFields = parameters.fields ? fieldsToGql(parameters.fields) : false; + + if (gqlFields) { + fields = gqlFields.fields; + fragmentOperationArguments = gqlFields.fragmentOperationArgument; + } + if (!fields) { const aggParameters = []; if (typeof parameters.count !== 'undefined') { @@ -500,7 +366,9 @@ class Table { buildQuery[aggParameter] = { children: parameters[aggParameter], }; - aggParameters.push(fieldsToGql(buildQuery)); + + const {fields} = fieldsToGql(buildQuery); + aggParameters.push(fields); } }); @@ -517,6 +385,49 @@ class Table { ${fields} } `, + fragmentOperationArguments, + }; + } + + /* + ArgumentKeys = ['where', ...] + type - d + */ + buildArguments(argumentKeys, parameters, type) { + const argumentDict = { + where: `${this.params.name}_bool_exp!`, + limit: `Int`, + offset: `Int`, + order_by: `[${this.params.name}_order_by!]`, + distinct_on: `[${this.params.name}_select_column!]`, + objects: `[${this.params.name}_insert_input!]!`, + on_conflict: `${this.params.name}_on_conflict`, + _set: `${this.params.name}_set_input`, + _inc: `${this.params.name}_inc_input`, + }; + + const variables = {}; + const query_arguments = []; + const operation_arguments = []; + argumentKeys.forEach((argumentKey) => { + const argumentType = argumentDict[argumentKey]; + const variableKey = `${type}_${this.params.name}_${argumentKey}`; + + // We will generate arguments only if client request contains them + if (parameters[argumentKey]) { + // Fill variables to pass to query + variables[variableKey] = parameters[argumentKey]; + // Query level arguments + query_arguments.push(`${argumentKey}: $${variableKey}`); + // Operation level variables definition + operation_arguments.push(`$${variableKey}: ${argumentType}`); + } + }); + + return { + variables, + query_arguments, + operation_arguments, }; } } diff --git a/src/utils/builders.js b/src/utils/builders.js index de47164..03d6bc9 100644 --- a/src/utils/builders.js +++ b/src/utils/builders.js @@ -1,9 +1,10 @@ /* Fields can be - string: ` id - name + logo (limit: $custom_limit) { + id + } ` array: [ @@ -12,14 +13,20 @@ Fields can be 'logo', [ 'id' - ] + ], + { + limit: 'custom_limit' + } ] or { key: 'logo', values: [ 'id', - ] + ], + filter: { + limit: 'custom_limit' + } } ] @@ -27,72 +34,173 @@ Fields can be 'id': {}, 'logo': { children: ['id'] + or children: ` id ` + or children: { 'id': {} } + + filter: { + limit: 'custom_limit' + } } } */ exports.fieldsToGql = function (input) { - const baseArray = []; + const fragmentFields = []; + const fragmentOperationArgument = []; + // Const fragmentVariables = {}; if (typeof input === 'string') { - return input; - } - - if (Array.isArray(input)) { + fragmentFields.push(input); + } else if (Array.isArray(input)) { input.forEach((element) => { if (typeof element === 'string') { - baseArray.push(element); + fragmentFields.push(element); + } else if (typeof element === 'object' && element._isFragment === true) { + const fields = element.fields(); + fragmentFields.push(fields); } else if (Array.isArray(element)) { - if (typeof element[0] !== 'string' || !Array.isArray(element[1])) { - throw new TypeError('array should contain exactly 2 values, key and children array'); + if (typeof element[0] !== 'string' || !Array.isArray(element[1]) || element.length > 3) { + throw new Error('[builders/fieldsToGql] array should contain exactly {2, 3} values, key and children array'); } const subFragment = exports.fieldsToGql(element[1]); - baseArray.push(` - ${element[0]} { - ${subFragment} + + const {argumentQuery, subQueryOperationArguments} = exports.queryFilters(element[2] || []); + fragmentOperationArgument.push(...subQueryOperationArguments); + fragmentOperationArgument.push(...subFragment.fragmentOperationArgument); + + fragmentFields.push(` + ${element[0]} ${argumentQuery} { + ${subFragment.fields} } `); } else if (typeof element === 'object') { if (typeof element.key === 'undefined' || typeof element.values === 'undefined') { - throw new TypeError('in array object must have keys: `key` & `values`'); + throw new TypeError('[builders/fieldsToGql] in array object must have keys: `key` & `values`'); } const subFragment = exports.fieldsToGql(element.values); - baseArray.push(` + fragmentOperationArgument.push(...subFragment.fragmentOperationArgument); + + fragmentFields.push(` ${element.key} { - ${subFragment} + ${subFragment.fields} } `); } else { - throw new TypeError(`unsupported type ${typeof element}`); + throw new TypeError(`[builders/fieldsToGql] unsupported type ${typeof element}`); } }); - return baseArray.join('\n'); - } - - if (typeof input === 'object') { + } else if (typeof input === 'object' && input._isFragment === true) { + const fields = input.fields(); + fragmentFields.push(fields); + } else if (typeof input === 'object') { Object.keys(input).forEach((key) => { const value = input[key]; if (value.children) { + const {argumentQuery, subQueryOperationArguments} = exports.queryFilters(value.arguments || []); + fragmentOperationArgument.push(...subQueryOperationArguments); + const subFragment = exports.fieldsToGql(value.children); - if (!subFragment) { - throw new Error(`cant create fields from object ${key}`); + fragmentOperationArgument.push(...subFragment.fragmentOperationArgument); + + if (!subFragment.fields) { + throw new Error(`[builders/fieldsToGql] cant create fields from object ${key}`); } - baseArray.push(` - ${key} { - ${subFragment} + fragmentFields.push(` + ${key}${argumentQuery} { + ${subFragment.fields} } `); } else { - baseArray.push(key); + fragmentFields.push(key); } }); - return baseArray.join('\n'); } - throw new Error(`unsupported type ${typeof input}`); + if (fragmentFields.length === 0) throw new Error(`[builders/fieldsToGql] unsupported type ${typeof input}`); + + return { + fields: fragmentFields.join('\n'), + fragmentOperationArgument, + }; +}; + +/* + Object { + _table: 'test', + limit: 'limit' + } + + return '' | (limit: $limit) +*/ +exports.queryFilters = function (input) { + const args = []; + const subQueryOperationArguments = []; + const table = input._table; + + if (!Array.isArray(input) && typeof input === 'object') { + if (!table) throw new Error('[builders/queryFilters] input._table is required'); + const argumentDict = { + where: `${table}_bool_exp`, + limit: `Int`, + offset: `Int`, + order_by: `[${table}_order_by!]`, + distinct_on: `[${table}_select_column!]`, + objects: `[${table}_insert_input!]!`, + on_conflict: `${table}_on_conflict`, + _set: `${table}_set_input`, + _inc: `${table}_inc_input`, + }; + + Object.keys(input).forEach((key) => { + if (key === '_table') return; + if (!argumentDict[key]) return; + + const value = input[key]; + if (typeof value !== 'string') throw new Error('[builders/queryFilters] value of object should be a string'); + + subQueryOperationArguments.push(`$${value}: ${argumentDict[key]}`); + args.push(`${key}: $${value}`); + }); + } + + const argumentQuery = args.length === 0 ? '' : `(${args.join(',')})`; + return {argumentQuery, subQueryOperationArguments}; +}; + +/* + Tag template function + let queryTemplate = template` + query ${'arguments'} { + ${'fields'} + } + ` + + queryTemplate({ + arguments: '(where: $where)', + fields: 'id' + }) => + + query (where: $where) { + id + } + +*/ +exports.template = function (strings, ...keys) { + return function (...values) { + const dict = values[values.length - 1] || {}; + const result = [strings[0]]; + keys.forEach(function (key, i) { + let value = ''; + if (Number.isInteger(key) && typeof values[key] !== 'undefined') value = values[key]; + else if (typeof dict[key] !== 'undefined') value = dict[key]; + else value = key; + result.push(value, strings[i + 1]); + }); + return result.join(''); + }; }; diff --git a/tests/aggregate.js b/tests/aggregate.js index c958753..2a833f7 100644 --- a/tests/aggregate.js +++ b/tests/aggregate.js @@ -127,10 +127,8 @@ test.serial('aggregate query test', async (t) => { }, }, }); - if (err) { - throw err; - } + t.is(err, null); t.is(response.count, 3); t.is(response.sum.increment, 18); t.is(response.min.increment, 3); @@ -155,9 +153,7 @@ test.serial('aggregate query test', async (t) => { }, }, }); - if (err) { - throw err; - } + t.is(err, null); t.is(response.count, 1); }); diff --git a/tests/fragment.js b/tests/fragment.js index 5c955ab..b84a4fe 100644 --- a/tests/fragment.js +++ b/tests/fragment.js @@ -45,15 +45,42 @@ test('getting fragment name', (t) => { table: 'test', fields: ['id'], }); + t.is(fragment.name(), 'base_fragment_test'); - // Right naming using table and fragment names - var {name} = fragment.build(); - t.is(name, 'base_fragment_test'); + const fragment2 = new Fragment({ + table: 'test2', + name: 'new', + fields: ['id'], + }); + t.is(fragment2.name(), 'new_fragment_test2'); +}); - fragment.params.table = 'test2'; - fragment.params.name = 'new'; - var {name} = fragment.build(); - t.is(name, 'new_fragment_test2'); +test('getting fragment arguments', (t) => { + const fragment = new Fragment({ + table: 'test', + fields: ['id'], + }); + t.deepEqual(fragment.arguments(), []); + + const fragment2 = new Fragment({ + table: 'test', + fields: [ + 'id', + [ + 'logo', + ['url'], + { + _table: 'images', + limit: 'logo_limit', + offset: 'logo_offset', + where: 'logo_where', + order_by: 'logo_order_by', + distinct_on: 'logo_distinct_on', + }, + ], + ], + }); + t.deepEqual(fragment2.arguments(), ['$logo_limit: Int', '$logo_offset: Int', '$logo_where: images_bool_exp', '$logo_order_by: [images_order_by!]', '$logo_distinct_on: [images_select_column!]']); }); test('checking fragment decalration', (t) => { @@ -79,16 +106,14 @@ test('checking fragment decalration', (t) => { }, ], }); - var {raw} = fragment.build(); - t.deepEqual(gql(raw), testFragment); + t.deepEqual(gql(fragment.fragment()), testFragment); // Declaring fields with array 2nd way var fragment = new Fragment({ table: 'test', fields: ['id', 'name', ['logo', ['url']]], }); - var {raw} = fragment.build(); - t.deepEqual(gql(raw), testFragment); + t.deepEqual(gql(fragment.fragment()), testFragment); // Declaring fileds with string var fragment = new Fragment({ @@ -101,8 +126,7 @@ test('checking fragment decalration', (t) => { } `, }); - var {raw} = fragment.build(); - t.deepEqual(gql(raw), testFragment); + t.deepEqual(gql(fragment.fragment()), testFragment); // Declaring fileds with object var fragment = new Fragment({ @@ -115,8 +139,39 @@ test('checking fragment decalration', (t) => { }, }, }); - var {raw} = fragment.build(); - t.deepEqual(gql(raw), testFragment); + t.deepEqual(gql(fragment.fragment()), testFragment); +}); + +test('nested arguments declaration', (t) => { + const testFragment = gql` + fragment base_fragment_test on test { + id + name + logo(where: $logo_where) { + url + } + } + `; + + var fragment = new Fragment({ + table: 'test', + name: 'base', + fields: { + id: {}, + name: {}, + logo: { + children: { + url: {}, + }, + arguments: { + _table: 'logo', + where: 'logo_where', + unexpected_argument: 'test', + }, + }, + }, + }); + t.deepEqual(gql(fragment.fragment()), testFragment); }); test('checking big declaration', (t) => { @@ -266,10 +321,10 @@ test('check extension', (t) => { name: 'main', table: 'test', fields: [ - baseTestFragment.gqlFields(), + baseTestFragment, { key: 'logo', - values: baseLogoFragment.gqlFields(), + values: baseLogoFragment, }, ], }); @@ -280,9 +335,9 @@ test('check extension', (t) => { name: 'main', table: 'test', fields: ` - ${baseTestFragment.gqlFields()} + ${baseTestFragment.fields()} logo { - ${baseLogoFragment.gqlFields()} + ${baseLogoFragment.fields()} } `, }); @@ -290,12 +345,12 @@ test('check extension', (t) => { t.deepEqual(gql(raw), testFragment); }); -test('check gqlFields function', (t) => { +test('check fields function', (t) => { const baseTestFragment = new Fragment({ name: 'base', table: 'test', fields: ['id', 'name'], }); - t.is(typeof baseTestFragment.gqlFields(), 'string'); + t.is(typeof baseTestFragment.fields(), 'string'); }); diff --git a/tests/nested-arguments.js b/tests/nested-arguments.js new file mode 100644 index 0000000..69b63ec --- /dev/null +++ b/tests/nested-arguments.js @@ -0,0 +1,97 @@ +require('dotenv').config(); +const test = require('ava'); +const {Hasura} = require('../src'); + +test.before(async (t) => { + const orm = new Hasura({ + graphqlUrl: process.env.GQL_ENDPOINT, + adminSecret: process.env.GQL_SECRET, + }); + await orm.init(); + + t.context.orm = orm; +}); + +test('nested draft', async (t) => { + const orm = t.context.orm; + + var [err] = await orm.mutate({ + _om_test: { + delete: { + where: {type: {_eq: 't2chNestedArgs'}}, + }, + }, + _om_test_types: { + delete: { + where: {type: {_eq: 't2chNestedArgs'}}, + }, + insert: { + objects: { + type: 't2chNestedArgs', + objects: { + data: [ + { + increment: 1, + text: 'test', + }, + { + increment: 2, + text: 'test2', + }, + ], + }, + }, + }, + }, + }); + if (err) throw err; + + const baseTestFragment = orm.table('_om_test').fragment('base'); + + orm.table('_om_test_types').createFragment('nested', [ + 'type', + [ + 'objects', + [baseTestFragment], + { + _table: '_om_test', + limit: 'objects_limit', + where: 'objects_where', + }, + ], + ]); + + var [err, response] = await orm.query({ + _om_test_types: { + fragment: 'nested', + where: {type: {_eq: 't2chNestedArgs'}}, + }, + }); + if (err) throw err; + t.is(response[0].objects.length, 2); + + var [err, response] = await orm.query({ + _om_test_types: { + fragment: 'nested', + where: {type: {_eq: 't2chNestedArgs'}}, + variables: { + objects_limit: 1, + }, + }, + }); + if (err) throw err; + t.is(response[0].objects.length, 1); + + var [err, response] = await orm.query({ + _om_test_types: { + fragment: 'nested', + where: {type: {_eq: 't2chNestedArgs'}}, + variables: { + objects_where: {increment: {_eq: 1}}, + }, + }, + }); + if (err) throw err; + t.is(response[0].objects.length, 1); + t.is(response[0].objects[0].text, 'test'); +});