Socket
Socket
Sign inDemoInstall

graphql-connections

Package Overview
Dependencies
267
Maintainers
2
Versions
13
Alerts
File Explorer

Advanced tools

Install Socket

Detect and block malicious and high-risk dependencies

Install

Comparing version 7.0.4 to 9.0.0

dist/coerce_string_value.d.ts

4

dist/connection_manager.d.ts

@@ -28,3 +28,3 @@ import { QueryBuilder as Knex } from 'knex';

addResult(result: KnexQueryResult): this;
readonly pageInfo: {
get pageInfo(): {
hasNextPage: boolean;

@@ -35,3 +35,3 @@ hasPreviousPage: boolean;

};
readonly edges: {
get edges(): {
cursor: string;

@@ -38,0 +38,0 @@ node: Node;

@@ -6,2 +6,3 @@ 'use strict';

var graphql = require('graphql');
var utilities = require('graphql/utilities');

@@ -209,2 +210,31 @@ class CursorEncoder {

const hasDotRegexp = /\./gi;
// tslint:disable-next-line: cyclomatic-complexity
function coerceStringValue(value) {
if (value === '') {
return value;
}
/**
* Only try casting to float if there's at least one `.`
*
* This MUST come before parseInt because parseInt will succeed to
* parse a float but it will be lossy, e.g.
* parseInt('1.24242', 10) === 1
*/
if (hasDotRegexp.test(value) && !isNaN(Number(value))) {
return Number(value);
}
if (!isNaN(Number(value))) {
const parsed = Number(value);
return parsed;
}
if (['true', 'false'].includes(value.toLowerCase())) {
return value.toLowerCase() === 'true';
}
if (value.toLowerCase() === 'null') {
return null;
}
return value;
}
/**

@@ -286,26 +316,26 @@ * KnexQueryBuilder

filterArgs(filter) {
const transformedFilter = this.filterTransformer(filter);
if (this.useSuggestedValueLiteralTransforms &&
transformedFilter.operator.toLowerCase() === '=' &&
(transformedFilter.value === null || transformedFilter.value.toLowerCase() === 'null')) {
const { field, operator, value } = this.filterTransformer(filter);
if (this.useSuggestedValueLiteralTransforms) {
const coercedValue = typeof value === 'string' ? coerceStringValue(value) : value;
if (coercedValue === null && operator.toLowerCase() === '=') {
return [
(builder) => {
builder.whereNull(this.computeFilterField(field));
}
];
}
if (coercedValue === null && operator.toLowerCase() === '<>') {
return [
(builder) => {
builder.whereNotNull(this.computeFilterField(field));
}
];
}
return [
(builder) => {
builder.whereNull(this.computeFilterField(transformedFilter.field));
}
this.computeFilterField(field),
this.computeFilterOperator(operator),
coercedValue
];
}
if (this.useSuggestedValueLiteralTransforms &&
transformedFilter.operator.toLowerCase() === '<>' &&
(transformedFilter.value === null || transformedFilter.value.toLowerCase() === 'null')) {
return [
(builder) => {
builder.whereNotNull(this.computeFilterField(transformedFilter.field));
}
];
}
return [
this.computeFilterField(transformedFilter.field),
this.computeFilterOperator(transformedFilter.operator),
transformedFilter.value
];
return [this.computeFilterField(field), this.computeFilterOperator(operator), value];
}

@@ -352,6 +382,9 @@ addFilterRecursively(filter, queryBuilder) {

const isFilter = (filter) => {
return (!!filter &&
!!filter.field &&
!!filter.operator &&
!!filter.value);
if (!filter) {
return false;
}
const asIFilter = filter;
return (asIFilter.field !== undefined &&
asIFilter.operator !== undefined &&
asIFilter.value !== undefined);
};

@@ -617,7 +650,12 @@

description,
serialize: (value) => value,
serialize: (value) => String(value),
parseValue: (value) => {
const hasType = inputTypes.reduce((acc, t) => {
const result = graphql.coerceValue(value, t);
return result.errors && result.errors.length > 0 ? acc : true;
try {
const result = utilities.coerceInputValue(value, t);
return result.errors && result.errors.length > 0 ? acc : true;
}
catch (error) {
return acc;
}
}, false);

@@ -629,16 +667,65 @@ if (hasType) {

},
// tslint:disable-next-line: cyclomatic-complexity
parseLiteral: ast => {
const inputType = inputTypes.reduce((acc, type) => {
const astClone = JSON.parse(JSON.stringify(ast));
try {
return graphql.isValidLiteralValue(type, astClone).length === 0 ? type : acc;
}
catch (e) {
const compoundFilterScalarType = inputTypes.find(type => type.name === 'CompoundFilterScalar');
const filterScalarType = inputTypes.find(type => type.name === 'FilterScalar');
if (!compoundFilterScalarType) {
throw new Error('Invalid input type provided');
}
if (!filterScalarType) {
throw new Error('Invalid input type provided');
}
if (ast.kind !== 'ObjectValue') {
throw new Error('Invalid AST kind');
}
/**
* Determine if the scalar provided is a compound (or, and)
* or plain filter scalar (field, operator, value)
* AND it must only have one of these present in the object root.
*/
const isCompoundFilterScalar = ast.fields.reduce((acc, field) => {
if (acc) {
return acc;
}
}, undefined);
if (inputType) {
return graphql.valueFromAST(ast, inputType);
if (['or', 'and', 'not'].includes(field.name.value.toLowerCase())) {
return true;
}
return acc;
}, false) && ast.fields.length === 1;
/** Determine if it is a filter scalar. */
const filterScalarFields = ast.fields
.map(field => field.name.value.toLowerCase())
.reduce((acc, fieldName) => {
if (fieldName === 'field') {
return {
...acc,
hasField: true
};
}
if (fieldName === 'operator') {
return {
...acc,
hasOperator: true
};
}
if (fieldName === 'value') {
return {
...acc,
hasValue: true
};
}
return acc;
}, { hasField: false, hasOperator: false, hasValue: false });
const isFilterScalar = filterScalarFields.hasField &&
filterScalarFields.hasOperator &&
filterScalarFields.hasValue;
if (!isCompoundFilterScalar && !isFilterScalar) {
throw generateInputTypeError(typeName, inputTypes);
}
throw generateInputTypeError(typeName, inputTypes);
if (isCompoundFilterScalar) {
return graphql.valueFromAST(ast, compoundFilterScalarType);
}
else {
return graphql.valueFromAST(ast, filterScalarType);
}
}

@@ -648,2 +735,28 @@ });

