├── .gitignore ├── .npmignore ├── Gruntfile.js ├── Makefile ├── README.md ├── eslint.yaml ├── package.json ├── sample ├── package.json └── sample.js └── src ├── gts-1-version.js ├── gts-2-util-hook.js ├── gts-3-util-graphql.js ├── gts-4-util-sequelize-options.js ├── gts-5-util-sequelize-fields.js ├── gts-6-util-fts.js ├── gts-7-entity-query.js ├── gts-8-entity-create.js ├── gts-9-entity-clone.js ├── gts-A-entity-update.js ├── gts-B-entity-delete.js └── gts.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | sample/node_modules 3 | sample/sample.db 4 | lib 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | sample/node_modules 3 | sample/sample.db 4 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | /* 2 | ** GraphQL-Tools-Sequelize -- Integration of GraphQL-Tools and Sequelize ORM 3 | ** Copyright (c) 2016-2023 Dr. Ralf S. Engelschall 4 | ** 5 | ** Permission is hereby granted, free of charge, to any person obtaining 6 | ** a copy of this software and associated documentation files (the 7 | ** "Software"), to deal in the Software without restriction, including 8 | ** without limitation the rights to use, copy, modify, merge, publish, 9 | ** distribute, sublicense, and/or sell copies of the Software, and to 10 | ** permit persons to whom the Software is furnished to do so, subject to 11 | ** the following conditions: 12 | ** 13 | ** The above copyright notice and this permission notice shall be included 14 | ** in all copies or substantial portions of the Software. 15 | ** 16 | ** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | ** EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | ** MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | ** IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | ** CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | ** TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | ** SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | */ 24 | 25 | /* global module: true */ 26 | module.exports = function (grunt) { 27 | grunt.loadNpmTasks("grunt-eslint") 28 | grunt.loadNpmTasks("grunt-babel") 29 | grunt.loadNpmTasks("grunt-contrib-clean") 30 | grunt.initConfig({ 31 | eslint: { 32 | options: { 33 | overrideConfigFile: "eslint.yaml" 34 | }, 35 | "graphql-tools-sequelize": [ "src/**/*.js", "tst/**/*.js" ] 36 | }, 37 | babel: { 38 | "graphql-tools-sequelize": { 39 | files: [ 40 | { 41 | expand: true, 42 | cwd: "src/", 43 | src: [ "*.js" ], 44 | dest: "lib/" 45 | } 46 | ], 47 | options: { 48 | sourceMap: false, 49 | presets: [ 50 | [ "@babel/preset-env", { 51 | "targets": { 52 | "node": "8.0.0" 53 | } 54 | } ] 55 | ], 56 | plugins: [ 57 | [ "@babel/plugin-transform-runtime", { 58 | "corejs": 2, 59 | "helpers": true, 60 | "regenerator": false 61 | } ] 62 | ] 63 | } 64 | } 65 | }, 66 | clean: { 67 | clean: [ "lib" ], 68 | distclean: [ "node_modules" ] 69 | } 70 | }) 71 | grunt.registerTask("default", [ "eslint", "babel" ]) 72 | } 73 | 74 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ## 2 | ## GraphQL-Tools-Sequelize -- Integration of GraphQL-Tools and Sequelize ORM 3 | ## Copyright (c) 2016-2023 Dr. Ralf S. Engelschall 4 | ## 5 | ## Permission is hereby granted, free of charge, to any person obtaining 6 | ## a copy of this software and associated documentation files (the 7 | ## "Software"), to deal in the Software without restriction, including 8 | ## without limitation the rights to use, copy, modify, merge, publish, 9 | ## distribute, sublicense, and/or sell copies of the Software, and to 10 | ## permit persons to whom the Software is furnished to do so, subject to 11 | ## the following conditions: 12 | ## 13 | ## The above copyright notice and this permission notice shall be included 14 | ## in all copies or substantial portions of the Software. 15 | ## 16 | ## THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | ## EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | ## MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | ## IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | ## CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | ## TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | ## SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | ## 24 | 25 | NPM = npm 26 | GRUNT = ./node_modules/grunt-cli/bin/grunt 27 | 28 | all: build 29 | 30 | bootstrap: 31 | @if [ ! -x $(GRUNT) ]; then $(NPM) install; fi 32 | 33 | build: bootstrap 34 | @$(GRUNT) 35 | 36 | clean: bootstrap 37 | @$(GRUNT) clean:clean 38 | 39 | distclean: bootstrap 40 | @$(GRUNT) clean:clean clean:distclean 41 | 42 | test: 43 | @$(GRUNT) test 44 | 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | GraphQL-Tools-Sequelize 3 | ======================== 4 | 5 | Integration of [GraphQL-Tools](https://github.com/apollostack/graphql-tools) and 6 | [Sequelize](http://sequelizejs.com) Object-Relational-Mapper (ORM). 7 | 8 |

9 | 10 | 11 |

