-
Notifications
You must be signed in to change notification settings - Fork 224
/
index.js
196 lines (180 loc) · 9.38 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
import assert from 'assert'
import * as queryAST from './query-ast-to-sql-ast'
import arrToConnection from './array-to-connection'
import AliasNamespace from './alias-namespace'
import nextBatch from './batch-planner'
import {
buildWhereFunction,
handleUserDbCall,
compileSqlAST,
getConfigFromSchemaObject,
} from './util'
/* _ _ _ _
___ __ _| | | |__ __ _ ___| | __
/ __/ _` | | | '_ \ / _` |/ __| |/ /
| (_| (_| | | | |_) | (_| | (__| <
\___\__,_|_|_|_.__/ \__,_|\___|_|\_\
_ __ _ _ _ _
__| | ___ / _(_)_ __ (_) |_(_) ___ _ __ ___
/ _` |/ _ \ |_| | '_ \| | __| |/ _ \| '_ \/ __|
| (_| | __/ _| | | | | | |_| | (_) | | | \__ \
\__,_|\___|_| |_|_| |_|_|\__|_|\___/|_| |_|___/
*/
/**
* User-defined function that sends a raw SQL query to the databse.
* @callback dbCall
* @param {String} sql - The SQL generated by `joinMonster` for the batch fetching. Use it to get the data from your database.
* @param {Function} [done] - An error-first "done" callback. Only define this parameter if you don't want to return a `Promise`.
* @returns {Promise.<Array>} The raw data as a flat array of objects. Each object must represent a row from the result set.
*/
/**
* Function for generating a SQL expression.
* @callback sqlExpr
* @param {String} tableAlias - The alias generated for this table. Already double-quoted.
* @param {Object} args - The GraphQL arguments for this field.
* @param {Object} context - An Object with arbitrary contextual information.
* @param {Object} sqlASTNode - Join Monster object that abstractly represents this field. Also includes a reference to its parent node. This is useful, for example, if you need to access the parent field's table alias or GraphQL arguments.
* @returns {String|Promise.<String>} The RAW expression interpolated into the query to compute the column. Unsafe user input must be scrubbed.
*/
/**
* Function for generating a `WHERE` condition.
* @callback where
* @param {String} tableAlias - The alias generated for this table. Already double-quoted.
* @param {Object} args - The GraphQL arguments for this field.
* @param {Object} context - An Object with arbitrary contextual information.
* @param {Object} sqlASTNode - Join Monster object that abstractly represents this field. Also includes a reference to its parent node. This is useful, for example, if you need to access the parent field's table alias or GraphQL arguments.
* @returns {String|Promise.<String>} The RAW condition for the `WHERE` clause. Omitted if falsy value returned. Unsafe user input must be scrubbed.
*/
/**
* Function for generating a `JOIN` condition.
* @callback sqlJoin
* @param {String} parentTable - The alias generated for the parent's table. Already double-quoted.
* @param {String} childTable - The alias for the child's table. Already double-quoted.
* @param {Object} args - The GraphQL arguments for this field.
* @param {Object} context - An Object with arbitrary contextual information.
* @param {Object} sqlASTNode - Join Monster object that abstractly represents this field. Also includes a reference to its parent node. This is useful, for example, if you need to access the parent field's table alias or GraphQL arguments.
* @returns {String} The RAW condition for the `LEFT JOIN`. Unsafe user input must be scrubbed.
*/
/**
* Rather than a constant value, its a function to dynamically return the value.
* @callback thunk
* @param {Object} args - The GraphQL arguments for this field.
* @param {Object} context - An Object with arbitrary contextual information.
*/
/* _ _
| |__ ___ __ _(_)_ __ ___ ___ _ _ _ __ ___ ___
| '_ \ / _ \/ _` | | '_ \ / __|/ _ \| | | | '__/ __/ _ \
| |_) | __/ (_| | | | | | \__ \ (_) | |_| | | | (_| __/
|_.__/ \___|\__, |_|_| |_| |___/\___/ \__,_|_| \___\___|
|___/
*/
/**
* Takes the GraphQL resolveInfo and returns a hydrated Object with the data.
* @param {Object} resolveInfo - Contains the parsed GraphQL query, schema definition, and more. Obtained from the fourth argument to the resolver.
* @param {Object} context - An arbitrary object that gets passed to the `where` function. Useful for contextual infomation that influeces the `WHERE` condition, e.g. session, logged in user, localization.
* @param {dbCall} dbCall - A function that is passed the compiled SQL that calls the database and returns a promise of the data.
* @param {Object} [options]
* @param {Boolean} options.minify - Generate minimum-length column names in the results table.
* @param {String} options.aliasPrefix - String to prefix to column and table names, useful to avoid conflicts in subquery expressions.
* @param {String} options.dialect - The dialect of SQL your Database uses. Currently `'pg'`, `'oracle'`, `'mariadb'`, `'mysql'`, and `'sqlite3'` are supported.
* @param {Object} options.dialectModule - An alternative to options.dialect. You can provide a custom implementation of one of the supported dialects.
* @returns {Promise.<Object>} The correctly nested data from the database.
*/
async function joinMonster(resolveInfo, context, dbCall, options = {}) {
// we need to read the query AST and build a new "SQL AST" from which the SQL and
const sqlAST = queryAST.queryASTToSqlAST(resolveInfo, options, context)
const { sql, shapeDefinition } = await compileSqlAST(sqlAST, context, options)
if (!sql) return {}
// call their function for querying the DB, handle the different cases, do some validation, return a promise of the object
let data = await handleUserDbCall(dbCall, sql, sqlAST, shapeDefinition)
// if they are paginating, we'll get back an array which is essentially a "slice" of the whole data.
// this function goes through the data tree and converts the arrays to Connection Objects
data = arrToConnection(data, sqlAST)
// so far we handled the first "batch". up until now, additional batches were ignored
// this function recursively scanss the sqlAST and runs remaining batches
await nextBatch(sqlAST, data, dbCall, context, options)
// check for batch data
if (Array.isArray(data)) {
const childrenToCheck = sqlAST.children.filter((child) => child.sqlBatch)
return data.filter((d) => {
for (const child of childrenToCheck) {
if (d[child.fieldName] == null) {
return false
}
}
return true
})
}
return data
}
/**
* A helper for resolving the Node type in Relay.
* @param {String} typeName - The Name of the GraphQLObjectType
* @param {Object} resolveInfo - Contains the parsed GraphQL query, schema definition, and more. Obtained from the fourth argument to the resolver.
* @param {Object} context - An arbitrary object that gets passed to the `where` function. Useful for contextual infomation that influeces the WHERE condition, e.g. session, logged in user, localization.
* @param {where|Number|String|Array} condition - A value to determine the `where` function for searching the node. If it's a function, that function will be used as the `where` function. Otherwise, it is assumed to be the value(s) of the `primaryKey`. An array of values is needed for composite primary keys.
* @param {Function} dbCall - A function that is passed the compiled SQL that calls the database and returns (a promise of) the data.
* @param {Object} [options] - Same as `joinMonster` function's options.
* @returns {Promise.<Object>} The correctly nested data from the database. The GraphQL Type is added to the "\_\_type\_\_" property, which is helpful for the `resolveType` function in the `nodeDefinitions` of **graphql-relay-js**.
*/
async function getNode(
typeName,
resolveInfo,
context,
condition,
dbCall,
options = {},
) {
// get the GraphQL type from the schema using the name
const type = resolveInfo.schema._typeMap[typeName]
assert(type, `Type "${typeName}" not found in your schema.`)
assert(
getConfigFromSchemaObject(type).sqlTable,
`joinMonster can't fetch a ${typeName} as a Node unless it has "sqlTable" tagged.`,
)
// we need to determine what the WHERE function should be
let where = buildWhereFunction(type, condition, options)
// our getGraphQLType expects every requested field to be in the schema definition. "node" isn't a parent of whatever type we're getting, so we'll just wrap that type in an object that LOOKS that same as a hypothetical Node type
const fakeParentNode = {
_fields: {
node: {
type,
name: type.name.toLowerCase(),
args: [],
extensions: {
joinMonster: {
where,
},
},
},
},
}
const namespace = new AliasNamespace(options.minify, options.aliasPrefix)
const sqlAST = {}
const fieldNodes = resolveInfo.fieldNodes || resolveInfo.fieldASTs
// uses the same underlying function as the main `joinMonster`
queryAST.populateASTNode.call(
resolveInfo,
fieldNodes[0],
fakeParentNode,
sqlAST,
namespace,
0,
options,
context,
)
queryAST.pruneDuplicateSqlDeps(sqlAST, namespace)
const { sql, shapeDefinition } = await compileSqlAST(sqlAST, context, options)
const data = arrToConnection(
await handleUserDbCall(dbCall, sql, sqlAST, shapeDefinition),
sqlAST,
)
await nextBatch(sqlAST, data, dbCall, context, options)
if (!data) return data
data.__type__ = type.name
return data
}
joinMonster.getNode = getNode
// expose the package version for debugging
joinMonster.version = require('../package.json').version
export default joinMonster