// tslint:disable: cyclomatic-complexity
/** @see https://stackoverflow.com/a/49911974 */
// tslint:disable-next-line: variable-name
const FilterValue = new graphql.GraphQLScalarType({
name: 'FilterValue',
serialize: value => value,
/**
* `parseValue` controls what is seen by the resolver.
*/
parseValue: value => value,
/**
* `parseLiteral` inputs the AST and returns the parsed value of the type.
*/
parseLiteral(ast) {
if (ast.kind === graphql.Kind.NULL) {
return null;
}
if (ast.kind === graphql.Kind.INT ||
ast.kind === graphql.Kind.FLOAT ||
ast.kind === graphql.Kind.BOOLEAN ||
ast.kind === graphql.Kind.STRING) {
return ast.value;
}
throw new Error('An invalid type was given for filter value. Must be either Int, Float, Boolean, Null, or String.');
}
});
const compoundFilterScalar = new graphql.GraphQLInputObjectType({

@@ -676,3 +789,3 @@ name: 'CompoundFilterScalar',

value: {
type: graphql.GraphQLString
type: FilterValue
}

@@ -804,2 +917,3 @@ };

};
// tslint:enable: cyclomatic-complexity

@@ -806,0 +920,0 @@ exports.ConnectionManager = ConnectionManager;

@@ -1,2 +0,3 @@

import { GraphQLScalarType, coerceValue, GraphQLError, isValidLiteralValue, valueFromAST, GraphQLInputObjectType, GraphQLList, GraphQLString, GraphQLInt } from 'graphql';
import { GraphQLScalarType, GraphQLError, valueFromAST, Kind, GraphQLInputObjectType, GraphQLList, GraphQLString, GraphQLInt } from 'graphql';
import { coerceInputValue } from 'graphql/utilities';

