├── .gitignore ├── README.md ├── index.js ├── package-lock.json └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lucidql 2 | 3 | `npm install @srtech/lucidql` 4 | 5 | ## About lucidql 6 | lucidql allows you to query an AdonisJS Lucid backed database from a client using structured JSON in a post request. 7 | The principle is very simple and similar to GraphQL which is where my inspiration for lucidql came from 8 | 9 | ## What is the benefit of lucidql 10 | 11 | **Data Structure** 12 | 13 | No need to setup and maintain your data structure, LUCID already knows all about this. 14 | Lucid also knows all about your table relationships, so getting related data is very simple. 15 | 16 | 17 | **Only get the data you need** 18 | 19 | lucidql allows you to select only the fields you want to see in both the main table and the related tables 20 | 21 | **Faimilar Knex syntact for filtering** 22 | 23 | If your already familier with KnexJS then wirting client queries for lucidql will be a breeze. 24 | 25 | **Easy to use with a simple POST request** 26 | 27 | Accessing your data from the client is as easy as making a POST request and passing some JSON to the server. 28 | 29 | ## Getting Started 30 | ### install lucidql 31 | `npm i @srtech/lucidql` 32 | 33 | ### setup your Adonis application and create models and a controller 34 | * Setup your models and relationships as you would normally. Nothing special here 35 | * Create a new controller, you can call it anything you want and set it up like in the example below, but referencing your own models 36 | 37 | You controller might look something like this. 38 | 39 | ``` 40 | //App/Controllers/Http/LucidQlController.js 41 | 42 | const lucidql = require('@srtech/lucidql') 43 | 44 | const PurchaseOrder = use('App/Models/PurchaseOrder') 45 | const PurchaseOrderLines = use('App/Models/PurchaseOrderLine') 46 | 47 | const classes = { 48 | __proto__: null, // to avoid people being able to pass something like `toString` 49 | PurchaseOrder, 50 | PurchaseOrderLines 51 | }; 52 | 53 | class LucidQlController { 54 | 55 | async query({ request }) { 56 | var { baseTable, query } = request.all() 57 | return lucidql.run(classes[baseTable], query) 58 | } 59 | } 60 | 61 | module.exports = LucidQlController 62 | ``` 63 | 64 | Note: Only Models that are imported and included in the classes object will be visible to lucidql as base tables for queries. 65 | 66 | setup a new route in `App/start/routes.js` 67 | 68 | `Route.post('/api/lucidql', 'LucidQlController.query')` 69 | 70 | 71 | ### Querying data 72 | 73 | Simply point your client i.e Axios or Postman etc at your route and send a structured JSON object to request your data. 74 | 75 | The structure of the JSON object you send needs to be in the following format 76 | 77 | ``` 78 | { 79 | "baseTable": "PurchaseOrder", 80 | "query": { 81 | "where": [ 82 | { 83 | "field": "PurchaseOrder", 84 | "value":"POC001460", 85 | "op":"==" 86 | } 87 | ], 88 | "fields": ["PurchaseOrder", "ShipFromAddress", "Currency"], 89 | "with": [ 90 | { 91 | "table": "lines", 92 | "fields": ["PurchaseOrder", "Position", "Item", "Price"], 93 | "where": [ 94 | { 95 | "field": "Position", 96 | "value": [5, 8], 97 | "op": "between" 98 | } 99 | ] 100 | } 101 | ] 102 | } 103 | } 104 | ``` 105 | 106 | The above query is the same as running the following LUCID query in your controller. 107 | 108 | ``` 109 | return await PurchaseOrder.query().where('PurchaseOrder', "POC001460") 110 | .with('lines', (builder)=>{ 111 | builder.whereBetween('Position', [5, 9]).select(["PurchaseOrder", "Position", "Item", "Price"]) 112 | }) 113 | .fetch() 114 | ``` 115 | 116 | ## baseTable 117 | 118 | Accepts a String 119 | This should match of the models you specified in the classes object you created in your controller 120 | 121 | ## query 122 | 123 | Accepts a JSON object 124 | Although the `query` property is required, it can be left empty. For example, this will return all records from the PurchaseOrder table. 125 | 126 | ``` 127 | { 128 | "baseTable": "PurchaseOrder", 129 | "query": {} 130 | } 131 | ``` 132 | 133 | The query property accepts these child properties 134 | 135 | * fields 136 | * where 137 | * order 138 | * paginate 139 | * aggregate 140 | * with 141 | * withCount 142 | * limit 143 | 144 | ### fields 145 | 146 | Accepts an Array of Strings 147 | 148 | This is the same as using .select(...) in a normal LUCID query. It is the fields that you want to include from your baseTable. 149 | 150 | **important** In order for relations to work using the `with` property (see below) you must include the fields that form the foreign key relationship, otherwise LUCID will not be able to make the relationship. 151 | 152 | ### where 153 | 154 | Accepts an Array of Objects 155 | 156 | The where property that is a direct child of `"query"` will filter the records from your base table, the above example will restrict the base table to returning one record where PurchaseOrder == 'POC001460' 157 | 158 | the structure of each condition is setup as follows 159 | 160 | ``` 161 | { 162 | "field": , 163 | "value": , 164 | "op": (many options that mostly map to Knex operators. See below for more details) 165 | } 166 | ``` 167 | You can add multiple conditions by simply adding more objects to the array. 168 | 169 | ### order 170 | 171 | Accepts an Array of Objects 172 | 173 | The order property allows you to order your query. It is an array of objects, each object containing the properties 174 | * field 175 | * direction 176 | 177 | If direction is omitted it defaults to `ASC` 178 | 179 | ``` 180 | "order": [ 181 | { 182 | "field": , 183 | "direction": <"ASC":"DESC"> 184 | } 185 | ] 186 | ``` 187 | 188 | ### paginate 189 | Accepts an object 190 | 191 | ``` 192 | "paginate": { 193 | "page": , 194 | "perPage": 195 | } 196 | ``` 197 | Using page will return a different format of results as it includes details about the pagination. The object that is returned looks like this 198 | 199 | ``` 200 | { 201 | total: '', 202 | perPage: '', 203 | lastPage: '', 204 | page: '', 205 | data: [{...}] 206 | } 207 | ``` 208 | 209 | Your data is included in the `data` property, but the other properties give you useful information that allow you to easily build your pagination logic. 210 | 211 | ### aggregate 212 | Accepts an Object 213 | 214 | ``` 215 | "aggregate": { 216 | "field": , 217 | "function": 218 | } 219 | ``` 220 | 221 | The function property should be any one of those supported by Lucid and documented here [https://adonisjs.com/docs/4.1/query-builder#_aggregate_helpers](https://adonisjs.com/docs/4.1/query-builder#_aggregate_helpers) 222 | 223 | Examples of aggregate functions you can use are 224 | 225 | * getCount 226 | * getCountDistinct 227 | * getMin 228 | * getMax 229 | * getSum 230 | * getSumDistinct 231 | * getAvg 232 | * getAvgDistinct 233 | 234 | ### with 235 | 236 | Accepts an array of Objects 237 | 238 | Just as if you were using LUCID directly in your controller, `with` allows you to join relations to your baseTable 239 | 240 | The structure of with is an array of objects, with each object describing filters and fields to include. 241 | 242 | ``` 243 | "with": [ 244 | { 245 | "table": "lines", 246 | "fields": ["PurchaseOrder", "Position", "Item", "Price"], 247 | "where": [ 248 | { 249 | "field": "Position", 250 | "value": [5, 8], 251 | "op": "between" 252 | } 253 | ] 254 | } 255 | ] 256 | ``` 257 | 258 | **table** 259 | Accepts a String 260 | this must match the name of the relation you have setup in your model. i.e. 261 | 262 | ``` 263 | lines() { 264 | return this.hasMany('App/Models/PurchaseOrderLine', 'PurchaseOrder', 'PurchaseOrder') 265 | } 266 | ``` 267 | 268 | in the case above you can see that `lines` is the name of our relation in the model and it is what was used to name the relation in the JSON object. 269 | 270 | You can add multiple relationships by simplying adding more objects to the array. 271 | 272 | **fields** - Optional 273 | Accepts an Array of Strings 274 | 275 | Works identically to the how the fields property is described above for the baseTable, but will limit the fields returned from your relationship, **Important** You must include in the list the field that forms part of the foreign key relationship 276 | 277 | If you ommit the fields property, all fields will be returned for this relationship 278 | 279 | **order** 280 | Just as with the baseQuery you can order the results of the related data. It is described in more detail above. 281 | 282 | **where** 283 | 284 | Accepts an Array of Objects 285 | 286 | Each object in this array is used to filter the results from related table. It works in exactly the same way as described above for the where property for the baseTable. 287 | 288 | 289 | ## where op 290 | 291 | As mentioned above a where property consists of an Array of Objects, each object describing a where condition, each of these objects must have an `op` property. Below is a list of the supported op's 292 | 293 | ### == 294 | 295 | ``` 296 | "where": [ 297 | { 298 | "field": "Position", 299 | "value": 8, 300 | "op": "==" 301 | } 302 | ] 303 | ``` 304 | Would return rows where `Position` is equal to 8 305 | 306 | ### <> / != 307 | 308 | ``` 309 | "where": [ 310 | { 311 | "field": "Position", 312 | "value": 8, 313 | "op": "!=" 314 | } 315 | ] 316 | ``` 317 | Would return rows where `Position` is not equal to 8 318 | 319 | 320 | ### < 321 | 322 | ``` 323 | "where": [ 324 | { 325 | "field": "Position", 326 | "value": 8, 327 | "op": "<" 328 | } 329 | ] 330 | ``` 331 | Would return rows where `Position` is less than 8 332 | 333 | ### > 334 | 335 | ``` 336 | "where": [ 337 | { 338 | "field": "Position", 339 | "value": 8, 340 | "op": ">" 341 | } 342 | ] 343 | ``` 344 | Would return rows where `Position` is greater than 8 345 | 346 | ### between 347 | 348 | ``` 349 | "where": [ 350 | { 351 | "field": "Position", 352 | "value": [4, 8], 353 | "op": "between" 354 | } 355 | ] 356 | ``` 357 | Would return rows where `Position` between 4 and 8 358 | 359 | ### notBetween 360 | 361 | ``` 362 | "where": [ 363 | { 364 | "field": "Position", 365 | "value": [4, 8], 366 | "op": "notBetween" 367 | } 368 | ] 369 | ``` 370 | Would return rows where `Position` is not between 4 and 8 371 | 372 | ### like 373 | ``` 374 | "like": [ 375 | { 376 | "field": "Company", 377 | "value": "%widgets", 378 | "op": "like" 379 | } 380 | ] 381 | ``` 382 | Would return any rows where the Company field ends in `widgets` 383 | 384 | ### in 385 | 386 | ``` 387 | "where": [ 388 | { 389 | "field": "Position", 390 | "value": [4, 5, 6, 7, 8], 391 | "op": "in" 392 | } 393 | ] 394 | ``` 395 | Would return rows where `Position` is one of 4 or 5 or 6 or 7 or 8 396 | 397 | ### notIn 398 | 399 | ``` 400 | "where": [ 401 | { 402 | "field": "Position", 403 | "value": [4, 5, 6, 7, 8], 404 | "op": "notIn" 405 | } 406 | ] 407 | ``` 408 | Would return rows where `Position` is NOT one of 4 or 5 or 6 or 7 or 8 409 | 410 | ### not 411 | 412 | Using `not` the `field` property can be omitted 413 | 414 | ``` 415 | "where": [ 416 | { 417 | "value": { 418 | "Position": 8, 419 | "Currency": "USD" 420 | }, 421 | "op": "not" 422 | } 423 | ] 424 | ``` 425 | Would return rows where `Position` is not 8 and `Currency` is not "USD" 426 | 427 | ### null 428 | Using `null` the `value` property can be omitted 429 | ``` 430 | "where": [ 431 | { 432 | "field": "Position", 433 | "op": "null" 434 | } 435 | ] 436 | ``` 437 | Would return rows where `Position` is null 438 | 439 | ### notNull 440 | Using `notNull` the `value` property can be omitted 441 | ``` 442 | "where": [ 443 | { 444 | "field": "Position", 445 | "op": "notNull" 446 | } 447 | ] 448 | ``` 449 | Would return rows where `Position` is not null 450 | 451 | ### withCount 452 | Accepts an Array of Objects 453 | Lucid's own documentation for this feature can be found here https://adonisjs.com/docs/4.0/relationships#_counts 454 | 455 | ```JSON 456 | "withCount": [ 457 | { 458 | "table": 459 | } 460 | ] 461 | ``` 462 | 463 | The data returned will include a `__meta__` property with a count of the records that are related to the baseTable 464 | 465 | 466 | 467 | ```JSON 468 | //The following 469 | "withCount": [ 470 | { 471 | "table": "tags" 472 | } 473 | ] 474 | 475 | //Would produce 476 | { 477 | ... 478 | "__meta__": { 479 | "tags_count": 1 480 | } 481 | } 482 | ``` 483 | ### limit 484 | Accepts an Object 485 | 486 | limit will `limit` the number of records that Lucid returns to you. 487 | 488 | The object should contain only a single property `qty` with a numeric value 489 | 490 | The code below would tell Lucid to limit the number of records returned from baseTable to 3. 491 | 492 | ```JSON 493 | limit: { qty: 3} 494 | ``` 495 | 496 | 497 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const Promise = require('bluebird'); 2 | const buildWhere = (object, where) => { 3 | if (where.op === '==') { 4 | return object.where(`${where.field}`, `${where.value}`); 5 | } 6 | if (where.op === '!=' || where.op === '<>' || where.op === '<' || where.op === '>' || where.op === 'like') { 7 | return object.where(`${where.field}`, `${where.op}`, `${where.value}`); 8 | } 9 | if (where.op === 'null') { 10 | return object.whereNull(`${where.field}`); 11 | } 12 | if (where.op === 'notNull') { 13 | return object.whereNotNull(`${where.field}`); 14 | } 15 | if (where.op === 'between') { 16 | return object.whereBetween(`${where.field}`, where.value); 17 | } 18 | if (where.op === 'notBetween') { 19 | return object.whereNotBetween(`${where.field}`, where.value); 20 | } 21 | if (where.op === 'in') { 22 | return object.whereIn(`${where.field}`, where.value); 23 | } 24 | if (where.op === 'notIn') { 25 | return object.whereNotIn(`${where.field}`, where.value); 26 | } 27 | if (where.op === 'not') { 28 | return object.whereNot(where.value); 29 | } 30 | }; 31 | 32 | const buildOrderBy = function(object, o) { 33 | return object.orderBy(`${o.field}`, `${o.hasOwnProperty('direction') ? o.direction : 'ASC'}`); 34 | }; 35 | 36 | exports.run = async function(model, query) { 37 | var baseQuery = model.query(); 38 | var queryOptions = query; 39 | if (queryOptions.hasOwnProperty('fields')) { 40 | for (var f of queryOptions.fields) { 41 | baseQuery.select(`${f}`); 42 | } 43 | } 44 | 45 | if (queryOptions.hasOwnProperty('order')) { 46 | for (var o of queryOptions.order) { 47 | buildOrderBy(baseQuery, o); 48 | } 49 | } 50 | 51 | if (queryOptions.hasOwnProperty('where')) { 52 | for (var w of queryOptions.where) { 53 | buildWhere(baseQuery, w); 54 | } 55 | } 56 | 57 | if (queryOptions.hasOwnProperty('with')) { 58 | await Promise.each(queryOptions.with, (w) => { 59 | return baseQuery 60 | .with(`${w.table}`, (builder) => { 61 | if (w.hasOwnProperty('fields')) { 62 | builder.select(w.fields); 63 | } 64 | 65 | if (w.hasOwnProperty('where')) { 66 | for (var v of w.where) { 67 | buildWhere(builder, v); 68 | } 69 | } 70 | 71 | if (w.hasOwnProperty('order')) { 72 | for (var o of w.order) { 73 | buildOrderBy(builder, o); 74 | } 75 | } 76 | }) 77 | .fetch(); 78 | }); 79 | } 80 | 81 | if (queryOptions.hasOwnProperty('withCount')) { 82 | for (var c of queryOptions.withCount) { 83 | baseQuery.withCount(`${c.table}`); 84 | } 85 | } 86 | 87 | if (queryOptions.hasOwnProperty('limit')) { 88 | baseQuery.limit(queryOptions.limit.qty); 89 | } 90 | 91 | if (queryOptions.hasOwnProperty('paginate')) { 92 | return await baseQuery.paginate(queryOptions.paginate.page, queryOptions.paginate.perPage || 20); 93 | } else if (queryOptions.hasOwnProperty('aggregate')) { 94 | return await baseQuery[`${queryOptions.aggregate.function}`](`${queryOptions.aggregate.field}`); 95 | } else { 96 | return await baseQuery.fetch(); 97 | } 98 | }; 99 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@srtech/lucidql", 3 | "version": "1.7.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "bluebird": { 8 | "version": "3.5.3", 9 | "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.3.tgz", 10 | "integrity": "sha512-/qKPUQlaW1OyR51WeCPBvRnAlnZFUJkCSG5HzGnuIqhgyJtF+T94lFnn33eiazjRm2LAHVy2guNnaq48X9SJuw==" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@srtech/lucidql", 3 | "version": "1.8.0", 4 | "description": "Query language to allow client apps to use structured JSON to get granular control over data returned from an API powered by Adonis and LUCID", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/SouthRibbleTech/lucidql.git" 12 | }, 13 | "keywords": [ "lucid", "adonis", "query", "api", "orm", "NodeJS", "knex", "MySQL", "PostgreSQL" ], 14 | "author": "Simon Carr", 15 | "license": "ISC", 16 | "bugs": { 17 | "url": "https://github.com/SouthRibbleTech/lucidql/issues" 18 | }, 19 | "homepage": "https://github.com/SouthRibbleTech/lucidql#readme", 20 | "dependencies": { 21 | "bluebird": "^3.5.3" 22 | } 23 | } 24 | --------------------------------------------------------------------------------