12 | 13 | 14 | About 15 | ----- 16 | 17 | This [Node.js](https://nodejs.org) module provides an integration of 18 | the [GraphQL.js](https://github.com/graphql/graphql-js) wrapper 19 | [GraphQL-Tools](https://github.com/apollostack/graphql-tools) and the 20 | [Sequelize](http://sequelizejs.com) Object-Relational-Mapper (ORM) in order to operate on 21 | the entities and their relationships of an underlying RDBMS through [GraphQL](http://graphql.org/). 22 | It provides functions for GraphQL schema definition entries and their corresponding resolver functions 23 | for querying and mutating entities and their relationships through GraphQL in a natural 24 | Object-Oriented (OO) way. It optionally provides Full-Text-Search (FTS) functionality 25 | through [ElasticLunr](http://elasticlunr.com/) and validation, authorization and tracing hooks. 26 | It provides an elaborate CRUD (Create, Read, Update, Delete) functionality for the entities and 27 | their relationships. 28 | 29 | Installation 30 | ------------ 31 | 32 | ```shell 33 | $ npm install \ 34 | graphql \ 35 | graphql-tools \ 36 | graphql-tools-types \ 37 | graphql-tools-sequelize \ 38 | sequelize \ 39 | --save-dev 40 | ``` 41 | 42 | Usage 43 | ----- 44 | 45 | Suppose we have a simple domain model, consisting of the two 46 | entities `OrgUnit` and `Person` and some relationships between them 47 | (in UML Class Diagram notation): 48 | 49 | ```txt 50 | parentUnit supervisor 51 | +------+ +------+ 52 | | | | | 53 | | V 0..1 0..1 V | 54 | | +-----------+ +-----------+ | 55 | +---| OrgUnit | belongsTo| Person |---+ 56 | |-----------|<----------|-----------| 57 | | id | | id | 58 | | initials |--------->*| initials | 59 | | name | members | name | 60 | +-----------+ +-----------+ 61 | | ^ 62 | | director | 63 | +-----------------+ 64 | ``` 65 | 66 | With Sequelize ORM this could be defined on the RDBMS level as: 67 | 68 | ```js 69 | import Sequelize from "sequelize" 70 | 71 | const db = new Sequelize(...) 72 | const dm = {} 73 | 74 | dm.OrgUnit = db.define("OrgUnit", { 75 | id: { type: Sequelize.UUID, primaryKey: true }, 76 | initials: { type: Sequelize.STRING(3), allowNull: false }, 77 | name: { type: Sequelize.STRING(100), allowNull: false } 78 | }) 79 | dm.Person = db.define("Person", { 80 | id: { type: Sequelize.UUID, primaryKey: true }, 81 | initials: { type: Sequelize.STRING(3), allowNull: false }, 82 | name: { type: Sequelize.STRING(100), allowNull: false } 83 | }) 84 | dm.OrgUnit.belongsTo(dm.OrgUnit, { as: "parentUnit", foreignKey: "parentUnitId" }) 85 | dm.Person .belongsTo(dm.Person, { as: "supervisor", foreignKey: "personId" }) 86 | dm.Person .belongsTo(dm.OrgUnit, { as: "belongsTo", foreignKey: "orgUnitId" }) 87 | dm.OrgUnit.hasMany (dm.Person, { as: "members", foreignKey: "orgUnitId" }) 88 | dm.OrgUnit.hasOne (dm.Person, { as: "director", foreignKey: "directorId" }) 89 | ``` 90 | 91 | You then establish a GraphQL-to-Sequelize mapping like this: 92 | 93 | ```js 94 | import GraphQLToolsSequelize from "graphql-tools-sequelize" 95 | 96 | const gts = new GraphQLToolsSequelize(db) 97 | await gts.boot() 98 | ``` 99 | 100 | Now you can use this mapping and its factory functions to conveniently 101 | create a GraphQL schema definition as the interface for operating on 102 | your domain model: 103 | 104 | ```js 105 | const definition = ` 106 | schema { 107 | query: Root 108 | mutation: Root 109 | } 110 | scalar UUID 111 | scalar JSON 112 | type Root { 113 | ${gts.entityQuerySchema("Root", "", "OrgUnit")} 114 | ${gts.entityQuerySchema("Root", "", "OrgUnit*")} 115 | ${gts.entityQuerySchema("Root", "", "Person")} 116 | ${gts.entityQuerySchema("Root", "", "Person*")} 117 | } 118 | type OrgUnit { 119 | ${gts.attrIdSchema("OrgUnit")} 120 | ${gts.attrHcSchema("OrgUnit")} 121 | initials: String 122 | name: String 123 | director: Person 124 | members: [Person]! 125 | parentUnit: OrgUnit 126 | ${gts.entityCloneSchema ("OrgUnit")} 127 | ${gts.entityCreateSchema("OrgUnit")} 128 | ${gts.entityUpdateSchema("OrgUnit")} 129 | ${gts.entityDeleteSchema("OrgUnit")} 130 | } 131 | type Person { 132 | ${gts.attrIdSchema("Person")} 133 | ${gts.attrHcSchema("Person")} 134 | initials: String 135 | name: String 136 | belongsTo: OrgUnit 137 | supervisor: Person 138 | ${gts.entityCloneSchema ("Person")} 139 | ${gts.entityCreateSchema("Person")} 140 | ${gts.entityUpdateSchema("Person")} 141 | ${gts.entityDeleteSchema("Person")} 142 | } 143 | ` 144 | ``` 145 | 146 | You also use it and its factory functions to define the corresponding 147 | GraphQL resolver functions: 148 | 149 | ```js 150 | import GraphQLToolsTypes from "graphql-tools-types" 151 | 152 | const resolvers = { 153 | UUID: GraphQLToolsTypes.UUID({ name: "UUID", storage: "string" }), 154 | JSON: GraphQLToolsTypes.JSON({ name: "JSON" }), 155 | Root: { 156 | OrgUnit: gts.entityQueryResolver ("Root", "", "OrgUnit"), 157 | OrgUnits: gts.entityQueryResolver ("Root", "", "OrgUnit*"), 158 | Person: gts.entityQueryResolver ("Root", "", "Person"), 159 | Persons: gts.entityQueryResolver ("Root", "", "Person*"), 160 | }, 161 | OrgUnit: { 162 | id: gts.attrIdResolver ("OrgUnit"), 163 | hc: gts.attrHcResolver ("OrgUnit"), 164 | director: gts.entityQueryResolver ("OrgUnit", "director", "Person"), 165 | members: gts.entityQueryResolver ("OrgUnit", "members", "Person*"), 166 | parentUnit: gts.entityQueryResolver ("OrgUnit", "parentUnit", "OrgUnit"), 167 | clone: gts.entityCloneResolver ("OrgUnit"), 168 | create: gts.entityCreateResolver("OrgUnit"), 169 | update: gts.entityUpdateResolver("OrgUnit"), 170 | delete: gts.entityDeleteResolver("OrgUnit") 171 | }, 172 | Person: { 173 | id: gts.attrIdResolver ("Person"), 174 | hc: gts.attrHcResolver ("Person"), 175 | belongsTo: gts.entityQueryResolver ("Person", "belongsTo", "OrgUnit"), 176 | supervisor: gts.entityQueryResolver ("Person", "supervisor", "Person"), 177 | clone: gts.entityCloneResolver ("Person"), 178 | create: gts.entityCreateResolver("Person"), 179 | update: gts.entityUpdateResolver("Person"), 180 | delete: gts.entityDeleteResolver("Person") 181 | } 182 | } 183 | ``` 184 | 185 | Then you use the established schema definition and resolver functions to 186 | generate an executable GraphQL schema with the help of GraphQL-Tools: 187 | 188 | ```js 189 | import * as GraphQLTools from "graphql-tools" 190 | 191 | const schema = GraphQLTools.makeExecutableSchema({ 192 | typeDefs: [ definition ], 193 | resolvers: resolvers 194 | }) 195 | ``` 196 | 197 | Finally, you now can execute GraphQL queries against your RDBMS: 198 | 199 | ```js 200 | const query = `query { OrgUnits { name } }` 201 | const variables = {} 202 | 203 | GraphQL.graphql(schema, query, null, null, variables).then((result) => { 204 | console.log("OK", util.inspect(result, { depth: null, colors: true })) 205 | }).catch((result) => { 206 | console.log("ERROR", result) 207 | }) 208 | ``` 209 | 210 | The following GraphQL mutation is a more elaborate example of how 211 | CRUD operations look like and what is possible: 212 | 213 | ```txt 214 | mutation { 215 | m1: Person { 216 | c1: create(id: "c9965340-a6c8-11e6-ac95-080027e303e4", with: { 217 | initials: "BB", 218 | name: "Big Boss" 219 | }) { id } 220 | c2: create(id: "ca1ace2c-a6c8-11e6-8ef0-080027e303e4", with: { 221 | initials: "JD", 222 | name: "John Doe", 223 | supervisor: "c9965340-a6c8-11e6-ac95-080027e303e4" 224 | }) { id } 225 | } 226 | m2: OrgUnit { 227 | c1: create(id: "ca8c588a-a6c8-11e6-8f19-080027e303e4", with: { 228 | initials: "EH", 229 | name: "Example Holding", 230 | director: "c9965340-a6c8-11e6-ac95-080027e303e4" 231 | }) { id } 232 | c2: create(id: "cabaa4ce-a6c8-11e6-9d6d-080027e303e4", with: { 233 | initials: "EC", 234 | name: "Example Corporation", 235 | parentUnit: "ca8c588a-a6c8-11e6-8f19-080027e303e4", 236 | director: "ca1ace2c-a6c8-11e6-8ef0-080027e303e4", 237 | members: { set: [ 238 | "c9965340-a6c8-11e6-ac95-080027e303e4", 239 | "ca1ace2c-a6c8-11e6-8ef0-080027e303e4" 240 | ] } 241 | }) { id } 242 | } 243 | q1: OrgUnits(where: { 244 | initials: "EC" 245 | }) { 246 | name 247 | director { initials name } 248 | members { initials name } 249 | parentUnit { 250 | name 251 | director { initials name } 252 | members { initials name } 253 | } 254 | } 255 | m3: Person(id: "c9965340-a6c8-11e6-ac95-080027e303e4") { 256 | update(with: { initials: "XXX" }) { 257 | id initials name 258 | } 259 | } 260 | c1: Person(id: "c9965340-a6c8-11e6-ac95-080027e303e4") { 261 | clone { 262 | id initials name 263 | } 264 | } 265 | m4: Person(id: "c9965340-a6c8-11e6-ac95-080027e303e4") { 266 | delete 267 | } 268 | q2: Persons { 269 | id initials name 270 | } 271 | } 272 | ``` 273 | 274 | For more details, see the [all-in-one sample](./sample/), which even 275 | provides a network interface through [HAPI](http://hapijs.com/) and the 276 | [GraphiQL](https://github.com/graphql/graphiql) web interface on top of it 277 | (with the help of its HAPI integration [HAPI-Plugin-GraphiQL](https://github.com/rse/hapi-plugin-graphiql)). 278 | 279 | Application Programming Interface (API) 280 | --------------------------------------- 281 | 282 | - `import GraphQLToolsSequelize from "graphql-tools-sequelize"`
283 | `gts = new GraphQLToolsSequelize(sequelize: Sequelize, options?: Object)`
284 | 285 | Creates a new GraphQL-Tools-Sequelize instance with an existing Sequelize instance `sequelize`. 286 | The `options` have to given, but can be an empty object. It can contain the following 287 | fields: 288 | 289 | - `validator(type: String, obj: Object, ctx: Object): Promise`:
290 | Optionally validate entity object `obj` (of entity type `type`) 291 | just before create or update operations. If the resulting 292 | Promise is rejected, the create or update operation fails. 293 | The `ctx` object is just passed through from the `GraphQL.graphql()` call. 294 | 295 | - `authorizer(moment: String, op: String, type: String, obj: Object, ctx: Object): Promise`:
296 | Optionally authorize entity object `obj` (of entity type `type`) 297 | for operation `op` (`create`, `read`, `update` or `delete`) at `moment` (`before` or `after`). 298 | Notice that for `read` there is no `before` and for `delete` there is no `after`, of course. The `ctx` object is just passed through from 299 | the `GraphQL.graphql()` call. If the resulting Promise is rejected 300 | or returns `false`, the operation fails. 301 | 302 | - `tracer(record: Object, ctx: Object): Promise`:
303 | Optionally trace the operation via the action `record`. The fields of `record` are: 304 | `{ op: String, arity: String, dstType: String, dstIds: String[], dstAttrs: String[] }`. 305 | The `ctx` object is just passed through from the `GraphQL.graphql()` call. 306 | 307 | - `fts: { [String]: String[] }`:
308 | Enables the Full-Text-Search (FTS) mechanism for all configured entity types 309 | and their listed attributes. 310 | 311 | - `idname: String = "id"`:
312 | Configures the GraphQL name of the mandatory unique identifier attribute on each entity. 313 | The default name is `id`. 314 | 315 | - `idtype: String = "UUID"`:
316 | Configures the GraphQL type of the mandatory unique identifier attribute on each entity. 317 | The default type `UUID` assumes that you define the GraphQL scalar type `UUID` with the help of 318 | [GraphQL-Tools-Types](https://github.com/rse/graphql-tools-types). 319 | 320 | - `idmake: Function = () => (new UUID(1)).format()`:
321 | Configures a function for generating unique identifiers for the mandatory unique identifier attribute on each entity. 322 | The default uses [pure-uuid](https://github.com/rse/pure-uuid) to generate UUIDs of version 1. 323 | 324 | - `hcname: String = "hc"`:
325 | Configures the GraphQL name of the optional hash-code attribute on each entity. 326 | The default name is `hc`. This attribute is NOT persisted and instead calculated on 327 | the fly and is intended to be used for optimistic locking purposes. 328 | 329 | - `hctype: String = "UUID"`:
330 | Configures the GraphQL type of the optional hash-code attribute on each entity. 331 | The default type `UUID` assumes that you define the GraphQL scalar type `UUID` with the help of 332 | [GraphQL-Tools-Types](https://github.com/rse/graphql-tools-types). 333 | 334 | - `hcmake: Function = (data) => (new UUID(5, "ns:URL", \`uri:gts:${data}\`)).format()`:
335 | Configures a function for generating hash-codes for the optional hash-code attribute on each entity. 336 | The default uses [pure-uuid](https://github.com/rse/pure-uuid) to generate UUIDs of version 5. 337 | 338 | - `gts.boot(): Promise`:
339 | 340 | Bootstrap the GraphQL-Tools-Sequelize instance. It internally 341 | mainly initialized the Full-Text-Search (FTS) mechanism. 342 | 343 | - `gts.attrIdSchema(source: String): String`,
344 | `gts.attrIdResolver(source: String): Function`:
345 | 346 | Generate a GraphQL schema entry and a corresponding GraphQL resolver 347 | function for querying the mandatory unique identifier attribute 348 | of an entity of type `source`. By default this generates a schema 349 | entry `: ` and a resolver which just returns 350 | `[]`. This mandatory unique identifier attribute has 351 | to be persisted and hence part of the Sequelize schema definition. 352 | 353 | - `gts.attrHcSchema(source: String): String`,
354 | `gts.attrHcResolver(source: String): Function`:
355 | 356 | Generate a GraphQL schema entry and a corresponding GraphQL resolver 357 | function for querying the optional hash-code attribute of an 358 | entity of type `source`. By default this generates a schema entry 359 | `: ` and a resolver which returns something like 360 | `(data())`, where `data()` is an internal function 361 | which deterministically concatenates the values of all attributes of 362 | ``. This optional hash-code attribute has NOT to be persisted 363 | and hence SHOULD NOT BE part of the Sequelize schema definition. 364 | 365 | - `gts.entityQuerySchema(source: String, relation: String, target: String): String`,
366 | `gts.entityQueryResolver(source: String, relation: String, target: String): Function`:
367 | 368 | Generate a GraphQL schema entry and a corresponding GraphQL resolver 369 | function for querying one, many or all entities of particular entity 370 | type `target` when coming from entity type `source` -- either 371 | directly (in case `relation` is the empty string) or via relationship 372 | `relation`. The `target` is either just the name `foo` of an entity 373 | type `foo` (for relationship of cardinality 0..1) or `foo*` (for 374 | relationship of cardinality 0..N). Based on the combination of 375 | `relation` and the cardinality of `target`, four distinct GraphQL schema 376 | entries (and corresponding GraphQL resolver functions) are generated: 377 | 378 | - empty `relation` and `target` cardinality 0..1:
379 | 380 | ```js 381 | `# Query one [${target}]() entity by its unique identifier (\`id\`) or condition (\`where\`) or` + 382 | `# open an anonymous context for the [${target}]() entity.\n` + 383 | `# The [${target}]() entity can be optionally filtered by a condition on some relationships (\`include\`).\n` + 384 | `${target}(id: ${idtype}, where: JSON, include: JSON): ${target}\n` 385 | ``` 386 | 387 | - empty `relation` and `target` cardinality 0..N:
388 | 389 | ```js 390 | `# Query one or many [${target}]() entities,\n` + 391 | "# by either an (optionally available) full-text-search (`query`)\n" + 392 | "# or an (always available) attribute-based condition (`where`),\n" + 393 | "# optionally filter them by a condition on some relationships (`include`),\n" + 394 | "# optionally sort them (`order`),\n" + 395 | "# optionally start the result set at the n-th entity (zero-based `offset`), and\n" + 396 | "# optionally reduce the result set to a maximum number of entities (`limit`).\n" + 397 | `${target}s(fts: String, where: JSON, include: JSON, order: JSON, offset: Int = 0, limit: Int = 100): [${target}]!\n` 398 | ``` 399 | 400 | - non-empty `relation` and `target` cardinality 0..1:
401 | 402 | ```js 403 | `# Query one [${target}]() entity by following the **${relation}** relation of [${source}]() entity.\n` + 404 | `# The [${target}]() entity can be optionally filtered by a condition (\`where\`).\n` + 405 | `# The [${target}]() entity can be optionally filtered by a condition on some relationships (\`include\`).\n` + 406 | `${relation}(where: JSON, include: JSON): ${target}\n` 407 | ``` 408 | 409 | - non-empty `relation` and `target` cardinality 0..N:
410 | 411 | ```js 412 | `# Query one or many [${target}]() entities\n` + 413 | `# by following the **${relation}** relation of [${source}]() entity,\n` + 414 | "# optionally filter them by a condition (`where`),\n" + 415 | "# optionally filter them by a condition on some relationships (`include`),\n" + 416 | "# optionally sort them (`order`),\n" + 417 | "# optionally start the result set at the n-th entity (zero-based `offset`), and\n" + 418 | "# optionally reduce the result set to a maximum number of entities (`limit`).\n" + 419 | `${relation}(where: JSON, include: JSON, order: JSON, offset: Int = 0, limit: Int = 100): [${target}]!\n` 420 | ``` 421 | 422 | The comments are intentionally also generated, as they document 423 | the entries in the GraphQL schema and are visible through 424 | GraphQL schema introspection tools like GraphiQL. 425 | 426 | - `gts.entity{Create,Clone,Update,Delete}Schema(type: String): String`,
427 | `gts.entity{Create,Clone,Update,Delete}Resolver(type: String): Function`:
428 | 429 | Generate a GraphQL schema entry and a corresponding GraphQL resolver 430 | function for mutating one, many or all entities of particular entity 431 | type `type`. The following GraphQL schema 432 | entries (and corresponding GraphQL resolver functions) are generated: 433 | 434 | - For `entityCreate{Schema,Resolver}(type)`:
435 | 436 | ```js 437 | `# Create new [${type}]() entity, optionally with specified attributes (\`with\`).\n` + 438 | `create(id: ${idtype}, with: JSON): ${type}!\n` 439 | ``` 440 | 441 | - For `entityClone{Schema,Resolver}(type)`:
442 | 443 | ```js 444 | `# Clone one [${type}]() entity by cloning its attributes (but not its relationships).\n` + 445 | `clone: ${type}!\n` 446 | ``` 447 | 448 | - For `entityUpdate{Schema,Resolver}(type)`:
449 | 450 | ```js 451 | `# Update one [${type}]() entity with specified attributes (\`with\`).\n` + 452 | `update(with: JSON!): ${type}!\n` 453 | ``` 454 | 455 | - For `entityDelete{Schema,Resolver}(type)`:
456 | 457 | ```js 458 | `# Delete one [${type}]() entity.\n` + 459 | `delete: ${idtype}!\n` 460 | ``` 461 | 462 | The comments are intentionally also generated, as they document 463 | the entries in the GraphQL schema and are visible through 464 | GraphQL schema introspection tools like GraphiQL. 465 | 466 | Assumptions 467 | ----------- 468 | 469 | It is assumed that all your Sequelize entities have an attribute 470 | `id` (see also `idname` configuration option) which is the 471 | (technical) primary key of an entity. By default, the type of 472 | field `id` is `UUID`, but this can be overridden (see `idtype` 473 | configuration option). In case of the type `UUID`, it is assumed 474 | that you define the GraphQL scalar type `UUID` with the help of 475 | [GraphQL-Tools-Types](https://github.com/rse/graphql-tools-types). 476 | 477 | Notice: all entities are required to have the field `id` and the type 478 | of all `id` fields have to be the same. But this does not prevent you 479 | from having *additional* domain-specific primary keys per entity of an 480 | arbitrary type, of course. GraphQL-Tools-Sequelize just uses the field 481 | `id` for its functionality. 482 | 483 | In addition, the scalar type `JSON` always has to be defined with the help of 484 | [GraphQL-Tools-Types](https://github.com/rse/graphql-tools-types). 485 | 486 | License 487 | ------- 488 | 489 | Copyright (c) 2016-2023 Dr. Ralf S. Engelschall (http://engelschall.com/) 490 | 491 | Permission is hereby granted, free of charge, to any person obtaining 492 | a copy of this software and associated documentation files (the 493 | "Software"), to deal in the Software without restriction, including 494 | without limitation the rights to use, copy, modify, merge, publish, 495 | distribute, sublicense, and/or sell copies of the Software, and to 496 | permit persons to whom the Software is furnished to do so, subject to 497 | the following conditions: 498 | 499 | The above copyright notice and this permission notice shall be included 500 | in all copies or substantial portions of the Software. 501 | 502 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 503 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 504 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 505 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 506 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 507 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 508 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 509 | 510 | -------------------------------------------------------------------------------- /eslint.yaml: -------------------------------------------------------------------------------- 1 | ## 2 | ## GraphQL-Tools-Sequelize -- Integration of GraphQL-Tools and Sequelize ORM 3 | ## Copyright (c) 2016-2023 Dr. Ralf S. Engelschall 4 | ## 5 | ## Permission is hereby granted, free of charge, to any person obtaining 6 | ## a copy of this software and associated documentation files (the 7 | ## "Software"), to deal in the Software without restriction, including 8 | ## without limitation the rights to use, copy, modify, merge, publish, 9 | ## distribute, sublicense, and/or sell copies of the Software, and to 10 | ## permit persons to whom the Software is furnished to do so, subject to 11 | ## the following conditions: 12 | ## 13 | ## The above copyright notice and this permission notice shall be included 14 | ## in all copies or substantial portions of the Software. 15 | ## 16 | ## THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | ## EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | ## MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | ## IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | ## CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | ## TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | ## SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | ## 24 | 25 | --- 26 | 27 | extends: 28 | - eslint:recommended 29 | - eslint-config-standard 30 | 31 | parserOptions: 32 | ecmaVersion: 8 33 | sourceType: module 34 | ecmaFeatures: 35 | jsx: false 36 | 37 | env: 38 | browser: true 39 | node: false 40 | commonjs: true 41 | worker: true 42 | serviceworker: true 43 | 44 | globals: 45 | process: true 46 | 47 | rules: 48 | # modified rules 49 | indent: [ "error", 4, { "SwitchCase": 1 } ] 50 | linebreak-style: [ "error", "unix" ] 51 | semi: [ "error", "never" ] 52 | operator-linebreak: [ "error", "after", { "overrides": { "&&": "before", "||": "before", ":": "before" } } ] 53 | brace-style: [ "error", "stroustrup", { "allowSingleLine": true } ] 54 | quotes: [ "error", "double" ] 55 | 56 | # disabled rules 57 | no-multi-spaces: off 58 | no-multiple-empty-lines: off 59 | key-spacing: off 60 | object-property-newline: off 61 | curly: off 62 | space-in-parens: off 63 | array-bracket-spacing: off 64 | lines-between-class-members: off 65 | computed-property-spacing: off 66 | multiline-ternary: off 67 | 68 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphql-tools-sequelize", 3 | "version": "2.3.0", 4 | "description": "Integration of GraphQL-Tools and Sequelize ORM", 5 | "keywords": [ "graphql", "graphql-tools", "sequelize", "schema", "resolver" ], 6 | "main": "lib/gts.js", 7 | "license": "MIT", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/rse/graphql-tools-sequelize.git" 11 | }, 12 | "author": { 13 | "name": "Dr. Ralf S. Engelschall", 14 | "email": "rse@engelschall.com", 15 | "url": "http://engelschall.com" 16 | }, 17 | "homepage": "https://github.com/rse/graphql-tools-sequelize", 18 | "bugs": "https://github.com/rse/graphql-tools-sequelize/issues", 19 | "peerDependencies": { 20 | "graphql": ">=0.13.0", 21 | "graphql-tools": ">=3.0.0", 22 | "graphql-tools-types": ">=1.1.0", 23 | "sequelize": ">=4.0.0" 24 | }, 25 | "dependencies": { 26 | "bluebird": "3.7.2", 27 | "ducky": "2.7.3", 28 | "elasticlunr": "0.9.5", 29 | "pure-uuid": "1.6.2", 30 | "aggregation": "1.2.7", 31 | "@babel/runtime-corejs2": "7.20.7" 32 | }, 33 | "devDependencies": { 34 | "grunt": "1.5.3", 35 | "grunt-cli": "1.4.3", 36 | "grunt-contrib-clean": "2.0.1", 37 | "grunt-contrib-watch": "1.1.0", 38 | "grunt-babel": "8.0.0", 39 | "grunt-eslint": "24.0.1", 40 | "babel-eslint": "10.1.0", 41 | "eslint": "8.32.0", 42 | "eslint-config-standard": "17.0.0", 43 | "eslint-plugin-promise": "6.1.1", 44 | "eslint-plugin-import": "2.27.5", 45 | "eslint-plugin-node": "11.1.0", 46 | "@babel/core": "7.20.12", 47 | "@babel/preset-env": "7.20.2", 48 | "@babel/plugin-transform-runtime": "7.19.6" 49 | }, 50 | "engines": { 51 | "node": ">=8.0.0" 52 | }, 53 | "scripts": { 54 | "prepublishOnly": "grunt default", 55 | "build": "grunt default", 56 | "test": "cd sample && npm start" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /sample/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sample", 3 | "version": "0.0.0", 4 | "description": "GraphQL-Tools-Sequelize Sample", 5 | "devDependencies": { 6 | "@babel/node": "7.20.7", 7 | "@babel/preset-env": "7.20.2" 8 | }, 9 | "dependencies": { 10 | "pure-uuid": "1.6.2", 11 | "hapi": "18.1.0", 12 | "hapi-plugin-graphiql": "2.3.0", 13 | "boom": "7.3.0", 14 | "graphql": "16.6.0", 15 | "@graphql-tools/schema": "9.0.13", 16 | "graphql-tools-types": "1.3.1", 17 | "graphql-tools-sequelize": "..", 18 | "sequelize": "6.28.0", 19 | "sqlite3": "5.1.4" 20 | }, 21 | "scripts": { 22 | "start": "babel-node --presets @babel/preset-env --only sample.js sample.js" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /sample/sample.js: -------------------------------------------------------------------------------- 1 | 2 | import UUID from "pure-uuid" 3 | import * as GraphQL from "graphql" 4 | import * as GraphQLTools from "@graphql-tools/schema" 5 | import GraphQLToolsSequelize from "graphql-tools-sequelize" 6 | import GraphQLToolsTypes from "graphql-tools-types" 7 | import HAPI from "hapi" 8 | import HAPIGraphiQL from "hapi-plugin-graphiql" 9 | import Boom from "boom" 10 | import Sequelize from "sequelize" 11 | 12 | ;(async function () { 13 | /* establish database connection */ 14 | let db = new Sequelize("./sample.db", "", "", { 15 | dialect: "sqlite", host: "", port: "", storage: "./sample.db", 16 | define: { freezeTableName: true, timestamps: false }, 17 | logging: (msg) => { console.log("Sequelize: " + msg) }, 18 | }) 19 | await db.authenticate() 20 | 21 | /* define database schema */ 22 | let dm = {} 23 | dm.OrgUnit = db.define("OrgUnit", { 24 | id: { type: Sequelize.UUID, primaryKey: true }, 25 | initials: { type: Sequelize.STRING(3), allowNull: false }, 26 | name: { type: Sequelize.STRING(100), allowNull: false } 27 | }) 28 | dm.Person = db.define("Person", { 29 | id: { type: Sequelize.UUID, primaryKey: true }, 30 | initials: { type: Sequelize.STRING(3), allowNull: false }, 31 | name: { type: Sequelize.STRING(100), allowNull: false }, 32 | role: { type: Sequelize.STRING(30), allowNull: true } 33 | }) 34 | dm.OrgUnit.belongsTo(dm.OrgUnit, { 35 | as: "parentUnit", 36 | foreignKey: "parentUnitId" 37 | }) 38 | dm.Person.belongsTo(dm.Person, { 39 | as: "supervisor", 40 | foreignKey: "personId" 41 | }) 42 | dm.Person.belongsTo(dm.OrgUnit, { 43 | as: "belongsTo", 44 | foreignKey: "orgUnitId" 45 | }) 46 | dm.OrgUnit.hasMany(dm.Person, { 47 | as: "members", 48 | foreignKey: "orgUnitId" 49 | }) 50 | dm.OrgUnit.hasOne(dm.Person, { 51 | as: "director", 52 | foreignKey: "directorId" 53 | }) 54 | 55 | /* on-the-fly (re-)create database schema */ 56 | await db.sync({ force: true }) 57 | 58 | /* fill database initially */ 59 | const uuid = () => (new UUID(1)).format() 60 | const uMSG = await dm.OrgUnit.create({ id: uuid(), initials: "msg", name: "msg systems ag" }) 61 | const uXT = await dm.OrgUnit.create({ id: uuid(), initials: "XT", name: "msg Applied Technology Research (XT)" }) 62 | const uXIS = await dm.OrgUnit.create({ id: uuid(), initials: "XIS", name: "msg Information Security (XIS)" }) 63 | const pHZ = await dm.Person.create ({ id: uuid(), initials: "HZ", name: "Hans Zehetmaier" }) 64 | const pJS = await dm.Person.create ({ id: uuid(), initials: "JS", name: "Jens Stäcker" }) 65 | const pRSE = await dm.Person.create ({ id: uuid(), initials: "RSE", name: "Ralf S. Engelschall" }) 66 | const pBEN = await dm.Person.create ({ id: uuid(), initials: "BEN", name: "Bernd Endras" }) 67 | const pCGU = await dm.Person.create ({ id: uuid(), initials: "CGU", name: "Carol Gutzeit" }) 68 | const pMWS = await dm.Person.create ({ id: uuid(), initials: "MWS", name: "Mark-W. Schmidt" }) 69 | const pBWE = await dm.Person.create ({ id: uuid(), initials: "BWE", name: "Bernhard Weber" }) 70 | const pFST = await dm.Person.create ({ id: uuid(), initials: "FST", name: "Florian Stahl", role: "employee" }) 71 | await uMSG.setDirector(pHZ) 72 | await uMSG.setMembers([ pHZ, pJS ]) 73 | await uXT.setDirector(pRSE) 74 | await uXT.setMembers([ pRSE, pBEN, pCGU ]) 75 | await uXT.setParentUnit(uMSG) 76 | await uXIS.setDirector(pMWS) 77 | await uXIS.setMembers([ pMWS, pBWE, pFST ]) 78 | await uXIS.setParentUnit(uMSG) 79 | await pJS.setSupervisor(pHZ) 80 | await pRSE.setSupervisor(pJS) 81 | await pBEN.setSupervisor(pRSE) 82 | await pCGU.setSupervisor(pRSE) 83 | await pMWS.setSupervisor(pJS) 84 | await pBWE.setSupervisor(pMWS) 85 | await pFST.setSupervisor(pMWS) 86 | 87 | /* establish GraphQL to Sequelize mapping */ 88 | const validator = async (/* type, obj */) => { 89 | return true 90 | } 91 | const authorizer = async (/* moment, op, type, obj, ctx */) => { 92 | return true 93 | } 94 | const gts = new GraphQLToolsSequelize(db, { 95 | validator: validator, 96 | authorizer: authorizer, 97 | tracer: async (record /*, ctx */) => { 98 | console.log(`trace: record=${JSON.stringify(record)}`) 99 | }, 100 | fts: { 101 | "OrgUnit": [ "name" ], 102 | "Person": [ "name" ] 103 | } 104 | }) 105 | await gts.boot() 106 | 107 | /* the GraphQL schema definition */ 108 | let definition = ` 109 | schema { 110 | query: Root 111 | mutation: Root 112 | } 113 | scalar UUID 114 | scalar JSON 115 | type Root { 116 | ${gts.entityQuerySchema("Root", "", "OrgUnit")} 117 | ${gts.entityQuerySchema("Root", "", "OrgUnit*")} 118 | ${gts.entityQuerySchema("Root", "", "Person")} 119 | ${gts.entityQuerySchema("Root", "", "Person*")} 120 | } 121 | type OrgUnit { 122 | ${gts.attrIdSchema("OrgUnit")} 123 | ${gts.attrHcSchema("OrgUnit")} 124 | initials: String 125 | name: String 126 | director: Person 127 | members: [Person]! 128 | parentUnit: OrgUnit 129 | ${gts.entityCloneSchema ("OrgUnit")} 130 | ${gts.entityCreateSchema("OrgUnit")} 131 | ${gts.entityUpdateSchema("OrgUnit")} 132 | ${gts.entityDeleteSchema("OrgUnit")} 133 | } 134 | type Person { 135 | ${gts.attrIdSchema("Person")} 136 | ${gts.attrHcSchema("Person")} 137 | initials: String 138 | name: String 139 | role: Role 140 | belongsTo: OrgUnit 141 | supervisor: Person 142 | ${gts.entityCloneSchema ("Person")} 143 | ${gts.entityCreateSchema("Person")} 144 | ${gts.entityUpdateSchema("Person")} 145 | ${gts.entityDeleteSchema("Person")} 146 | } 147 | enum Role { 148 | principal 149 | employee 150 | assistant 151 | } 152 | ` 153 | 154 | /* the GraphQL schema resolvers */ 155 | let resolvers = { 156 | UUID: GraphQLToolsTypes.UUID({ name: "UUID", storage: "string" }), 157 | JSON: GraphQLToolsTypes.JSON({ name: "JSON" }), 158 | Root: { 159 | OrgUnit: gts.entityQueryResolver ("Root", "", "OrgUnit"), 160 | OrgUnits: gts.entityQueryResolver ("Root", "", "OrgUnit*"), 161 | Person: gts.entityQueryResolver ("Root", "", "Person"), 162 | Persons: gts.entityQueryResolver ("Root", "", "Person*"), 163 | }, 164 | OrgUnit: { 165 | id: gts.attrIdResolver ("OrgUnit"), 166 | hc: gts.attrHcResolver ("OrgUnit"), 167 | director: gts.entityQueryResolver ("OrgUnit", "director", "Person"), 168 | members: gts.entityQueryResolver ("OrgUnit", "members", "Person*"), 169 | parentUnit: gts.entityQueryResolver ("OrgUnit", "parentUnit", "OrgUnit"), 170 | clone: gts.entityCloneResolver ("OrgUnit"), 171 | create: gts.entityCreateResolver("OrgUnit"), 172 | update: gts.entityUpdateResolver("OrgUnit"), 173 | delete: gts.entityDeleteResolver("OrgUnit") 174 | }, 175 | Person: { 176 | id: gts.attrIdResolver ("Person"), 177 | hc: gts.attrHcResolver ("Person"), 178 | role: ({ role }) => role, 179 | belongsTo: gts.entityQueryResolver ("Person", "belongsTo", "OrgUnit"), 180 | supervisor: gts.entityQueryResolver ("Person", "supervisor", "Person"), 181 | clone: gts.entityCloneResolver ("Person"), 182 | create: gts.entityCreateResolver("Person"), 183 | update: gts.entityUpdateResolver("Person"), 184 | delete: gts.entityDeleteResolver("Person") 185 | } 186 | } 187 | 188 | /* generate executable GraphQL schema */ 189 | let schema = GraphQLTools.makeExecutableSchema({ 190 | typeDefs: [ definition ], 191 | resolvers: resolvers, 192 | allowUndefinedInResolve: false, 193 | resolverValidationOptions: { 194 | requireResolversForArgs: true, 195 | requireResolversForNonScalar: true, 196 | requireResolversForAllFields: false 197 | } 198 | }) 199 | 200 | /* GraphQL query */ 201 | let query = ` 202 | mutation AddCoCWT { 203 | m1: Person { 204 | create( 205 | id: "acf34c80-9f83-11e6-8d46-080027e303e4", 206 | with: { 207 | initials: "JHO", 208 | name: "Jochen Hörtreiter", 209 | supervisor: "${pRSE.id}" 210 | } 211 | ) { 212 | id initials name 213 | } 214 | } 215 | m2: OrgUnit { 216 | create( 217 | id: "acf34c80-9f83-11e6-8d47-080027e303e4", 218 | with: { 219 | initials: "CoC-WT", 220 | name: "CoC Web Technologies", 221 | parentUnit: "${uXT.id}", 222 | director: "acf34c80-9f83-11e6-8d46-080027e303e4" 223 | } 224 | ) { 225 | id initials name 226 | } 227 | } 228 | q1: OrgUnits(where: { 229 | initials: "CoC-WT" 230 | }) { 231 | id 232 | name 233 | director { id name } 234 | parentUnit { id name } 235 | members { id name } 236 | } 237 | u1: Person(id: "acf34c80-9f83-11e6-8d46-080027e303e4") { 238 | update(with: { initials: "XXX", role: "assistant" }) { 239 | id initials name role 240 | } 241 | } 242 | c1: Person(id: "acf34c80-9f83-11e6-8d46-080027e303e4") { 243 | clone { 244 | id initials name 245 | } 246 | } 247 | d1: Person(id: "acf34c80-9f83-11e6-8d46-080027e303e4") { 248 | delete 249 | } 250 | } 251 | ` 252 | 253 | /* setup network service */ 254 | let server = new HAPI.Server({ 255 | address: "0.0.0.0", 256 | port: 12345 257 | }) 258 | 259 | /* establish the HAPI route for GraphiQL UI */ 260 | await server.register({ 261 | plugin: HAPIGraphiQL, 262 | options: { 263 | graphiqlURL: "/api", 264 | graphqlFetchURL: "/api", 265 | graphqlFetchOpts: `{ 266 | method: "POST", 267 | headers: { 268 | "Content-Type": "application/json", 269 | "Accept": "application/json" 270 | }, 271 | body: JSON.stringify(params), 272 | credentials: "same-origin" 273 | }`, 274 | graphqlExample: query.replace(/^\n/, "").replace(/^ /mg, "") 275 | } 276 | }) 277 | 278 | /* establish the HAPI route for GraphQL API */ 279 | server.route({ 280 | method: "POST", 281 | path: "/api", 282 | config: { 283 | payload: { output: "data", parse: true, allow: "application/json" } 284 | }, 285 | handler: async (request, h) => { 286 | /* determine request */ 287 | if (typeof request.payload !== "object" || request.payload === null) 288 | return Boom.badRequest("invalid request") 289 | let query = request.payload.query 290 | let variables = request.payload.variables 291 | let operation = request.payload.operationName 292 | 293 | /* support special case of GraphiQL */ 294 | if (typeof variables === "string") 295 | variables = JSON.parse(variables) 296 | if (typeof operation === "object" && operation !== null) 297 | return Boom.badRequest("invalid request") 298 | 299 | /* wrap GraphQL operation into a database transaction */ 300 | return db.transaction({ 301 | autocommit: false, 302 | deferrable: true, 303 | type: Sequelize.Transaction.TYPES.DEFERRED, 304 | isolationLevel: Sequelize.Transaction.ISOLATION_LEVELS.SERIALIZABLE 305 | }, (tx) => { 306 | /* create context for GraphQL resolver functions */ 307 | let ctx = { tx } 308 | 309 | /* execute the GraphQL query against the GraphQL schema */ 310 | return GraphQL.graphql(schema, query, null, ctx, variables, operation) 311 | }).then((result) => { 312 | /* success/commit */ 313 | return h.response(result).code(200) 314 | }).catch((result) => { 315 | /* error/rollback */ 316 | if (typeof result === "object" && result instanceof Error) 317 | result = `${result.name}: ${result.message}` 318 | else if (typeof result !== "string") 319 | result = result.toString() 320 | result = { errors: [ { message: result } ] } 321 | return h.response(result).code(200) 322 | }) 323 | } 324 | }) 325 | 326 | /* start server */ 327 | await server.start() 328 | console.log(`GraphiQL UI: [GET] http://${server.info.host}:${server.info.port}/api`) 329 | console.log(`GraphQL API: [POST] http://${server.info.host}:${server.info.port}/api`) 330 | })().catch((ex) => { 331 | console.log(`ERROR: ${ex}`) 332 | }) 333 | 334 | -------------------------------------------------------------------------------- /src/gts-1-version.js: -------------------------------------------------------------------------------- 1 | /* 2 | ** GraphQL-Tools-Sequelize -- Integration of GraphQL-Tools and Sequelize ORM 3 | ** Copyright (c) 2016-2023 Dr. Ralf S. Engelschall 4 | ** 5 | ** Permission is hereby granted, free of charge, to any person obtaining 6 | ** a copy of this software and associated documentation files (the 7 | ** "Software"), to deal in the Software without restriction, including 8 | ** without limitation the rights to use, copy, modify, merge, publish, 9 | ** distribute, sublicense, and/or sell copies of the Software, and to 10 | ** permit persons to whom the Software is furnished to do so, subject to 11 | ** the following conditions: 12 | ** 13 | ** The above copyright notice and this permission notice shall be included 14 | ** in all copies or substantial portions of the Software. 15 | ** 16 | ** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | ** EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | ** MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | ** IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | ** CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | ** TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | ** SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | */ 24 | 25 | /* the mixin class */ 26 | export default class gtsVersion { 27 | version () { 28 | return { major: 2, minor: 1, micro: 1, date: 20190202 } 29 | } 30 | } 31 | 32 | -------------------------------------------------------------------------------- /src/gts-2-util-hook.js: -------------------------------------------------------------------------------- 1 | /* 2 | ** GraphQL-Tools-Sequelize -- Integration of GraphQL-Tools and Sequelize ORM 3 | ** Copyright (c) 2016-2023 Dr. Ralf S. Engelschall 4 | ** 5 | ** Permission is hereby granted, free of charge, to any person obtaining 6 | ** a copy of this software and associated documentation files (the 7 | ** "Software"), to deal in the Software without restriction, including 8 | ** without limitation the rights to use, copy, modify, merge, publish, 9 | ** distribute, sublicense, and/or sell copies of the Software, and to 10 | ** permit persons to whom the Software is furnished to do so, subject to 11 | ** the following conditions: 12 | ** 13 | ** The above copyright notice and this permission notice shall be included 14 | ** in all copies or substantial portions of the Software. 15 | ** 16 | ** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | ** EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | ** MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | ** IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | ** CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | ** TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | ** SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | */ 24 | 25 | /* the mixin class */ 26 | export default class gtsUtilHook { 27 | /* mixin initialization */ 28 | initializer (sequelize, options) { 29 | this._validator = (typeof options.validator === "function" ? options.validator : null) 30 | this._authorizer = (typeof options.authorizer === "function" ? options.authorizer : null) 31 | this._tracer = (typeof options.tracer === "function" ? options.tracer : null) 32 | } 33 | 34 | /* optionally check authorization */ 35 | _authorized (moment, op, type, obj, ctx) { 36 | if (this._authorizer === null) 37 | return Promise.resolve(true) 38 | let result 39 | try { 40 | result = this._authorizer.call(null, moment, op, type, obj, ctx) 41 | } 42 | catch (ex) { 43 | result = Promise.resolve(false) 44 | } 45 | if (!(typeof result === "object" && typeof result.then === "function")) 46 | result = Promise.resolve(result) 47 | return result 48 | } 49 | 50 | /* optionally provide tracing information */ 51 | _trace (record, ctx) { 52 | if (this._tracer === null) 53 | return Promise.resolve(true) 54 | let result 55 | try { 56 | result = this._tracer.call(null, record, ctx) 57 | } 58 | catch (ex) { 59 | result = Promise.resolve(false) 60 | } 61 | if (!(typeof result === "object" && typeof result.then === "function")) 62 | result = Promise.resolve(result) 63 | return result 64 | } 65 | 66 | /* optionally validate attributes of entity */ 67 | _validate (type, obj, ctx) { 68 | if (this._validator === null) 69 | return Promise.resolve(true) 70 | let result = this._validator.call(null, type, obj, ctx) 71 | if (!(typeof result === "object" && typeof result.then === "function")) 72 | result = Promise.resolve(result) 73 | return result 74 | } 75 | } 76 | 77 | -------------------------------------------------------------------------------- /src/gts-3-util-graphql.js: -------------------------------------------------------------------------------- 1 | /* 2 | ** GraphQL-Tools-Sequelize -- Integration of GraphQL-Tools and Sequelize ORM 3 | ** Copyright (c) 2016-2023 Dr. Ralf S. Engelschall 4 | ** 5 | ** Permission is hereby granted, free of charge, to any person obtaining 6 | ** a copy of this software and associated documentation files (the 7 | ** "Software"), to deal in the Software without restriction, including 8 | ** without limitation the rights to use, copy, modify, merge, publish, 9 | ** distribute, sublicense, and/or sell copies of the Software, and to 10 | ** permit persons to whom the Software is furnished to do so, subject to 11 | ** the following conditions: 12 | ** 13 | ** The above copyright notice and this permission notice shall be included 14 | ** in all copies or substantial portions of the Software. 15 | ** 16 | ** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | ** EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | ** MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | ** IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | ** CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | ** TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | ** SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | */ 24 | 25 | /* external dependencies */ 26 | import Ducky from "ducky" 27 | 28 | /* the mixin class */ 29 | export default class gtsUtilGraphQL { 30 | /* return requested fields of GraphQL query */ 31 | _graphqlRequestedFields (info, obj) { 32 | const flattenAST = (ast, obj) => { 33 | let selections = [] 34 | if ( ast 35 | && ast.selectionSet 36 | && ast.selectionSet.selections 37 | && ast.selectionSet.selections.length > 0) 38 | selections = ast.selectionSet.selections 39 | return selections.reduce((flattened, ast) => { 40 | if (ast.kind === "InlineFragment") 41 | flattened = flattenAST(ast, flattened) 42 | else if (ast.kind === "FragmentSpread") 43 | flattened = flattenAST(info.fragments[ast.name.value], flattened) 44 | else { 45 | const name = ast.name.value 46 | if (flattened[name]) 47 | Object.assign(flattened[name], flattenAST(ast, flattened[name])) 48 | else 49 | flattened[name] = flattenAST(ast, {}) 50 | } 51 | return flattened 52 | }, obj) 53 | } 54 | return info.fieldNodes.reduce((obj, ast) => flattenAST(ast, obj), obj || {}) 55 | } 56 | 57 | /* determine fields (and their type) of a GraphQL object type */ 58 | _fieldsOfGraphQLType (info, entity) { 59 | const fields = { attribute: {}, relation: {}, method: {} } 60 | const fieldsAll = info.schema._typeMap[entity]._fields 61 | Object.keys(fieldsAll).forEach((field) => { 62 | let type = fieldsAll[field].type 63 | while (typeof type.ofType === "object") 64 | type = type.ofType 65 | if (field.match(/^(?:clone|create|update|delete)$/)) 66 | fields.method[field] = type.name 67 | else if ( type.constructor.name === "GraphQLScalarType" 68 | || type.constructor.name === "GraphQLEnumType" ) 69 | fields.attribute[field] = type.name 70 | else if ( type.constructor.name === "GraphQLObjectType" 71 | && typeof fieldsAll[field].resolve === "function") 72 | fields.relation[field] = type.name 73 | else 74 | throw new Error(`unknown type "${type.constructor.name}" for field "${field}"`) 75 | }) 76 | return fields 77 | } 78 | 79 | /* determine fields (and their type) of a GraphQL request */ 80 | _fieldsOfGraphQLRequest (args, info, entity) { 81 | const defined = this._fieldsOfGraphQLType(info, entity) 82 | const fields = { attribute: {}, relation: {} } 83 | if (typeof args.with === "object") { 84 | Object.keys(args.with).forEach((name) => { 85 | if (defined.relation[name]) { 86 | let value = args.with[name] 87 | if (typeof value === "string") 88 | value = { set: value } 89 | if (typeof value !== "object") 90 | throw new Error(`invalid value for relation "${name}" on type "${entity}"`) 91 | if (value === null) 92 | value = { set: [] } 93 | else { 94 | if (value.set === null) value.set = [] 95 | if (value.add === null) value.add = [] 96 | if (value.del === null) value.del = [] 97 | if (typeof value.set === "string") value.set = [ value.set ] 98 | if (typeof value.add === "string") value.add = [ value.add ] 99 | if (typeof value.del === "string") value.del = [ value.del ] 100 | if (!Ducky.validate(value, "{ set?: [ string* ], add?: [ string+ ], del?: [ string+ ] }")) 101 | throw new Error(`invalid value for relation "${name}" on type "${entity}"`) 102 | } 103 | fields.relation[name] = value 104 | } 105 | else if (defined.attribute[name]) { 106 | let value = args.with[name] 107 | let type = info.schema._typeMap[entity]._fields[name].type 108 | while (typeof type.ofType === "object") 109 | type = type.ofType 110 | if ( type.constructor.name === "GraphQLScalarType" 111 | && typeof type.parseValue === "function" 112 | && value !== null) 113 | value = type.parseValue(value) 114 | else if ( type.constructor.name === "GraphQLEnumType" 115 | && value !== null) { 116 | if (typeof value !== "string") 117 | throw new Error("invalid value type (expected string) for " + 118 | `enumeration "${type.name}" on field "${name}" on type "${entity}"`) 119 | if (info.schema._typeMap[type]._values.find((enumValue) => { return enumValue.name === value }) === undefined) 120 | throw new Error("invalid value for " + 121 | `enumeration "${type.name}" on field "${name}" on type "${entity}"`) 122 | } 123 | fields.attribute[name] = value 124 | } 125 | else 126 | throw new Error(`field "${name}" not known on type "${entity}"`) 127 | }) 128 | } 129 | return fields 130 | } 131 | } 132 | 133 | -------------------------------------------------------------------------------- /src/gts-4-util-sequelize-options.js: -------------------------------------------------------------------------------- 1 | /* 2 | ** GraphQL-Tools-Sequelize -- Integration of GraphQL-Tools and Sequelize ORM 3 | ** Copyright (c) 2016-2023 Dr. Ralf S. Engelschall 4 | ** 5 | ** Permission is hereby granted, free of charge, to any person obtaining 6 | ** a copy of this software and associated documentation files (the 7 | ** "Software"), to deal in the Software without restriction, including 8 | ** without limitation the rights to use, copy, modify, merge, publish, 9 | ** distribute, sublicense, and/or sell copies of the Software, and to 10 | ** permit persons to whom the Software is furnished to do so, subject to 11 | ** the following conditions: 12 | ** 13 | ** The above copyright notice and this permission notice shall be included 14 | ** in all copies or substantial portions of the Software. 15 | ** 16 | ** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | ** EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | ** MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | ** IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | ** CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | ** TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | ** SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | */ 24 | 25 | /* external dependencies */ 26 | import Ducky from "ducky" 27 | 28 | /* the mixin class */ 29 | export default class gtsUtilSequelizeOptions { 30 | /* determine Sequelize operators */ 31 | _sequelizeOpMap () { 32 | const map = {} 33 | const symbols = this._sequelize.queryInterface.queryGenerator.OperatorMap 34 | for (const symbol of Object.getOwnPropertySymbols(symbols)) 35 | map[symbol.description] = symbol 36 | return map 37 | } 38 | 39 | /* build Sequelize "where" parameter */ 40 | _buildWhere (entity, src, allowed, opMap) { 41 | /* pass-through non-object sources (end of recursion) */ 42 | if (typeof src !== "object") 43 | return src 44 | 45 | /* build destination parameter */ 46 | let dst 47 | if (src instanceof Array) { 48 | dst = [] 49 | for (const value of src) 50 | dst.push(this._buildWhere(entity, value, allowed, opMap)) /* RECURSION */ 51 | } 52 | else { 53 | dst = {} 54 | for (const key of Object.keys(src)) { 55 | if (opMap[key] !== undefined) 56 | dst[opMap[key]] = this._buildWhere(entity, src[key], allowed, opMap) 57 | else if (allowed.attribute[key]) 58 | dst[key] = this._buildWhere(entity, src[key], allowed, opMap) 59 | else 60 | throw new Error(`invalid "where" argument: no such field "${key}" on type "${entity}"`) 61 | } 62 | } 63 | return dst 64 | } 65 | 66 | /* build Sequelize "include" parameter */ 67 | _buildInclude (entity, src, allowed, opMap) { 68 | /* sanity check source */ 69 | if (src instanceof Array) 70 | throw new Error("invalid \"include\" argument (object expected)") 71 | 72 | /* build destination parameter */ 73 | const dst = [] 74 | for (const key of Object.keys(src)) { 75 | if (allowed.relation[key] === undefined) 76 | throw new Error(`invalid "include" argument: no such relation "${key}" on type "${entity}"`) 77 | dst.push({ 78 | model: this._models[allowed.relation[key]], 79 | as: key, 80 | where: this._buildWhere(entity, src[key], allowed, opMap) 81 | }) 82 | } 83 | return dst 84 | } 85 | 86 | /* GraphQL standard options to Sequelize findByPk() options conversion */ 87 | _findOneOptions (entity, args, info) { 88 | const opts = {} 89 | 90 | /* determine allowed fields */ 91 | const allowed = this._fieldsOfGraphQLType(info, entity) 92 | 93 | /* determine Sequelize operator map */ 94 | const opMap = this._sequelizeOpMap() 95 | 96 | /* determine Sequelize "where" parameter */ 97 | if (args.where !== undefined) { 98 | if (typeof args.where !== "object") 99 | throw new Error("invalid \"where\" argument (object expected)") 100 | opts.where = this._buildWhere(entity, args.where, allowed, opMap) 101 | } 102 | 103 | /* determine Sequelize "include" parameter */ 104 | if (args.include !== undefined) { 105 | if (typeof args.include !== "object") 106 | throw new Error("invalid \"include\" argument (object expected)") 107 | opts.include = this._buildInclude(entity, args.include, allowed, opMap) 108 | } 109 | 110 | /* determine Sequelize "attributes" parameter */ 111 | const fieldInfo = this._graphqlRequestedFields(info) 112 | const fields = Object.keys(fieldInfo) 113 | const meth = fields.filter((field) => allowed.method[field]) 114 | const attr = fields.filter((field) => allowed.attribute[field]) 115 | const rels = fields.filter((field) => allowed.relation[field]) 116 | if ( args[this._hcname] === undefined 117 | && fieldInfo[this._hcname] === undefined 118 | && meth.length === 0 119 | && rels.length === 0 120 | && attr.filter((a) => !this._models[entity].rawAttributes[a]).length === 0) { 121 | /* in case no relationships should be followed at all from this entity, 122 | we can load the requested attributes only. If any relationship 123 | should be followed from this entity, we have to avoid 124 | such an attribute filter, as this means that at least "hasOne" relationships 125 | would be "null" when dereferenced afterwards. */ 126 | if (attr.length === 0) 127 | /* special case of plain method calls (neither attribute nor relationship) */ 128 | opts.attributes = [ this._idname ] 129 | else 130 | opts.attributes = attr 131 | } 132 | 133 | return opts 134 | } 135 | 136 | /* GraphQL standard options to Sequelize findAll() options conversion */ 137 | _findManyOptions (entity, args, info) { 138 | const opts = {} 139 | 140 | /* determine allowed fields */ 141 | const allowed = this._fieldsOfGraphQLType(info, entity) 142 | 143 | /* determine Sequelize operator map */ 144 | const opMap = this._sequelizeOpMap() 145 | 146 | /* determine Sequelize "where" parameter */ 147 | if (args.where !== undefined) { 148 | if (typeof args.where !== "object") 149 | throw new Error("invalid \"where\" argument (object expected)") 150 | opts.where = this._buildWhere(entity, args.where, allowed, opMap) 151 | } 152 | 153 | /* determine Sequelize "offset" parameter */ 154 | if (args.offset !== undefined) 155 | opts.offset = args.offset 156 | 157 | /* determine Sequelize "limit" parameter */ 158 | if (args.limit !== undefined) 159 | opts.limit = args.limit 160 | 161 | /* determine Sequelize "order" parameter */ 162 | if (args.order !== undefined) { 163 | if (!Ducky.validate(args.order, "( string | [ (string | [ string, string ])+ ])")) 164 | throw new Error("invalid \"order\" argument: wrong structure") 165 | opts.order = args.order 166 | } 167 | 168 | /* determine Sequelize "include" parameter */ 169 | if (args.include !== undefined) { 170 | if (typeof args.include !== "object") 171 | throw new Error("invalid \"include\" argument (object expected)") 172 | opts.include = this._buildInclude(entity, args.include, allowed, opMap) 173 | } 174 | 175 | /* determine Sequelize "attributes" parameter */ 176 | const fieldInfo = this._graphqlRequestedFields(info) 177 | const fields = Object.keys(fieldInfo) 178 | const meth = fields.filter((field) => allowed.method[field]) 179 | const attr = fields.filter((field) => allowed.attribute[field]) 180 | const rels = fields.filter((field) => allowed.relation[field]) 181 | if ( fieldInfo[this._hcname] === undefined 182 | && meth.length === 0 183 | && rels.length === 0 184 | && attr.filter((a) => !this._models[entity].rawAttributes[a]).length === 0) { 185 | /* in case no relationships should be followed at all from this entity, 186 | we can load the requested attributes only. If any relationship 187 | should be followed from this entity, we have to avoid 188 | such an attribute filter, as this means that at least "hasOne" relationships 189 | would be "null" when dereferenced afterwards. */ 190 | if (attr.length === 0) 191 | /* should not happen as GraphQL does not allow an entirely empty selection */ 192 | opts.attributes = [ this._idname ] 193 | else 194 | opts.attributes = attr 195 | } 196 | 197 | return opts 198 | } 199 | } 200 | 201 | -------------------------------------------------------------------------------- /src/gts-5-util-sequelize-fields.js: -------------------------------------------------------------------------------- 1 | /* 2 | ** GraphQL-Tools-Sequelize -- Integration of GraphQL-Tools and Sequelize ORM 3 | ** Copyright (c) 2016-2023 Dr. Ralf S. Engelschall 4 | ** 5 | ** Permission is hereby granted, free of charge, to any person obtaining 6 | ** a copy of this software and associated documentation files (the 7 | ** "Software"), to deal in the Software without restriction, including 8 | ** without limitation the rights to use, copy, modify, merge, publish, 9 | ** distribute, sublicense, and/or sell copies of the Software, and to 10 | ** permit persons to whom the Software is furnished to do so, subject to 11 | ** the following conditions: 12 | ** 13 | ** The above copyright notice and this permission notice shall be included 14 | ** in all copies or substantial portions of the Software. 15 | ** 16 | ** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | ** EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | ** MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | ** IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | ** CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | ** TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | ** SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | */ 24 | 25 | /* the mixin class */ 26 | export default class gtsUtilSequelizeFields { 27 | /* capitalize the first letter of an identifier */ 28 | _capitalize (id) { 29 | return (id.substr(0, 1).toUpperCase() + id.substr(1)) 30 | } 31 | 32 | /* update all relation fields of an entity */ 33 | async _entityUpdateFields (type, obj, def, upd, ctx, info) { 34 | /* determine common Sequelize options */ 35 | const opts = {} 36 | if (ctx.tx !== undefined) 37 | opts.transaction = ctx.tx 38 | 39 | /* iterate over all relationships... */ 40 | const rels = Object.keys(upd) 41 | for (let i = 0; i < rels.length; i++) { 42 | const name = rels[i] 43 | 44 | /* determine target type and relationship cardinality */ 45 | let t = info.schema._typeMap[type]._fields[name].type 46 | let many = false 47 | while (typeof t.ofType === "object") { 48 | if (t.constructor.name === "GraphQLList") 49 | many = true 50 | t = t.ofType 51 | } 52 | const target = t.name 53 | 54 | /* helper method for changing a single relationship */ 55 | const changeRelation = async (prefix, ids) => { 56 | /* map all ids onto real ORM objects */ 57 | const opts2 = Object.assign({}, opts, { where: { [ this._idname ]: ids } }) 58 | const objs = await this._models[target].findAll(opts2) 59 | 60 | /* sanity check requested ids */ 61 | if (objs.length < ids.length) { 62 | const found = {} 63 | objs.forEach((obj) => { found[obj[this._idname]] = true }) 64 | for (let j = 0; j < ids.length; j++) 65 | if (!found[ids[j]]) 66 | throw new Error(`no such entity ${target}#${ids[j]} found`) 67 | } 68 | 69 | /* sanity check usage */ 70 | if (!many && ids.length > 1) 71 | throw new Error(`relationship ${name} on type ${type} has cardinality 0..1 ` + 72 | "and cannot receive more than one foreign entity") 73 | 74 | /* change relationship */ 75 | if (many) { 76 | /* change relationship of cardinality 0..N */ 77 | const method = `${prefix}${this._capitalize(name)}` 78 | if (typeof obj[method] !== "function") 79 | throw new Error("relationship mutation method not found " + 80 | `to ${prefix} relation ${name} on type ${type}`) 81 | await obj[method](objs, opts) 82 | } 83 | else { 84 | /* change relationship of cardinality 0..1 */ 85 | if (prefix === "add") 86 | prefix = "set" 87 | const method = `${prefix}${this._capitalize(name)}` 88 | if (typeof obj[method] !== "function") 89 | throw new Error("relationship mutation method not found " + 90 | `to ${prefix} relation ${name} on type ${type}`) 91 | const relObj = prefix !== "remove" ? (objs.length ? objs[0] : null) : null 92 | await obj[method](relObj, opts) 93 | } 94 | } 95 | 96 | /* determine relationship value and dispatch according to operation */ 97 | const value = upd[name] 98 | if (value.set) await changeRelation("set", value.set) 99 | if (value.del) await changeRelation("remove", value.del) 100 | if (value.add) await changeRelation("add", value.add) 101 | } 102 | } 103 | 104 | /* map Sequelize "undefined" values to GraphQL "null" values to 105 | ensure that the GraphQL engine does not complain about resolvers 106 | which return "undefined" for "null" values. */ 107 | _mapFieldValues (type, obj, ctx, info) { 108 | /* determine allowed fields */ 109 | const allowed = this._fieldsOfGraphQLType(info, type) 110 | 111 | /* iterate over all GraphQL attributes */ 112 | Object.keys(allowed.attribute).forEach((attribute) => { 113 | /* map Sequelize "undefined" to GraphQL "null" */ 114 | if (obj[attribute] === undefined) 115 | obj[attribute] = null 116 | }) 117 | } 118 | } 119 | 120 | -------------------------------------------------------------------------------- /src/gts-6-util-fts.js: -------------------------------------------------------------------------------- 1 | /* 2 | ** GraphQL-Tools-Sequelize -- Integration of GraphQL-Tools and Sequelize ORM 3 | ** Copyright (c) 2016-2023 Dr. Ralf S. Engelschall 4 | ** 5 | ** Permission is hereby granted, free of charge, to any person obtaining 6 | ** a copy of this software and associated documentation files (the 7 | ** "Software"), to deal in the Software without restriction, including 8 | ** without limitation the rights to use, copy, modify, merge, publish, 9 | ** distribute, sublicense, and/or sell copies of the Software, and to 10 | ** permit persons to whom the Software is furnished to do so, subject to 11 | ** the following conditions: 12 | ** 13 | ** The above copyright notice and this permission notice shall be included 14 | ** in all copies or substantial portions of the Software. 15 | ** 16 | ** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | ** EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | ** MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | ** IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | ** CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | ** TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | ** SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | */ 24 | 25 | /* external dependencies */ 26 | import elasticlunr from "elasticlunr" 27 | 28 | /* the mixin class */ 29 | export default class gtsUtilFTS { 30 | /* mixin initialization */ 31 | initializer (sequelize, options) { 32 | this._ftsCfg = (typeof options.fts === "object" ? options.fts : null) 33 | this._ftsIdx = {} 34 | } 35 | 36 | /* cherry-pick fields for FTS indexing */ 37 | _ftsObj2Doc (type, obj) { 38 | const id = String(obj[this._idname]) 39 | const doc = { [ this._idname ]: id, __any: id } 40 | this._ftsCfg[type].forEach((field) => { 41 | const val = String(obj[field]) 42 | doc[field] = val 43 | doc.__any += ` ${val}` 44 | }) 45 | return doc 46 | } 47 | 48 | /* bootstrap FTS by creating initial in-memory index */ 49 | async _ftsBoot () { 50 | /* operate only if FTS is configured */ 51 | if (this._ftsCfg === null) 52 | return 53 | 54 | /* iterate over all entity types... */ 55 | for (const type of Object.keys(this._ftsCfg)) { 56 | /* create a new in-memory index */ 57 | this._ftsIdx[type] = new elasticlunr.Index() 58 | this._ftsIdx[type].saveDocument(false) 59 | this._ftsIdx[type].addField(this._idname) 60 | this._ftsIdx[type].addField("__any") 61 | this._ftsCfg[type].forEach((field) => { 62 | this._ftsIdx[type].addField(field) 63 | }) 64 | this._ftsIdx[type].setRef(this._idname) 65 | 66 | /* iterate over all entity objects... */ 67 | const opts = { attributes: this._ftsCfg[type].concat([ this._idname ]) } 68 | const objs = await this._models[type].findAll(opts) 69 | objs.forEach((obj) => { 70 | /* add entity objects to index */ 71 | const doc = this._ftsObj2Doc(type, obj) 72 | this._ftsIdx[type].addDoc(doc) 73 | }) 74 | } 75 | } 76 | 77 | /* update the FTS index */ 78 | _ftsUpdate (type, oid, obj, op) { 79 | /* operate only if FTS is configured */ 80 | if (this._ftsCfg === null) 81 | return 82 | if (this._ftsCfg[type] === undefined) 83 | return 84 | 85 | /* dispatch according to operation */ 86 | if (op === "create") { 87 | /* add entity to index */ 88 | const doc = this._ftsObj2Doc(type, obj) 89 | this._ftsIdx[type].addDoc(doc) 90 | } 91 | else if (op === "update") { 92 | /* update entity in index */ 93 | const doc = this._ftsObj2Doc(type, obj) 94 | this._ftsIdx[type].updateDoc(doc) 95 | } 96 | else if (op === "delete") { 97 | /* delete entity from index */ 98 | this._ftsIdx[type].removeDocByRef(oid) 99 | } 100 | } 101 | 102 | /* search in the FTS index */ 103 | _ftsSearch (type, query, order, offset, limit, ctx) { 104 | /* operate only if FTS is configured */ 105 | if (this._ftsCfg === null) 106 | return new Error("Full-Text-Search (FTS) not available at all") 107 | if (this._ftsCfg[type] === undefined) 108 | return new Error(`Full-Text-Search (FTS) not available for entity "${type}"`) 109 | 110 | /* parse "[field:]keyword [field:]keyword [, ...]" query string */ 111 | const queries = [] 112 | query.split(/\s*,\s*/).forEach((query) => { 113 | const fields = {} 114 | query.split(/\s+/).forEach((field) => { 115 | let fn = "__any" 116 | let kw = field 117 | let m 118 | if ((m = field.match(/^(.+):(.+)$/)) !== null) { 119 | fn = m[1] 120 | kw = m[2] 121 | } 122 | if (fn !== "__any" && this._ftsCfg[type].indexOf(fn) < 0) 123 | throw new Error(`Full-Text-Search (FTS) not available for field "${fn}" of entity "${type}"`) 124 | if (fields[fn] === undefined) 125 | fields[fn] = [] 126 | fields[fn].push(kw) 127 | }) 128 | queries.push(fields) 129 | }) 130 | 131 | /* iterate over all queries... */ 132 | const results1 = {} 133 | queries.forEach((query) => { 134 | /* iterate over all fields... */ 135 | const results2 = {} 136 | Object.keys(query).forEach((field) => { 137 | /* lookup entity ids from index for particular field */ 138 | const kw = query[field].join(" ") 139 | const config = { 140 | fields: { 141 | [field]: { 142 | boost: 1, 143 | expand: true, 144 | bool: "AND" 145 | } 146 | } 147 | } 148 | const results = this._ftsIdx[type].search(kw, config) 149 | 150 | /* reduce result list to set of unique ids */ 151 | const results3 = {} 152 | results.forEach((result) => { 153 | const oid = result.ref 154 | results3[oid] = true 155 | }) 156 | 157 | /* AND-combine results with previous results */ 158 | const oids = Object.keys(results2) 159 | if (oids.length === 0) 160 | Object.keys(results3).forEach((oid) => { 161 | results2[oid] = true 162 | }) 163 | else { 164 | oids.forEach((oid) => { 165 | if (!results3[oid]) 166 | delete results2[oid] 167 | }) 168 | } 169 | }) 170 | 171 | /* OR-combine results with previous results */ 172 | Object.keys(results2).forEach((oid) => { 173 | results1[oid] = true 174 | }) 175 | }) 176 | 177 | /* query entity objects from database */ 178 | const opts = { where: { [this._idname]: Object.keys(results1) } } 179 | if (order !== undefined) opts.order = order 180 | if (offset !== undefined) opts.offset = offset 181 | if (limit !== undefined) opts.limit = limit 182 | if (ctx.tx !== undefined) opts.transaction = ctx.tx 183 | return this._models[type].findAll(opts) 184 | } 185 | } 186 | 187 | -------------------------------------------------------------------------------- /src/gts-7-entity-query.js: -------------------------------------------------------------------------------- 1 | /* 2 | ** GraphQL-Tools-Sequelize -- Integration of GraphQL-Tools and Sequelize ORM 3 | ** Copyright (c) 2016-2023 Dr. Ralf S. Engelschall 4 | ** 5 | ** Permission is hereby granted, free of charge, to any person obtaining 6 | ** a copy of this software and associated documentation files (the 7 | ** "Software"), to deal in the Software without restriction, including 8 | ** without limitation the rights to use, copy, modify, merge, publish, 9 | ** distribute, sublicense, and/or sell copies of the Software, and to 10 | ** permit persons to whom the Software is furnished to do so, subject to 11 | ** the following conditions: 12 | ** 13 | ** The above copyright notice and this permission notice shall be included 14 | ** in all copies or substantial portions of the Software. 15 | ** 16 | ** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | ** EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | ** MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | ** IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | ** CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | ** TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | ** SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | */ 24 | 25 | /* external dependencies */ 26 | import Bluebird from "bluebird" 27 | 28 | /* the mixin class */ 29 | export default class gtsEntityQuery { 30 | /* calculate hash code of entity */ 31 | _hashCodeForEntity (info, type, obj) { 32 | const fields = this._fieldsOfGraphQLType(info, type) 33 | const data = Object.keys(fields.attribute) 34 | .sort() 35 | .filter((name) => name !== this._hcname) 36 | .map((attribute) => JSON.stringify(obj[attribute])) 37 | .join(",") 38 | return this._hcmake(data) 39 | } 40 | 41 | /* API: query/read identifier and hash-code attributes */ 42 | attrIdSchema (source) { 43 | return "" + 44 | `# the unique identifier of the [${source}]() entity.\n` + 45 | `${this._idname}: ${this._idtype}!\n` 46 | } 47 | attrIdResolver (source) { 48 | return (parent, args, ctx, info) => { 49 | return parent[this._idname] 50 | } 51 | } 52 | attrHcSchema (source) { 53 | return "" + 54 | `# the hash-code of the [${source}]() entity.\n` + 55 | `${this._hcname}: ${this._hctype}!\n` 56 | } 57 | attrHcResolver (source) { 58 | return (parent, args, ctx, info) => { 59 | return this._hashCodeForEntity(info, source, parent) 60 | } 61 | } 62 | 63 | /* API: query/read one or many entities (directly or via relation) */ 64 | entityQuerySchema (source, relation, target) { 65 | let isMany = false 66 | let m 67 | if ((m = target.match(/^(.+)\*$/)) !== null) { 68 | target = m[1] 69 | isMany = true 70 | } 71 | if (isMany) { 72 | /* MANY */ 73 | if (relation === "") 74 | /* directly */ 75 | return "" + 76 | `# Query one or many [${target}]() entities,\n` + 77 | "# by either an (optionally available) full-text-search (`query`)\n" + 78 | "# or an (always available) attribute-based condition (`where`),\n" + 79 | "# optionally filter them by a condition on some relationships (`include`),\n" + 80 | "# optionally sort them (`order`),\n" + 81 | "# optionally start the result set at the n-th entity (zero-based `offset`), and\n" + 82 | "# optionally reduce the result set to a maximum number of entities (`limit`).\n" + 83 | `${target}s(fts: String, where: JSON, include: JSON, order: JSON, offset: Int = 0, limit: Int = 100): [${target}]!\n` 84 | else 85 | /* via relation */ 86 | return "" + 87 | `# Query one or many [${target}]() entities\n` + 88 | `# by following the **${relation}** relation of [${source}]() entity,\n` + 89 | "# optionally filter them by a condition (`where`),\n" + 90 | "# optionally filter them by a condition on some relationships (`include`),\n" + 91 | "# optionally sort them (`order`),\n" + 92 | "# optionally start the result set at the n-th entity (zero-based `offset`), and\n" + 93 | "# optionally reduce the result set to a maximum number of entities (`limit`).\n" + 94 | `${relation}(where: JSON, include: JSON, order: JSON, offset: Int = 0, limit: Int = 100): [${target}]!\n` 95 | } 96 | else { 97 | /* ONE */ 98 | if (relation === "") 99 | /* directly */ 100 | return "" + 101 | `# Query one [${target}]() entity by its unique identifier (\`${this._idname}\`) or condition (\`where\`) or` + 102 | `# open an anonymous context for the [${target}]() entity.\n` + 103 | `# The [${target}]() entity can be optionally required to have a particular hash-code (\`${this._hcname}\`) for optimistic locking purposes.\n` + 104 | `# The [${target}]() entity can be optionally filtered by a condition on some relationships (\`include\`).\n` + 105 | `${target}(${this._idname}: ${this._idtype}, ${this._hcname}: ${this._hctype}, where: JSON, include: JSON): ${target}\n` 106 | else 107 | /* via relation */ 108 | return "" + 109 | `# Query one [${target}]() entity by following the **${relation}** relation of [${source}]() entity.\n` + 110 | `# The [${target}]() entity can be optionally required to have a particular hash-code (\`${this._hcname}\`) for optimistic locking purposes.\n` + 111 | `# The [${target}]() entity can be optionally filtered by a condition (\`where\`).\n` + 112 | `# The [${target}]() entity can be optionally filtered by a condition on some relationships (\`include\`).\n` + 113 | `${relation}(${this._hcname}: ${this._hctype}, where: JSON, include: JSON): ${target}\n` 114 | } 115 | } 116 | entityQueryResolver (source, relation, target) { 117 | let isMany = false 118 | let m 119 | if ((m = target.match(/^(.+)\*$/)) !== null) { 120 | target = m[1] 121 | isMany = true 122 | } 123 | return async (parent, args, ctx, info) => { 124 | if (isMany) { 125 | /* MANY */ 126 | 127 | /* determine filter options */ 128 | const opts = this._findManyOptions(target, args, info) 129 | if (ctx.tx !== undefined) 130 | opts.transaction = ctx.tx 131 | 132 | /* find entities */ 133 | let objs 134 | if (relation === "") { 135 | /* directly */ 136 | if (args.fts !== undefined) 137 | /* directly, via FTS index */ 138 | objs = await this._ftsSearch(target, args.fts, args.order, args.offset, args.limit, ctx) 139 | else 140 | /* directly, via database */ 141 | objs = await this._models[target].findAll(opts) 142 | } 143 | else { 144 | /* via relation */ 145 | const getter = `get${this._capitalize(relation)}` 146 | objs = await parent[getter](opts) 147 | } 148 | 149 | /* check authorization */ 150 | objs = await Bluebird.filter(objs, (obj) => { 151 | return this._authorized("after", "read", target, obj, ctx) 152 | }) 153 | 154 | /* map field values */ 155 | await Bluebird.each(objs, (obj) => { 156 | this._mapFieldValues(target, obj, ctx, info) 157 | }) 158 | 159 | /* trace access */ 160 | await this._trace(Object.assign({}, relation !== "" ? { 161 | srcType: source, 162 | srcId: parent[this._idname], 163 | srcAttr: relation 164 | } : {}, { 165 | op: "read", 166 | arity: "many", 167 | dstType: target, 168 | dstIds: objs.map((obj) => obj[this._idname]), 169 | dstAttrs: Object.keys(this._graphqlRequestedFields(info)) 170 | }), ctx) 171 | 172 | return objs 173 | } 174 | else { 175 | /* ONE */ 176 | 177 | /* determine filter options */ 178 | const opts = this._findOneOptions(target, args, info) 179 | if (ctx.tx !== undefined) 180 | opts.transaction = ctx.tx 181 | 182 | /* find entity */ 183 | let obj 184 | if (relation === "") { 185 | /* directly */ 186 | if (args[this._idname] !== undefined) 187 | /* regular case: non-anonymous context, find by identifier */ 188 | obj = await this._models[target].findByPk(args[this._idname], opts) 189 | else if (args.where !== undefined) 190 | /* regular case: non-anonymous context, find by condition */ 191 | obj = await this._models[target].findOne(opts) 192 | else 193 | /* special case: anonymous context */ 194 | return new this._anonCtx(target) 195 | } 196 | else { 197 | /* via relation */ 198 | const getter = `get${this._capitalize(relation)}` 199 | obj = await parent[getter](opts) 200 | } 201 | if (obj === null) 202 | return null 203 | 204 | /* check optional hash-code */ 205 | if (args[this._hcname] !== undefined) { 206 | const hc = this._hashCodeForEntity(info, target, obj) 207 | if (hc !== args[this._hcname]) 208 | throw new Error(`entity ${target}#${obj[this._idname]} has hash-code ${hc} ` + 209 | `(expected hash-code ${args[this._hcname]})`) 210 | } 211 | 212 | /* check authorization */ 213 | if (!(await this._authorized("after", "read", target, obj, ctx))) 214 | return null 215 | 216 | /* map field values */ 217 | this._mapFieldValues(target, obj, ctx, info) 218 | 219 | /* trace access */ 220 | await this._trace(Object.assign({}, relation !== "" ? { 221 | srcType: source, 222 | srcId: parent[this._idname], 223 | srcAttr: relation 224 | } : {}, { 225 | op: "read", 226 | arity: "one", 227 | dstType: target, 228 | dstIds: [ obj[this._idname] ], 229 | dstAttrs: Object.keys(this._graphqlRequestedFields(info)) 230 | }), ctx) 231 | 232 | return obj 233 | } 234 | } 235 | } 236 | } 237 | 238 | -------------------------------------------------------------------------------- /src/gts-8-entity-create.js: -------------------------------------------------------------------------------- 1 | /* 2 | ** GraphQL-Tools-Sequelize -- Integration of GraphQL-Tools and Sequelize ORM 3 | ** Copyright (c) 2016-2023 Dr. Ralf S. Engelschall 4 | ** 5 | ** Permission is hereby granted, free of charge, to any person obtaining 6 | ** a copy of this software and associated documentation files (the 7 | ** "Software"), to deal in the Software without restriction, including 8 | ** without limitation the rights to use, copy, modify, merge, publish, 9 | ** distribute, sublicense, and/or sell copies of the Software, and to 10 | ** permit persons to whom the Software is furnished to do so, subject to 11 | ** the following conditions: 12 | ** 13 | ** The above copyright notice and this permission notice shall be included 14 | ** in all copies or substantial portions of the Software. 15 | ** 16 | ** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | ** EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | ** MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | ** IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | ** CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | ** TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | ** SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | */ 24 | 25 | /* the mixin class */ 26 | export default class gtsEntityCreate { 27 | /* API: create a new entity */ 28 | entityCreateSchema (type) { 29 | return "" + 30 | `# Create new [${type}]() entity, optionally with specified unique identifier (\`${this._idname}\`) and attributes (\`with\`).\n` + 31 | `create(${this._idname}: ${this._idtype}, with: JSON): ${type}!\n` 32 | } 33 | entityCreateResolver (type) { 34 | return async (entity, args, ctx, info) => { 35 | /* sanity check usage context */ 36 | if (info && info.operation && info.operation.operation !== "mutation") 37 | throw new Error("method \"create\" only allowed under \"mutation\" operation") 38 | if (!(typeof entity === "object" && entity instanceof this._anonCtx && entity.isType(type))) 39 | throw new Error(`method "create" only allowed in anonymous ${type} context`) 40 | 41 | /* determine fields of entity as defined in GraphQL schema */ 42 | const defined = this._fieldsOfGraphQLType(info, type) 43 | 44 | /* determine fields of entity as requested in GraphQL request */ 45 | const build = this._fieldsOfGraphQLRequest(args, info, type) 46 | 47 | /* handle unique id */ 48 | if (args[this._idname] === undefined) 49 | /* auto-generate the id */ 50 | build.attribute[this._idname] = this._idmake() 51 | else { 52 | /* take over id, but ensure it is unique */ 53 | build.attribute[this._idname] = args[this._idname] 54 | const opts = {} 55 | if (ctx.tx !== undefined) 56 | opts.transaction = ctx.tx 57 | opts.attributes = [ this._idname ] 58 | const existing = await this._models[type].findByPk(build.attribute[this._idname], opts) 59 | if (existing !== null) 60 | throw new Error(`entity ${type}#${build.attribute[this._idname]} already exists`) 61 | } 62 | 63 | /* validate attributes */ 64 | await this._validate(type, build, ctx) 65 | 66 | /* build a new entity */ 67 | const obj = this._models[type].build(build.attribute) 68 | 69 | /* check access to entity before action */ 70 | if (!(await this._authorized("before", "create", type, obj, ctx))) 71 | throw new Error(`will not be allowed to create entity of type "${type}"`) 72 | 73 | /* save new entity */ 74 | const opts = {} 75 | if (ctx.tx !== undefined) 76 | opts.transaction = ctx.tx 77 | const err = await obj.save(opts).catch((err) => err) 78 | if (typeof err === "object" && err instanceof Error) 79 | throw new Error("Sequelize: save: " + err.message + ":" + 80 | err.errors.map((e) => e.message).join("; ")) 81 | 82 | /* post-adjust the relationships according to the request */ 83 | await this._entityUpdateFields(type, obj, 84 | defined.relation, build.relation, ctx, info) 85 | 86 | /* check access to entity after action */ 87 | if (!(await this._authorized("after", "create", type, obj, ctx))) 88 | throw new Error(`was not allowed to create entity of type "${type}"`) 89 | 90 | /* check access to entity again */ 91 | if (!(await this._authorized("after", "read", type, obj, ctx))) 92 | throw new Error(`was not allowed to read (created) entity of type "${type}"`) 93 | 94 | /* map field values */ 95 | this._mapFieldValues(type, obj, ctx, info) 96 | 97 | /* update FTS index */ 98 | this._ftsUpdate(type, obj[this._idname], obj, "create") 99 | 100 | /* trace access */ 101 | await this._trace({ 102 | op: "create", 103 | arity: "one", 104 | dstType: type, 105 | dstIds: [ obj[this._idname] ], 106 | dstAttrs: Object.keys(build.attribute).concat(Object.keys(build.relation)) 107 | }, ctx) 108 | 109 | /* return new entity */ 110 | return obj 111 | } 112 | } 113 | } 114 | 115 | -------------------------------------------------------------------------------- /src/gts-9-entity-clone.js: -------------------------------------------------------------------------------- 1 | /* 2 | ** GraphQL-Tools-Sequelize -- Integration of GraphQL-Tools and Sequelize ORM 3 | ** Copyright (c) 2016-2023 Dr. Ralf S. Engelschall 4 | ** 5 | ** Permission is hereby granted, free of charge, to any person obtaining 6 | ** a copy of this software and associated documentation files (the 7 | ** "Software"), to deal in the Software without restriction, including 8 | ** without limitation the rights to use, copy, modify, merge, publish, 9 | ** distribute, sublicense, and/or sell copies of the Software, and to 10 | ** permit persons to whom the Software is furnished to do so, subject to 11 | ** the following conditions: 12 | ** 13 | ** The above copyright notice and this permission notice shall be included 14 | ** in all copies or substantial portions of the Software. 15 | ** 16 | ** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | ** EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | ** MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | ** IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | ** CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | ** TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | ** SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | */ 24 | 25 | /* the mixin class */ 26 | export default class gtsEntityClone { 27 | /* API: clone an entity (without relationships) */ 28 | entityCloneSchema (type) { 29 | return "" + 30 | `# Clone one [${type}]() entity by cloning its attributes (but not its relationships).\n` + 31 | `clone: ${type}!\n` 32 | } 33 | entityCloneResolver (type) { 34 | return async (entity, args, ctx, info) => { 35 | /* sanity check usage context */ 36 | if (info && info.operation && info.operation.operation !== "mutation") 37 | throw new Error("method \"clone\" only allowed under \"mutation\" operation") 38 | if (typeof entity === "object" && entity instanceof this._anonCtx && entity.isType(type)) 39 | throw new Error(`method "clone" only allowed in non-anonymous ${type} context`) 40 | 41 | /* determine fields of entity as defined in GraphQL schema */ 42 | const defined = this._fieldsOfGraphQLType(info, type) 43 | 44 | /* check access to parent entity */ 45 | if (!(await this._authorized("after", "read", type, entity, ctx))) 46 | throw new Error(`not allowed to read entity of type "${type}"`) 47 | 48 | /* build a new entity */ 49 | const data = {} 50 | data[this._idname] = this._idmake() 51 | Object.keys(defined.attribute).forEach((attr) => { 52 | if (attr !== this._idname) 53 | data[attr] = entity[attr] 54 | }) 55 | const obj = this._models[type].build(data) 56 | 57 | /* check access to entity before action */ 58 | if (!(await this._authorized("before", "create", type, obj, ctx))) 59 | throw new Error(`will not be allowed to clone entity of type "${type}"`) 60 | 61 | /* save new entity */ 62 | const opts = {} 63 | if (ctx.tx !== undefined) 64 | opts.transaction = ctx.tx 65 | const err = await obj.save(opts).catch((err) => err) 66 | if (typeof err === "object" && err instanceof Error) 67 | throw new Error("Sequelize: save: " + err.message + ":" + 68 | err.errors.map((e) => e.message).join("; ")) 69 | 70 | /* check access to entity after action */ 71 | if (!(await this._authorized("after", "create", type, obj, ctx))) 72 | throw new Error(`was not allowed to clone entity of type "${type}"`) 73 | 74 | /* check access to entity again */ 75 | if (!(await this._authorized("after", "read", type, obj, ctx))) 76 | throw new Error(`was not allowed to read (cloned) entity of type "${type}"`) 77 | 78 | /* map field values */ 79 | this._mapFieldValues(type, obj, ctx, info) 80 | 81 | /* update FTS index */ 82 | this._ftsUpdate(type, obj[this._idname], obj, "create") 83 | 84 | /* trace access */ 85 | await this._trace({ 86 | op: "create", 87 | arity: "one", 88 | dstType: type, 89 | dstIds: [ obj[this._idname] ], 90 | dstAttrs: Object.keys(data) 91 | }, ctx) 92 | 93 | /* return new entity */ 94 | return obj 95 | } 96 | } 97 | } 98 | 99 | -------------------------------------------------------------------------------- /src/gts-A-entity-update.js: -------------------------------------------------------------------------------- 1 | /* 2 | ** GraphQL-Tools-Sequelize -- Integration of GraphQL-Tools and Sequelize ORM 3 | ** Copyright (c) 2016-2023 Dr. Ralf S. Engelschall 4 | ** 5 | ** Permission is hereby granted, free of charge, to any person obtaining 6 | ** a copy of this software and associated documentation files (the 7 | ** "Software"), to deal in the Software without restriction, including 8 | ** without limitation the rights to use, copy, modify, merge, publish, 9 | ** distribute, sublicense, and/or sell copies of the Software, and to 10 | ** permit persons to whom the Software is furnished to do so, subject to 11 | ** the following conditions: 12 | ** 13 | ** The above copyright notice and this permission notice shall be included 14 | ** in all copies or substantial portions of the Software. 15 | ** 16 | ** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | ** EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | ** MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | ** IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | ** CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | ** TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | ** SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | */ 24 | 25 | /* the mixin class */ 26 | export default class gtsEntityUpdate { 27 | /* API: update an entity */ 28 | entityUpdateSchema (type) { 29 | return "" + 30 | `# Update one [${type}]() entity with specified attributes (\`with\`).\n` + 31 | `update(with: JSON!): ${type}!\n` 32 | } 33 | entityUpdateResolver (type) { 34 | return async (entity, args, ctx, info) => { 35 | /* sanity check usage context */ 36 | if (info && info.operation && info.operation.operation !== "mutation") 37 | throw new Error("method \"update\" only allowed under \"mutation\" operation") 38 | if (typeof entity === "object" && entity instanceof this._anonCtx && entity.isType(type)) 39 | throw new Error(`method "update" only allowed in non-anonymous ${type} context`) 40 | 41 | /* determine fields of entity as defined in GraphQL schema */ 42 | const defined = this._fieldsOfGraphQLType(info, type) 43 | 44 | /* determine fields of entity as requested in GraphQL request */ 45 | const build = this._fieldsOfGraphQLRequest(args, info, type) 46 | 47 | /* check access to entity before action */ 48 | if (!(await this._authorized("before", "update", type, entity, ctx))) 49 | throw new Error(`will not be allowed to update entity of type "${type}"`) 50 | 51 | /* validate attributes */ 52 | await this._validate(type, build, ctx) 53 | 54 | /* adjust the attributes according to the request */ 55 | const opts = {} 56 | if (ctx.tx !== undefined) 57 | opts.transaction = ctx.tx 58 | await entity.update(build.attribute, opts) 59 | 60 | /* adjust the relationships according to the request */ 61 | await this._entityUpdateFields(type, entity, 62 | defined.relation, build.relation, ctx, info) 63 | 64 | /* check access to entity after action */ 65 | if (!(await this._authorized("after", "update", type, entity, ctx))) 66 | throw new Error(`was not allowed to update entity of type "${type}"`) 67 | 68 | /* check access to entity again */ 69 | if (!(await this._authorized("after", "read", type, entity, ctx))) 70 | throw new Error(`was not allowed to read (updated) entity of type "${type}"`) 71 | 72 | /* map field values */ 73 | this._mapFieldValues(type, entity, ctx, info) 74 | 75 | /* update FTS index */ 76 | this._ftsUpdate(type, entity[this._idname], entity, "update") 77 | 78 | /* trace access */ 79 | await this._trace({ 80 | op: "update", 81 | arity: "one", 82 | dstType: type, 83 | dstIds: [ entity[this._idname] ], 84 | dstAttrs: Object.keys(build.attribute).concat(Object.keys(build.relation)) 85 | }, ctx) 86 | 87 | /* return updated entity */ 88 | return entity 89 | } 90 | } 91 | } 92 | 93 | -------------------------------------------------------------------------------- /src/gts-B-entity-delete.js: -------------------------------------------------------------------------------- 1 | /* 2 | ** GraphQL-Tools-Sequelize -- Integration of GraphQL-Tools and Sequelize ORM 3 | ** Copyright (c) 2016-2023 Dr. Ralf S. Engelschall 4 | ** 5 | ** Permission is hereby granted, free of charge, to any person obtaining 6 | ** a copy of this software and associated documentation files (the 7 | ** "Software"), to deal in the Software without restriction, including 8 | ** without limitation the rights to use, copy, modify, merge, publish, 9 | ** distribute, sublicense, and/or sell copies of the Software, and to 10 | ** permit persons to whom the Software is furnished to do so, subject to 11 | ** the following conditions: 12 | ** 13 | ** The above copyright notice and this permission notice shall be included 14 | ** in all copies or substantial portions of the Software. 15 | ** 16 | ** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | ** EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | ** MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | ** IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | ** CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | ** TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | ** SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | */ 24 | 25 | /* the mixin class */ 26 | export default class gtsEntityDelete { 27 | /* API: delete an entity */ 28 | entityDeleteSchema (type) { 29 | return "" + 30 | `# Delete one [${type}]() entity.\n` + 31 | `delete: ${this._idtype}!\n` 32 | } 33 | entityDeleteResolver (type) { 34 | return async (entity, args, ctx, info) => { 35 | /* sanity check usage context */ 36 | if (info && info.operation && info.operation.operation !== "mutation") 37 | throw new Error("method \"delete\" only allowed under \"mutation\" operation") 38 | if (typeof entity === "object" && entity instanceof this._anonCtx && entity.isType(type)) 39 | throw new Error(`method "delete" only allowed in non-anonymous ${type} context`) 40 | 41 | /* check access to target before action */ 42 | if (!(await this._authorized("before", "delete", type, entity, ctx))) 43 | return new Error(`will not be allowed to delete entity of type "${type}"`) 44 | 45 | /* delete the instance */ 46 | const opts = {} 47 | if (ctx.tx !== undefined) 48 | opts.transaction = ctx.tx 49 | const result = entity[this._idname] 50 | await entity.destroy(opts) 51 | 52 | /* update FTS index */ 53 | this._ftsUpdate(type, result, null, "delete") 54 | 55 | /* trace access */ 56 | await this._trace({ 57 | op: "delete", 58 | arity: "one", 59 | dstType: type, 60 | dstIds: [ result ], 61 | dstAttrs: [ "*" ] 62 | }, ctx) 63 | 64 | /* return id of deleted entity */ 65 | return result 66 | } 67 | } 68 | } 69 | 70 | -------------------------------------------------------------------------------- /src/gts.js: -------------------------------------------------------------------------------- 1 | /* 2 | ** GraphQL-Tools-Sequelize -- Integration of GraphQL-Tools and Sequelize ORM 3 | ** Copyright (c) 2016-2023 Dr. Ralf S. Engelschall 4 | ** 5 | ** Permission is hereby granted, free of charge, to any person obtaining 6 | ** a copy of this software and associated documentation files (the 7 | ** "Software"), to deal in the Software without restriction, including 8 | ** without limitation the rights to use, copy, modify, merge, publish, 9 | ** distribute, sublicense, and/or sell copies of the Software, and to 10 | ** permit persons to whom the Software is furnished to do so, subject to 11 | ** the following conditions: 12 | ** 13 | ** The above copyright notice and this permission notice shall be included 14 | ** in all copies or substantial portions of the Software. 15 | ** 16 | ** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | ** EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | ** MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | ** IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | ** CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | ** TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | ** SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | */ 24 | 25 | /* external dependencies */ 26 | import UUID from "pure-uuid" 27 | import aggregation from "aggregation/es6" 28 | 29 | /* internal dependencies */ 30 | import gtsVersion from "./gts-1-version" 31 | import gtsUtilHook from "./gts-2-util-hook" 32 | import gtsUtilGraphQL from "./gts-3-util-graphql" 33 | import gtsUtilSequelizeOptions from "./gts-4-util-sequelize-options" 34 | import gtsUtilSequelizeFields from "./gts-5-util-sequelize-fields" 35 | import gtsUtilFTS from "./gts-6-util-fts" 36 | import gtsEntityQuery from "./gts-7-entity-query" 37 | import gtsEntityCreate from "./gts-8-entity-create" 38 | import gtsEntityClone from "./gts-9-entity-clone" 39 | import gtsEntityUpdate from "./gts-A-entity-update" 40 | import gtsEntityDelete from "./gts-B-entity-delete" 41 | 42 | /* the API class */ 43 | class GraphQLToolsSequelize extends aggregation( 44 | gtsVersion, 45 | gtsUtilHook, 46 | gtsUtilGraphQL, 47 | gtsUtilSequelizeOptions, 48 | gtsUtilSequelizeFields, 49 | gtsUtilFTS, 50 | gtsEntityQuery, 51 | gtsEntityCreate, 52 | gtsEntityClone, 53 | gtsEntityUpdate, 54 | gtsEntityDelete 55 | ) { 56 | constructor (sequelize, options = {}) { 57 | super(sequelize, options) 58 | this._sequelize = sequelize 59 | this._models = sequelize.models 60 | this._idname = (typeof options.idname === "string" ? options.idtype : "id") 61 | this._idtype = (typeof options.idtype === "string" ? options.idtype : "UUID") 62 | this._idmake = (typeof options.idmake === "function" ? options.idmake : () => (new UUID(1)).format()) 63 | this._hcname = (typeof options.hcname === "string" ? options.hctype : "hc") 64 | this._hctype = (typeof options.hctype === "string" ? options.hctype : "UUID") 65 | this._hcmake = (typeof options.hcmake === "function" ? options.hcmake : (data) => (new UUID(5, "ns:URL", `uri:gts:${data}`)).format()) 66 | this._anonCtx = function (type) { this.__$type$ = type } 67 | this._anonCtx.prototype.isType = function (type) { return this.__$type$ === type } 68 | } 69 | boot () { 70 | return this._ftsBoot() 71 | } 72 | } 73 | 74 | /* export the traditional way for interoperability reasons 75 | (as Babel would export an object with a 'default' field) */ 76 | module.exports = GraphQLToolsSequelize 77 | 78 | --------------------------------------------------------------------------------