@@ -204,2 +205,31 @@ class CursorEncoder {

const hasDotRegexp = /\./gi;
// tslint:disable-next-line: cyclomatic-complexity
function coerceStringValue(value) {
if (value === '') {
return value;
}
/**
* Only try casting to float if there's at least one `.`
*
* This MUST come before parseInt because parseInt will succeed to
* parse a float but it will be lossy, e.g.
* parseInt('1.24242', 10) === 1
*/
if (hasDotRegexp.test(value) && !isNaN(Number(value))) {
return Number(value);
}
if (!isNaN(Number(value))) {
const parsed = Number(value);
return parsed;
}
if (['true', 'false'].includes(value.toLowerCase())) {
return value.toLowerCase() === 'true';
}
if (value.toLowerCase() === 'null') {
return null;
}
return value;
}
/**

@@ -281,26 +311,26 @@ * KnexQueryBuilder

filterArgs(filter) {
const transformedFilter = this.filterTransformer(filter);
if (this.useSuggestedValueLiteralTransforms &&
transformedFilter.operator.toLowerCase() === '=' &&
(transformedFilter.value === null || transformedFilter.value.toLowerCase() === 'null')) {
const { field, operator, value } = this.filterTransformer(filter);
if (this.useSuggestedValueLiteralTransforms) {
const coercedValue = typeof value === 'string' ? coerceStringValue(value) : value;
if (coercedValue === null && operator.toLowerCase() === '=') {
return [
(builder) => {
builder.whereNull(this.computeFilterField(field));
}
];
}
if (coercedValue === null && operator.toLowerCase() === '<>') {
return [
(builder) => {
builder.whereNotNull(this.computeFilterField(field));
}
];
}
return [
(builder) => {
builder.whereNull(this.computeFilterField(transformedFilter.field));
}
this.computeFilterField(field),
this.computeFilterOperator(operator),
coercedValue
];
}
if (this.useSuggestedValueLiteralTransforms &&
transformedFilter.operator.toLowerCase() === '<>' &&
(transformedFilter.value === null || transformedFilter.value.toLowerCase() === 'null')) {
return [
(builder) => {
builder.whereNotNull(this.computeFilterField(transformedFilter.field));
}
];
}
return [
this.computeFilterField(transformedFilter.field),
this.computeFilterOperator(transformedFilter.operator),
transformedFilter.value
];
return [this.computeFilterField(field), this.computeFilterOperator(operator), value];
}

@@ -347,6 +377,9 @@ addFilterRecursively(filter, queryBuilder) {

const isFilter = (filter) => {
return (!!filter &&
!!filter.field &&
!!filter.operator &&
!!filter.value);
if (!filter) {
return false;
}
const asIFilter = filter;
return (asIFilter.field !== undefined &&
asIFilter.operator !== undefined &&
asIFilter.value !== undefined);
};

@@ -612,7 +645,12 @@

description,
serialize: (value) => value,
serialize: (value) => String(value),
parseValue: (value) => {
const hasType = inputTypes.reduce((acc, t) => {
const result = coerceValue(value, t);
return result.errors && result.errors.length > 0 ? acc : true;
try {
const result = coerceInputValue(value, t);
return result.errors && result.errors.length > 0 ? acc : true;
}
catch (error) {
return acc;
}
}, false);

@@ -624,16 +662,65 @@ if (hasType) {

},
// tslint:disable-next-line: cyclomatic-complexity
parseLiteral: ast => {
const inputType = inputTypes.reduce((acc, type) => {
const astClone = JSON.parse(JSON.stringify(ast));
try {
return isValidLiteralValue(type, astClone).length === 0 ? type : acc;
}
catch (e) {
const compoundFilterScalarType = inputTypes.find(type => type.name === 'CompoundFilterScalar');
const filterScalarType = inputTypes.find(type => type.name === 'FilterScalar');
if (!compoundFilterScalarType) {
throw new Error('Invalid input type provided');
}
if (!filterScalarType) {
throw new Error('Invalid input type provided');
}
if (ast.kind !== 'ObjectValue') {
throw new Error('Invalid AST kind');
}
/**
* Determine if the scalar provided is a compound (or, and)
* or plain filter scalar (field, operator, value)
* AND it must only have one of these present in the object root.
*/
const isCompoundFilterScalar = ast.fields.reduce((acc, field) => {
if (acc) {
return acc;
}
}, undefined);
if (inputType) {
return valueFromAST(ast, inputType);
if (['or', 'and', 'not'].includes(field.name.value.toLowerCase())) {
return true;
}
return acc;
}, false) && ast.fields.length === 1;
/** Determine if it is a filter scalar. */
const filterScalarFields = ast.fields
.map(field => field.name.value.toLowerCase())
.reduce((acc, fieldName) => {
if (fieldName === 'field') {
return {
...acc,
hasField: true
};
}
if (fieldName === 'operator') {
return {
...acc,
hasOperator: true
};
}
if (fieldName === 'value') {
return {
...acc,
hasValue: true
};
}
return acc;
}, { hasField: false, hasOperator: false, hasValue: false });
const isFilterScalar = filterScalarFields.hasField &&
filterScalarFields.hasOperator &&
filterScalarFields.hasValue;
if (!isCompoundFilterScalar && !isFilterScalar) {
throw generateInputTypeError(typeName, inputTypes);
}
throw generateInputTypeError(typeName, inputTypes);
if (isCompoundFilterScalar) {
return valueFromAST(ast, compoundFilterScalarType);
}
else {
return valueFromAST(ast, filterScalarType);
}
}

@@ -643,2 +730,28 @@ });

// tslint:disable: cyclomatic-complexity
/** @see https://stackoverflow.com/a/49911974 */
// tslint:disable-next-line: variable-name
const FilterValue = new GraphQLScalarType({
name: 'FilterValue',
serialize: value => value,
/**
* `parseValue` controls what is seen by the resolver.
*/
parseValue: value => value,
/**
* `parseLiteral` inputs the AST and returns the parsed value of the type.
*/
parseLiteral(ast) {
if (ast.kind === Kind.NULL) {
return null;
}
if (ast.kind === Kind.INT ||
ast.kind === Kind.FLOAT ||
ast.kind === Kind.BOOLEAN ||
ast.kind === Kind.STRING) {
return ast.value;
}
throw new Error('An invalid type was given for filter value. Must be either Int, Float, Boolean, Null, or String.');
}
});
const compoundFilterScalar = new GraphQLInputObjectType({

@@ -671,3 +784,3 @@ name: 'CompoundFilterScalar',

value: {
type: GraphQLString
type: FilterValue
}

@@ -799,3 +912,4 @@ };

};
// tslint:enable: cyclomatic-complexity
export { ConnectionManager, CursorEncoder, KnexQueryBuilder as Knex, KnexMySQLFullTextQueryBuilder as KnexMySQL, QueryContext, QueryResult, gqlTypes, resolvers, typeDefs };

@@ -41,3 +41,3 @@ import { ICursorObj, IQueryContext, IInputArgs, IQueryContextOptions, IInputFilter } from './types';

*/
readonly isPagingBackwards: boolean;
get isPagingBackwards(): boolean;
/**

@@ -44,0 +44,0 @@ * Sets the limit for the desired query result

@@ -22,3 +22,3 @@ import { IQueryContext, ICursorObj, IQueryResult, IQueryResultOptions } from './types';

constructor(result: Result, queryContext: QueryContext, options?: IQueryResultOptions<ICursorObj<string>, Node>);
readonly pageInfo: {
get pageInfo(): {
hasPreviousPage: boolean;

@@ -34,12 +34,12 @@ hasNextPage: boolean;

*/
readonly hasNextPage: boolean;
readonly hasPrevPage: boolean;
get hasNextPage(): boolean;
get hasPrevPage(): boolean;
/**
* The first cursor in the nodes list
*/
readonly startCursor: string;
get startCursor(): string;
/**
* The last cursor in the nodes list
*/
readonly endCursor: string;
get endCursor(): string;
/**

@@ -46,0 +46,0 @@ * It is very likely the results we get back from the data store

import { ORDER_DIRECTION } from './enums';
export interface IFilter {
value: string;
value: string | boolean | number | null;
operator: string;

@@ -5,0 +5,0 @@ field: string;

{
"name": "graphql-connections",
"version": "7.0.4",
"version": "9.0.0",
"description": "Build and handle Relay-like GraphQL connections using a Knex query builder",

@@ -60,4 +60,3 @@ "main": "dist/index.cjs.js",

"knex": "0.20.13",
"graphql": "14.7.0",
"graphql-tools": "^4.0.4"
"graphql": "14.7.0"
},

@@ -67,3 +66,2 @@ "devDependencies": {

"@types/faker": "^4.1.5",
"@types/graphql": "^14.0.7",
"@types/jest": "^24.0.9",

@@ -77,3 +75,3 @@ "@types/superagent": "^4.1.1",

"koa": "^2.7.0",
"mysql2": "^1.6.5",
"mysql2": "2.2.5",
"nodemon": "^1.18.10",

@@ -84,3 +82,3 @@ "prettier": "^1.16.4",

"rollup-plugin-typescript2": "^0.24.3",
"sqlite3": "^4.1.1",
"sqlite3": "5.0.2",
"supertest": "^4.0.2",

@@ -92,4 +90,4 @@ "ts-jest": "^24.0.0",

"tslint-eslint-rules": "^5.4.0",
"typescript": "^3.3.3333"
"typescript": "4.1.3"
}
}
# GraphQL-Connections :diamond_shape_with_a_dot_inside:
- [GraphQL-Connections :diamond_shape_with_a_dot_inside:](#graphql-connections-diamondshapewithadotinside)
- [GraphQL-Connections :diamond_shape_with_a_dot_inside:](#graphql-connections-diamond_shape_with_a_dot_inside)
- [Install](#install)

@@ -23,3 +23,3 @@ - [About](#about)

- [C. specify an attributeMap](#c-specify-an-attributemap)
- [2. build the query query](#2-build-the-query-query)
- [2. build the query](#2-build-the-query)
- [3. execute the query and build the `connection`](#3-execute-the-query-and-build-the-connection)

@@ -42,4 +42,4 @@ - [Options](#options)

- [Search](#search)
- [Filtering on computed columns](#filtering-on-computed-columns)
## Install

@@ -55,3 +55,3 @@

`GraphQL-Connections` helps handle the traversal of edges between nodes.
`GraphQL-Connections` helps handle the traversal of edges between nodes.

@@ -72,3 +72,3 @@ In a graph, nodes connect to other nodes via edges. In the relay graphql spec, multiple edges can be represented as a single `Connection` type, which has the signature:

A connection object is returned to a user when a `query request` asks for multiple child nodes connected to a parent node.
A connection object is returned to a user when a `query request` asks for multiple child nodes connected to a parent node.
For example, a music artist has multiple songs. In order to get all the `songs` for an `artist` you would write the graphql query request:

@@ -78,35 +78,33 @@

query {
artist(id: 1) {
songs {
songName
songLength
artist(id: 1) {
songs {
songName
songLength
}
}
}
}
```
However, sometimes you may only want a portion of the songs returned to you. To allow for this scenario, a `connection` is used to represent the response type of a `song`.
However, sometimes you may only want a portion of the songs returned to you. To allow for this scenario, a `connection` is used to represent the response type of a `song`.
```graphql
query {
artist(id: 1) {
songs {
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
edges {
cursor
node {
songName
songLength
artist(id: 1) {
songs {
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
edges {
cursor
node {
songName
songLength
}
}
}
}
}
}
}
```

@@ -144,15 +142,14 @@

3. Support multiple input types that can sort, group, limit, and filter the edges in a connection
## Run locally
1. Run the migrations
- `NODE_ENV=development npm run migrate:sqlite:latest`
- `NODE_ENV=development npm run migrate:mysql:latest`
2. Seed the database
- `NODE_ENV=development npm run seed:sqlite:run`
- `NODE_ENV=development npm run seed:mysql:run`
3. Run the dev server
- `npm run dev:sqlite` (search is not supported)
- `npm run dev:mysql` (search IS supported :))
1. Run the migrations
- `NODE_ENV=development npm run migrate:sqlite:latest`
- `NODE_ENV=development npm run migrate:mysql:latest`
2. Seed the database
- `NODE_ENV=development npm run seed:sqlite:run`
- `NODE_ENV=development npm run seed:mysql:run`
3. Run the dev server
- `npm run dev:sqlite` (search is not supported)
- `npm run dev:mysql` (search IS supported :))
4. Visit the GraphQL playground [http://localhost:4000/graphql](http://localhost:4000/graphql)

@@ -163,28 +160,30 @@ 5. Run some queries!

query {
users(
first: 100,
orderBy: "haircolor",
filter: { and: [
{field: "id", operator: ">", value: "19990"},
{field: "age", operator: "<", value: "90"},
]},
search: "random search term"
) {
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
users(
first: 100
orderBy: "haircolor"
filter: {
and: [
{field: "id", operator: ">", value: "19990"}
{field: "age", operator: "<", value: "90"}
]
}
search: "random search term"
) {
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
edges {
cursor
node {
username
lastname
id
haircolor
bio
}
}
}
edges {
cursor
node {
username
lastname
id
haircolor
bio
}
}
}
}

@@ -195,21 +194,18 @@ ```

query {
users(
first: 10,
after: "eyJmaXJzdFJlc3VsdElkIjoxOTk5MiwibGFzdFJlc3VsdE"
) {
pageInfo {
hasNextPage
hasPreviousPage
users(first: 10, after: "eyJmaXJzdFJlc3VsdElkIjoxOTk5MiwibGFzdFJlc3VsdE") {
pageInfo {
hasNextPage
hasPreviousPage
}
edges {
cursor
node {
username
lastname
id
haircolor
bio
}
}
}
edges {
cursor
node {
username
lastname
id
haircolor
bio
}
}
}
}

@@ -228,12 +224,12 @@ ```

type Query {
users(
first: First
last: Last
orderBy: OrderBy
orderDir: OrderDir
before: Before
after: After
filter: Filter
search: Search
): QueryUsersConnection
users(
first: First
last: Last
orderBy: OrderBy
orderDir: OrderDir
before: Before
after: After
filter: Filter
search: Search
): QueryUsersConnection
}

@@ -346,5 +342,3 @@ ```

// create a new node connection instance
const nodeConnection = new ConnectionManager<
INode,
>(inputArgs, attributeMap);
const nodeConnection = new ConnectionManager<INode>(inputArgs, attributeMap);

@@ -355,3 +349,3 @@ // apply the connection to the queryBuilder

// run the query
const result = await appliedQuery.select()
const result = await appliedQuery.select();

@@ -366,3 +360,3 @@ // add the result to the nodeConnection

};
}
};
```

@@ -398,3 +392,3 @@

addResult: (KnexQueryResult) => void
pageInfo?: IPageInfo
pageInfo?: IPageInfo
edges?: IEdge[]

@@ -408,5 +402,5 @@

}
interface IEdge {
cursor: string;
cursor: string;
node: INode

@@ -423,6 +417,6 @@ }

To use a `nodeConnection` you will have to:
1. initialize the nodeConnection
2. build the connection query
3. build the connection from the executed query
1. initialize the nodeConnection
2. build the connection query
3. build the connection from the executed query

@@ -437,3 +431,3 @@ #### 1. Initialize the `nodeConnection`

For example, in this case we create an `IUserNode`
For example, in this case we create an `IUserNode`

@@ -458,3 +452,3 @@ ```typescript

orderBy?: string; // order by a node field
orderDir: 'asc' | 'desc'
orderDir: 'asc' | 'desc';
filter?: IOperationFilter;

@@ -482,31 +476,37 @@ }

query {
users(filter: {
or: [
{ field: "age", operator: "=", value: "40"},
{ field: "age", operator: "<", value: "30"},
{ and: [
{ field: "haircolor", operator: "=", value: "blue"},
{ field: "age", operator: "=", value: "70"},
{ or: [
{ field: "username", operator: "=", value: "Ellie86"},
{ field: "username", operator: "=", value: "Euna_Oberbrunner"},
]}
]},
],
}) {
pageInfo {
hasNextPage
hasPreviousPage
users(
filter: {
or: [
{field: "age", operator: "=", value: "40"}
{field: "age", operator: "<", value: "30"}
{
and: [
{field: "haircolor", operator: "=", value: "blue"}
{field: "age", operator: "=", value: "70"}
{
or: [
{field: "username", operator: "=", value: "Ellie86"}
{field: "username", operator: "=", value: "Euna_Oberbrunner"}
]
}
]
}
]
}
) {
pageInfo {
hasNextPage
hasPreviousPage
}
edges {
cursor
node {
id
age
haircolor
lastname
username
}
}
}
edges {
cursor
node {
id
age
haircolor
lastname
username
}
}
}
}

@@ -518,7 +518,7 @@ ```

```sql
SELECT *
FROM `mock`
WHERE `age` = '40' OR `age` < '30' OR (`haircolor` = 'blue' AND `age` = '70' AND (`username` = 'Ellie86' OR `username` = 'Euna_Oberbrunner'))
ORDER BY `id`
ASC
SELECT *
FROM `mock`
WHERE `age` = '40' OR `age` < '30' OR (`haircolor` = 'blue' AND `age` = '70' AND (`username` = 'Ellie86' OR `username` = 'Euna_Oberbrunner'))
ORDER BY `id`
ASC
LIMIT 1001

@@ -533,5 +533,5 @@ ```

ex:
ex:
```typescript
```typescript
const attributeMap = {

@@ -543,3 +543,3 @@ id: 'id',

#### 2. build the query query
#### 2. build the query

@@ -606,3 +606,3 @@ ```typescript

```typescript
const options = {
const options = {
contextOptions: { ... }

@@ -622,3 +622,3 @@ resultOptions: { ... }

```typescript
number
number;
```

@@ -642,3 +642,3 @@

```typescript
type filterTransformer = (filter: IFilter) => IFilter
type filterTransformer = (filter: IFilter) => IFilter;
```

@@ -654,3 +654,3 @@

interface IFilterMap {
[operator: string]: string
[operator: string]: string;
}

@@ -689,3 +689,3 @@ ```

```typescript
searchColumns: string
searchColumns: string;
```

@@ -733,8 +733,8 @@

Internally, the `ConnectionManager` manages the orchestration of the `QueryContext`, `QueryBuilder`, and `QueryResult`.
Internally, the `ConnectionManager` manages the orchestration of the `QueryContext`, `QueryBuilder`, and `QueryResult`.
The orchestration follows the steps:
1. The `QueryContext` extracts the connection attributes from the input connection arguments.
2. The `QueryBuilder` (or `KnexQueryBuilder` in the default case) consumes the connection attributes and builds a query. The query is submitted to the database by the user and the result is sent to the `QueryResult`.
1. The `QueryContext` extracts the connection attributes from the input connection arguments.
2. The `QueryBuilder` (or `KnexQueryBuilder` in the default case) consumes the connection attributes and builds a query. The query is submitted to the database by the user and the result is sent to the `QueryResult`.
3. The `QueryResult` uses the result to build the `edges` (which contain a `cursor` and `node`) and extract the `page info`.

@@ -744,3 +744,3 @@

![Image of Architecture](https://docs.google.com/drawings/d/e/2PACX-1vRwtC2UiFwLXFDbmBNoq_6bD1YTyACV49SWHxfj2ce_K5T_XEZYlgGP7ntbcskoMVWqXp5C2Uj-K7Jj/pub?w=1163&amp;h=719)
![Image of Architecture](https://docs.google.com/drawings/d/e/2PACX-1vRwtC2UiFwLXFDbmBNoq_6bD1YTyACV49SWHxfj2ce_K5T_XEZYlgGP7ntbcskoMVWqXp5C2Uj-K7Jj/pub?w=1163&h=719)

@@ -783,1 +783,72 @@ ## Search

```
## Filtering on computed columns
Sometimes you may compute a field that depends on some other table than the one being paged over. In this case, you can use a derived table as your `from` and alias it to the primary table. In the following example we create a derived alias of "segment", the table we are paging over, to allow filtering and sorting on "popularity", a field computed on the aggregation of values from another table.
```ts
import {Resolver} from 'types/resolver';
import {ISegmentNode} from 'types/graphql';
import {segment as segmentTransformer} from 'transformers/sql_to_graphql';
import {IQueryResult, IInputArgs, ConnectionManager} from 'graphql-connections';
const attributeMap = {
createdAt: 'created_at',
updatedAt: 'updated_at',
name: 'name',
explorer: 'explorer_id',
popularity: 'popularity'
};
const segments: Resolver<Promise<IQueryResult<ISegmentNode>>, undefined, IInputArgs> = async (
_,
input: IInputArgs,
ctx
) => {
const {connection} = ctx.clients.sqlClient;
const queryBuilder = connection.queryBuilder().from(
connection.raw(
`(
select
segment.*,
coalesce(sum(user_segment.usage_count), 0) as popularity
from segment
left join user_segment on user_segment.segment_id = segment.id
group by
segment.id
) as segment`
)
);
const nodeConnection = new ConnectionManager<ISegmentNode>(input, attributeMap, {
builderOptions: {
filterTransformer(filter) {
if (filter.field === 'popularity') {
return {
field: 'popularity',
operator: filter.operator,
value: Number(filter.value) as any
};
}
return filter;
}
},
resultOptions: {
nodeTransformer: segmentTransformer
}
});
const query = nodeConnection.createQuery(queryBuilder).select('*');
nodeConnection.addResult(await query);
return {
pageInfo: nodeConnection.pageInfo,
edges: nodeConnection.edges
};
};
export default segments;
```
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc