From a93c78f011ccf9f7ea3e8a62b098a7698db5f9ec Mon Sep 17 00:00:00 2001 From: zemanl Date: Mon, 26 Oct 2020 12:30:11 +0100 Subject: [PATCH] added batch functionality (resolver and schema for batch operations (possible operation in a batch: create, clone, update, delete)) --- package.json | 3 +- sample/package.json | 4 +- sample/sample.js | 164 ++++++++++++++++++++++++++++++++++++- src/gts-3-util-graphql.js | 2 +- src/gts-8-entity-create.js | 140 ++++++++++++++++--------------- src/gts-9-entity-clone.js | 95 +++++++++++---------- src/gts-A-entity-update.js | 80 +++++++++--------- src/gts-B-entity-delete.js | 50 ++++++----- src/gts-C-entity-batch.js | 146 +++++++++++++++++++++++++++++++++ src/gts.js | 4 +- 10 files changed, 510 insertions(+), 178 deletions(-) create mode 100644 src/gts-C-entity-batch.js diff --git a/package.json b/package.json index 7b42d7c..0de568d 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "scripts": { "prepublishOnly": "grunt default", "build": "grunt default", - "test": "cd sample && npm start" + "test": "cd sample && npm start", + "retest": "npm run build && cd sample && npm run restart" } } diff --git a/sample/package.json b/sample/package.json index 3e2c766..6178e03 100644 --- a/sample/package.json +++ b/sample/package.json @@ -19,6 +19,8 @@ "sqlite3": "4.2.0" }, "scripts": { - "start": "babel-node --presets @babel/preset-env --only sample.js sample.js" + "start": "babel-node --presets @babel/preset-env --only sample.js sample.js", + "clean": "shx rm -rf node_modules/graphql-tools-sequelize", + "restart": "npm run clean && npm i graphql-tools-sequelize && npm run start" } } diff --git a/sample/sample.js b/sample/sample.js index 3d2be80..98e1ea2 100644 --- a/sample/sample.js +++ b/sample/sample.js @@ -130,6 +130,7 @@ import Sequelize from "sequelize" ${gts.entityCreateSchema("OrgUnit")} ${gts.entityUpdateSchema("OrgUnit")} ${gts.entityDeleteSchema("OrgUnit")} + ${gts.entityBatchSchema("OrgUnit")} } type Person { ${gts.attrIdSchema("Person")} @@ -143,6 +144,7 @@ import Sequelize from "sequelize" ${gts.entityCreateSchema("Person")} ${gts.entityUpdateSchema("Person")} ${gts.entityDeleteSchema("Person")} + ${gts.entityBatchSchema("Person")} } enum Role { principal @@ -170,7 +172,8 @@ import Sequelize from "sequelize" clone: gts.entityCloneResolver ("OrgUnit"), create: gts.entityCreateResolver("OrgUnit"), update: gts.entityUpdateResolver("OrgUnit"), - delete: gts.entityDeleteResolver("OrgUnit") + delete: gts.entityDeleteResolver("OrgUnit"), + batch: gts.entityBatchResolver("OrgUnit") }, Person: { id: gts.attrIdResolver ("Person"), @@ -181,7 +184,8 @@ import Sequelize from "sequelize" clone: gts.entityCloneResolver ("Person"), create: gts.entityCreateResolver("Person"), update: gts.entityUpdateResolver("Person"), - delete: gts.entityDeleteResolver("Person") + delete: gts.entityDeleteResolver("Person"), + batch: gts.entityBatchResolver("Person") } } @@ -220,6 +224,7 @@ import Sequelize from "sequelize" name: "CoC Web Technologies", parentUnit: "${uXT.id}", director: "acf34c80-9f83-11e6-8d46-080027e303e4" + members: "acf34c80-9f83-11e6-8d46-080027e303e4" } ) { id initials name @@ -234,6 +239,13 @@ import Sequelize from "sequelize" parentUnit { id name } members { id name } } + q2: Person(where: { + id: "acf34c80-9f83-11e6-8d46-080027e303e4" + }) { + id + name + belongsTo { id name } + } u1: Person(id: "acf34c80-9f83-11e6-8d46-080027e303e4") { update(with: { initials: "XXX", role: "assistant" }) { id initials name role @@ -244,6 +256,154 @@ import Sequelize from "sequelize" id initials name } } + b1: OrgUnit { + batch ( + collection: [ { + op: "CLONE", + type: "OrgUnit", + id: "acf34c80-9f83-11e6-8d47-080027e303e4" + } ] + ) { id } + } + b2: OrgUnit { + batch ( + collection: [ { + op: "CREATE", + type: "OrgUnit", + ref: "$orgId", + id: "48a44b20-f363-11ea-a870-d322e0e3adb0" + with: { + initials: "CoC-UX", + name: "CoC User Experience", + parentUnit: "${uXT.id}", + director: "acf34c80-9f83-11e6-8d46-080027e303e4" + } + }, { + op: "CREATE", + type: "Person", + id: "2e1579e0-f364-11ea-9e92-8bbb944fc462" + with: { + initials: "LZE", + name: "Linda Zeman", + supervisor: "${pRSE.id}", + belongsTo: "$orgId" + } + }, { + op: "CREATE", + type: "Person", + id: "1a74d8e0-f350-11ea-bede-335547113b7d" + with: { + initials: "TSC", + name: "Thomas Schöpf", + supervisor: "${pRSE.id}", + belongsTo: "$orgId" + } + }, { + op: "CREATE", + type: "Person", + with: { + initials: "EWA", + name: "Erwin Wacha", + supervisor: "${pRSE.id}", + belongsTo: "$orgId" + } + } ] + ) { + id initials name parentUnit { id name } director { id name } members { id name } + } + } + b3: OrgUnit { + batch ( + collection: [ { + op: "UPDATE", + type: "OrgUnit", + id: "acf34c80-9f83-11e6-8d47-080027e303e4", + with: { + initials: "CoC-WT2", + name: "CoC Web Technologies 2" + } + }, { + op: "UPDATE", + type: "OrgUnit", + id: "48a44b20-f363-11ea-a870-d322e0e3adb0", + root: true, + with: { + initials: "CoC-UX 2", + name: "CoC User Experience 2" + } + } ] + ) { id initials name } + } + b4: OrgUnit(id: "48a44b20-f363-11ea-a870-d322e0e3adb0") { + batch ( + collection: [ { + op: "UPDATE", + type: "OrgUnit", + id: "48a44b20-f363-11ea-a870-d322e0e3adb0", + with: { + id: "48a44b20-f363-11ea-a870-d322e0e3adb0", + initials: "CoC-AA", + name: "CoC Application Architecture", + } + }, { + op: "CREATE", + type: "Person", + with: { + initials: "LSC", + name: "Lisa Schötz", + supervisor: "${pRSE.id}", + belongsTo: "48a44b20-f363-11ea-a870-d322e0e3adb0" + } + }, { + op: "UPDATE", + type: "Person", + id: "2e1579e0-f364-11ea-9e92-8bbb944fc462", + with: { name: "Linda Elisabeth Zeman" } + }, { + op: "DELETE", + type: "Person", + id: "1a74d8e0-f350-11ea-bede-335547113b7d" + } ] + ) { + id initials name members { id name } + } + } + b5: OrgUnit { + batch ( + collection: [ { + op: "UPDATE", + type: "Person", + id: "2e1579e0-f364-11ea-9e92-8bbb944fc462", + with: { initials: "LTU" } + } ] + ) { id } + } + b6: OrgUnit(id: "48a44b20-f363-11ea-a870-d322e0e3adb0") { + batch ( + collection: [ { + op: "UPDATE", + type: "Person", + id: "2e1579e0-f364-11ea-9e92-8bbb944fc462", + with: { name: "Linda Elisabeth Turke" } + } ] + ) { + id initials name members { id name } + } + } + b7: OrgUnit { + batch ( + collection: [ { + op: "DELETE", + type: "OrgUnit", + id: "acf34c80-9f83-11e6-8d47-080027e303e4" + }, { + op: "DELETE", + type: "OrgUnit", + id: "48a44b20-f363-11ea-a870-d322e0e3adb0", + root: true + } ] + ) { id } + } d1: Person(id: "acf34c80-9f83-11e6-8d46-080027e303e4") { delete } diff --git a/src/gts-3-util-graphql.js b/src/gts-3-util-graphql.js index 05b1efd..56881c2 100644 --- a/src/gts-3-util-graphql.js +++ b/src/gts-3-util-graphql.js @@ -62,7 +62,7 @@ export default class gtsUtilGraphQL { let type = fieldsAll[field].type while (typeof type.ofType === "object") type = type.ofType - if (field.match(/^(?:clone|create|update|delete)$/)) + if (field.match(/^(?:clone|create|update|delete|batch)$/)) fields.method[field] = type.name else if ( type.constructor.name === "GraphQLScalarType" || type.constructor.name === "GraphQLEnumType" ) diff --git a/src/gts-8-entity-create.js b/src/gts-8-entity-create.js index 426ab20..c1a64c8 100644 --- a/src/gts-8-entity-create.js +++ b/src/gts-8-entity-create.js @@ -38,77 +38,83 @@ export default class gtsEntityCreate { if (!(typeof entity === "object" && entity instanceof this._anonCtx && entity.isType(type))) throw new Error(`method "create" only allowed in anonymous ${type} context`) - /* determine fields of entity as defined in GraphQL schema */ - const defined = this._fieldsOfGraphQLType(info, type) - - /* determine fields of entity as requested in GraphQL request */ - const build = this._fieldsOfGraphQLRequest(args, info, type) - - /* handle unique id */ - if (args[this._idname] === undefined) - /* auto-generate the id */ - build.attribute[this._idname] = this._idmake() - else { - /* take over id, but ensure it is unique */ - build.attribute[this._idname] = args[this._idname] - const opts = {} - if (ctx.tx !== undefined) - opts.transaction = ctx.tx - opts.attributes = [ this._idname ] - const existing = await this._models[type].findByPk(build.attribute[this._idname], opts) - if (existing !== null) - throw new Error(`entity ${type}#${build.attribute[this._idname]} already exists`) - } - - /* validate attributes */ - await this._validate(type, build, ctx) - - /* build a new entity */ - const obj = this._models[type].build(build.attribute) - - /* check access to entity before action */ - if (!(await this._authorized("before", "create", type, obj, ctx))) - throw new Error(`will not be allowed to create entity of type "${type}"`) - - /* save new entity */ - const opts = {} - if (ctx.tx !== undefined) - opts.transaction = ctx.tx - const err = await obj.save(opts).catch((err) => err) - if (typeof err === "object" && err instanceof Error) - throw new Error("Sequelize: save: " + err.message + ":" + - err.errors.map((e) => e.message).join("; ")) - - /* post-adjust the relationships according to the request */ - await this._entityUpdateFields(type, obj, - defined.relation, build.relation, ctx, info) - - /* check access to entity after action */ - if (!(await this._authorized("after", "create", type, obj, ctx))) - throw new Error(`was not allowed to create entity of type "${type}"`) - - /* check access to entity again */ - if (!(await this._authorized("after", "read", type, obj, ctx))) - throw new Error(`was not allowed to read (created) entity of type "${type}"`) - - /* map field values */ - this._mapFieldValues(type, obj, ctx, info) - - /* update FTS index */ - this._ftsUpdate(type, obj[this._idname], obj, "create") - - /* trace access */ - await this._trace({ - op: "create", - arity: "one", - dstType: type, - dstIds: [ obj[this._idname] ], - dstAttrs: Object.keys(build.attribute).concat(Object.keys(build.relation)) - }, ctx) + const obj = await this._entityCreate(type, entity, args, ctx, info) /* return new entity */ return obj } } + /* argument args must be of type object and have attribute 'with' */ + async _entityCreate (type, entity, args, ctx, info) { + /* determine fields of entity as defined in GraphQL schema */ + const defined = this._fieldsOfGraphQLType(info, type) + + /* determine fields of entity as requested in GraphQL request */ + const build = this._fieldsOfGraphQLRequest(args, info, type) + + /* handle unique id */ + if (args[this._idname] === undefined) + /* auto-generate the id */ + build.attribute[this._idname] = this._idmake() + else { + /* take over id, but ensure it is unique */ + build.attribute[this._idname] = args[this._idname] + const opts = {} + if (ctx.tx !== undefined) + opts.transaction = ctx.tx + opts.attributes = [ this._idname ] + const existing = await this._models[type].findByPk(build.attribute[this._idname], opts) + if (existing !== null) + throw new Error(`entity ${type}#${build.attribute[this._idname]} already exists`) + } + + /* validate attributes */ + await this._validate(type, build, ctx) + + /* build a new entity */ + const obj = this._models[type].build(build.attribute) + + /* check access to entity before action */ + if (!(await this._authorized("before", "create", type, obj, ctx))) + throw new Error(`will not be allowed to create entity of type "${type}"`) + + /* save new entity */ + const opts = {} + if (ctx.tx !== undefined) + opts.transaction = ctx.tx + const err = await obj.save(opts).catch((err) => err) + if (typeof err === "object" && err instanceof Error) + throw new Error("Sequelize: save: " + err.message + ":" + + err.errors.map((e) => e.message).join("; ")) + + /* post-adjust the relationships according to the request */ + await this._entityUpdateFields(type, obj, + defined.relation, build.relation, ctx, info) + + /* check access to entity after action */ + if (!(await this._authorized("after", "create", type, obj, ctx))) + throw new Error(`was not allowed to create entity of type "${type}"`) + + /* check access to entity again */ + if (!(await this._authorized("after", "read", type, obj, ctx))) + throw new Error(`was not allowed to read (created) entity of type "${type}"`) + + /* map field values */ + this._mapFieldValues(type, obj, ctx, info) + + /* update FTS index */ + this._ftsUpdate(type, obj[this._idname], obj, "create") + + /* trace access */ + await this._trace({ + op: "create", + arity: "one", + dstType: type, + dstIds: [ obj[this._idname] ], + dstAttrs: Object.keys(build.attribute).concat(Object.keys(build.relation)) + }, ctx) + + return obj + } } diff --git a/src/gts-9-entity-clone.js b/src/gts-9-entity-clone.js index 81c69f3..1e8ba22 100644 --- a/src/gts-9-entity-clone.js +++ b/src/gts-9-entity-clone.js @@ -38,61 +38,66 @@ export default class gtsEntityClone { if (typeof entity === "object" && entity instanceof this._anonCtx && entity.isType(type)) throw new Error(`method "clone" only allowed in non-anonymous ${type} context`) - /* determine fields of entity as defined in GraphQL schema */ - const defined = this._fieldsOfGraphQLType(info, type) + const obj = await this._entityClone(type, entity, args, ctx, info) - /* check access to parent entity */ - if (!(await this._authorized("after", "read", type, entity, ctx))) - throw new Error(`not allowed to read entity of type "${type}"`) + /* return new entity */ + return obj + } + } + async _entityClone (type, entity, args, ctx, info) { + /* determine fields of entity as defined in GraphQL schema */ + const defined = this._fieldsOfGraphQLType(info, type) - /* build a new entity */ - const data = {} - data[this._idname] = this._idmake() - Object.keys(defined.attribute).forEach((attr) => { - if (attr !== this._idname) - data[attr] = entity[attr] - }) - const obj = this._models[type].build(data) + /* check access to parent entity */ + if (!(await this._authorized("after", "read", type, entity, ctx))) + throw new Error(`not allowed to read entity of type "${type}"`) - /* check access to entity before action */ - if (!(await this._authorized("before", "create", type, obj, ctx))) - throw new Error(`will not be allowed to clone entity of type "${type}"`) + /* build a new entity */ + const data = {} + data[this._idname] = this._idmake() + Object.keys(defined.attribute).forEach((attr) => { + if (attr !== this._idname) + data[attr] = entity[attr] + }) + const obj = this._models[type].build(data) - /* save new entity */ - const opts = {} - if (ctx.tx !== undefined) - opts.transaction = ctx.tx - const err = await obj.save(opts).catch((err) => err) - if (typeof err === "object" && err instanceof Error) - throw new Error("Sequelize: save: " + err.message + ":" + - err.errors.map((e) => e.message).join("; ")) + /* check access to entity before action */ + if (!(await this._authorized("before", "create", type, obj, ctx))) + throw new Error(`will not be allowed to clone entity of type "${type}"`) - /* check access to entity after action */ - if (!(await this._authorized("after", "create", type, obj, ctx))) - throw new Error(`was not allowed to clone entity of type "${type}"`) + /* save new entity */ + const opts = {} + if (ctx.tx !== undefined) + opts.transaction = ctx.tx + const err = await obj.save(opts).catch((err) => err) + if (typeof err === "object" && err instanceof Error) + throw new Error("Sequelize: save: " + err.message + ":" + + err.errors.map((e) => e.message).join("; ")) - /* check access to entity again */ - if (!(await this._authorized("after", "read", type, obj, ctx))) - throw new Error(`was not allowed to read (cloned) entity of type "${type}"`) + /* check access to entity after action */ + if (!(await this._authorized("after", "create", type, obj, ctx))) + throw new Error(`was not allowed to clone entity of type "${type}"`) - /* map field values */ - this._mapFieldValues(type, obj, ctx, info) + /* check access to entity again */ + if (!(await this._authorized("after", "read", type, obj, ctx))) + throw new Error(`was not allowed to read (cloned) entity of type "${type}"`) - /* update FTS index */ - this._ftsUpdate(type, obj[this._idname], obj, "create") + /* map field values */ + this._mapFieldValues(type, obj, ctx, info) - /* trace access */ - await this._trace({ - op: "create", - arity: "one", - dstType: type, - dstIds: [ obj[this._idname] ], - dstAttrs: Object.keys(data) - }, ctx) + /* update FTS index */ + this._ftsUpdate(type, obj[this._idname], obj, "create") - /* return new entity */ - return obj - } + /* trace access */ + await this._trace({ + op: "create", + arity: "one", + dstType: type, + dstIds: [ obj[this._idname] ], + dstAttrs: Object.keys(data) + }, ctx) + + return obj } } diff --git a/src/gts-A-entity-update.js b/src/gts-A-entity-update.js index 468a0af..5440b12 100644 --- a/src/gts-A-entity-update.js +++ b/src/gts-A-entity-update.js @@ -38,55 +38,59 @@ export default class gtsEntityUpdate { if (typeof entity === "object" && entity instanceof this._anonCtx && entity.isType(type)) throw new Error(`method "update" only allowed in non-anonymous ${type} context`) - /* determine fields of entity as defined in GraphQL schema */ - const defined = this._fieldsOfGraphQLType(info, type) + await this._entityUpdate(type, entity, args, ctx, info) - /* determine fields of entity as requested in GraphQL request */ - const build = this._fieldsOfGraphQLRequest(args, info, type) + /* return updated entity */ + return entity + } + } + /* argument args must be of type object and have attribute 'with' */ + async _entityUpdate (type, entity, args, ctx, info) { + /* determine fields of entity as defined in GraphQL schema */ + const defined = this._fieldsOfGraphQLType(info, type) - /* check access to entity before action */ - if (!(await this._authorized("before", "update", type, entity, ctx))) - throw new Error(`will not be allowed to update entity of type "${type}"`) + /* determine fields of entity as requested in GraphQL request */ + const build = this._fieldsOfGraphQLRequest(args, info, type) - /* validate attributes */ - await this._validate(type, build, ctx) + /* check access to entity before action */ + if (!(await this._authorized("before", "update", type, entity, ctx))) + throw new Error(`will not be allowed to update entity of type "${type}"`) - /* adjust the attributes according to the request */ - const opts = {} - if (ctx.tx !== undefined) - opts.transaction = ctx.tx - await entity.update(build.attribute, opts) + /* validate attributes */ + await this._validate(type, build, ctx) - /* adjust the relationships according to the request */ - await this._entityUpdateFields(type, entity, - defined.relation, build.relation, ctx, info) + /* adjust the attributes according to the request */ + const opts = {} + if (ctx.tx !== undefined) + opts.transaction = ctx.tx + await entity.update(build.attribute, opts) - /* check access to entity after action */ - if (!(await this._authorized("after", "update", type, entity, ctx))) - throw new Error(`was not allowed to update entity of type "${type}"`) + /* adjust the relationships according to the request */ + await this._entityUpdateFields(type, entity, + defined.relation, build.relation, ctx, info) - /* check access to entity again */ - if (!(await this._authorized("after", "read", type, entity, ctx))) - throw new Error(`was not allowed to read (updated) entity of type "${type}"`) + /* check access to entity after action */ + if (!(await this._authorized("after", "update", type, entity, ctx))) + throw new Error(`was not allowed to update entity of type "${type}"`) - /* map field values */ - this._mapFieldValues(type, entity, ctx, info) + /* check access to entity again */ + if (!(await this._authorized("after", "read", type, entity, ctx))) + throw new Error(`was not allowed to read (updated) entity of type "${type}"`) - /* update FTS index */ - this._ftsUpdate(type, entity[this._idname], entity, "update") + /* map field values */ + this._mapFieldValues(type, entity, ctx, info) - /* trace access */ - await this._trace({ - op: "update", - arity: "one", - dstType: type, - dstIds: [ entity[this._idname] ], - dstAttrs: Object.keys(build.attribute).concat(Object.keys(build.relation)) - }, ctx) + /* update FTS index */ + this._ftsUpdate(type, entity[this._idname], entity, "update") - /* return updated entity */ - return entity - } + /* trace access */ + await this._trace({ + op: "update", + arity: "one", + dstType: type, + dstIds: [ entity[this._idname] ], + dstAttrs: Object.keys(build.attribute).concat(Object.keys(build.relation)) + }, ctx) } } diff --git a/src/gts-B-entity-delete.js b/src/gts-B-entity-delete.js index d140e02..d345828 100644 --- a/src/gts-B-entity-delete.js +++ b/src/gts-B-entity-delete.js @@ -38,32 +38,38 @@ export default class gtsEntityDelete { if (typeof entity === "object" && entity instanceof this._anonCtx && entity.isType(type)) throw new Error(`method "delete" only allowed in non-anonymous ${type} context`) - /* check access to target before action */ - if (!(await this._authorized("before", "delete", type, entity, ctx))) - return new Error(`will not be allowed to delete entity of type "${type}"`) - - /* delete the instance */ - const opts = {} - if (ctx.tx !== undefined) - opts.transaction = ctx.tx - const result = entity[this._idname] - await entity.destroy(opts) - - /* update FTS index */ - this._ftsUpdate(type, result, null, "delete") - - /* trace access */ - await this._trace({ - op: "delete", - arity: "one", - dstType: type, - dstIds: [ result ], - dstAttrs: [ "*" ] - }, ctx) + const result = await this._entityDelete(type, entity, args, ctx, info) /* return id of deleted entity */ return result } } + async _entityDelete (type, entity, args, ctx) { + /* check access to target before action */ + if (!(await this._authorized("before", "delete", type, entity, ctx))) + return new Error(`will not be allowed to delete entity of type "${type}"`) + + /* delete the instance */ + const opts = {} + if (ctx.tx !== undefined) + opts.transaction = ctx.tx + const result = entity[this._idname] + await entity.destroy(opts) + + /* update FTS index */ + this._ftsUpdate(type, result, null, "delete") + + /* trace access */ + await this._trace({ + op: "delete", + arity: "one", + dstType: type, + dstIds: [ result ], + dstAttrs: [ "*" ] + }, ctx) + + /* return id of deleted entity */ + return result + } } diff --git a/src/gts-C-entity-batch.js b/src/gts-C-entity-batch.js new file mode 100644 index 0000000..55bd154 --- /dev/null +++ b/src/gts-C-entity-batch.js @@ -0,0 +1,146 @@ +/* +** GraphQL-Tools-Sequelize -- Integration of GraphQL-Tools and Sequelize ORM +** Copyright (c) 2016-2020 Dr. Ralf S. Engelschall +** +** Permission is hereby granted, free of charge, to any person obtaining +** a copy of this software and associated documentation files (the +** "Software"), to deal in the Software without restriction, including +** without limitation the rights to use, copy, modify, merge, publish, +** distribute, sublicense, and/or sell copies of the Software, and to +** permit persons to whom the Software is furnished to do so, subject to +** the following conditions: +** +** The above copyright notice and this permission notice shall be included +** in all copies or substantial portions of the Software. +** +** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +** EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +** MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +** IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +** CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +** TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +** SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +/* the mixin class */ +import Ducky from "ducky" + +export default class gtsEntityBatch { + /* API: batch for a entity or its composition/s */ + entityBatchSchema (type) { + return "" + + `# Run a batch with create, clone, update or delete operation for type [${type}]() or composition entities\n` + + `batch(collection: JSON): ${type}\n` + } + entityBatchResolver (type) { + return async (entity, args, ctx, info) => { + /* sanity check usage context */ + if (info && info.operation && info.operation.operation !== "mutation") + throw new Error("method \"batch\" only allowed under \"mutation\" operation") + + /* check anonymous context (method batch is allowed in anonymous AND non-anonymous context) */ + let isAnonymous = true + if (!(typeof entity === "object" && entity instanceof this._anonCtx && entity.isType(type))) + isAnonymous = false + + /* define validations fpr operations */ + const createValidation = "{ op: string, type: string, id?: string, root?: boolean, ref?: string, with: object }" + const cloneValidation = "{ op: string, type: string, id: string, root?: boolean, ref?: string }" + const updateValidation = "{ op: string, type: string, id: string, root?: boolean, with: object }" + const deleteValidation = "{ op: string, type: string, id: string, root?: boolean }" + + const opts = {} + if (ctx.tx !== undefined) + opts.transaction = ctx.tx + + const refs = {} + const batchArray = args.collection + + for (let i = 0; i < batchArray.length; i++) { + const batchObj = batchArray[i] + + /* Resolve references */ + if (batchObj.with !== undefined) { + for (const ref in refs) { + for (const key in batchObj.with) { + if (batchObj.with[key] === ref) + batchObj.with[key] = refs[ref] + } + } + } + + if (batchObj.op === "CREATE") { + if (!Ducky.validate(batchObj, createValidation)) + throw new Error(`invalid argument for method "batch": argument collection object with type "CREATE" must have the structure: ${createValidation}`) + + /* create a new entity and remember entity */ + batchObj.entity = await this._entityCreate(batchObj.type, entity, batchObj, ctx, info) + + /* if there are references, remember them for resolving later */ + if (batchObj.ref !== undefined) + if (refs[batchObj.ref] === undefined) + refs[batchObj.ref] = batchObj.entity.id + else + throw new Error(`reference "${batchObj.ref}" already exists, but it must be unique in one batch.`) + } + else if (batchObj.op === "CLONE") { + if (!Ducky.validate(batchObj, cloneValidation)) + throw new Error(`invalid argument for method "batch": argument collection object with type "CLONE" must have the structure: ${cloneValidation}`) + + /* find and clone entity and remember entity */ + const entityObj = await this._models[batchObj.type].findByPk(batchObj.id, opts) + batchObj.entity = await this._entityClone(batchObj.type, entityObj, batchObj, ctx, info) + + /* if there are references, remember them for resolving later */ + if (batchObj.ref !== undefined) + if (refs[batchObj.ref] === undefined) + refs[batchObj.ref] = batchObj.entity.id + else + throw new Error(`reference "${batchObj.ref}" already exists, but it must be unique in one batch.`) + } + else if (batchObj.op === "UPDATE") { + if (!Ducky.validate(batchObj, updateValidation)) + throw new Error(`invalid argument for method "batch": argument collection object with type "UPDATE" must have the structure: ${updateValidation}`) + + /* find and update entity and remember entity */ + const entityObj = await this._models[batchObj.type].findByPk(batchObj.id, opts) + await this._entityUpdate(batchObj.type, entityObj, batchObj, ctx, info) + batchObj.entity = entityObj + } + else if (batchObj.op === "DELETE") { + if (!Ducky.validate(batchObj, deleteValidation)) + throw new Error(`invalid argument for method "batch": argument collection object with type "DELETE" must have the structure: ${deleteValidation}`) + + /* find and delete entity and remember id */ + const entityObj = await this._models[batchObj.type].findByPk(batchObj.id, opts) + await this._entityDelete(batchObj.type, entityObj, args, ctx, info) + batchObj.entity = null + } + else + throw new Error(`invalid operation "${batchObj.op}". Operation must be "CREATE", "CLONE", "UPDATE" or "DELETE".`) + } + + /* find result depending on anonymous or non-anonymous context */ + let result = null + if (isAnonymous) { + const root = batchArray.find((batchObj) => { return batchObj.root }) + const firstInArray = batchArray.find((batchObj) => { return batchObj.type === type }) + if (root !== undefined) + result = root.entity + else if (firstInArray !== undefined) + result = firstInArray.entity + } + else { + /* if non-anonymous context result is entity of given context, no matter if it was changed */ + if (batchArray.find((batchObj) => { return batchObj.id === entity.id && batchObj.op === "DELETE" })) + result = null + else + result = await this._models[type].findByPk(entity.id, opts) + } + + /* return result entity */ + return result + } + } +} + diff --git a/src/gts.js b/src/gts.js index ec0d2e5..81ad5df 100644 --- a/src/gts.js +++ b/src/gts.js @@ -38,6 +38,7 @@ import gtsEntityCreate from "./gts-8-entity-create" import gtsEntityClone from "./gts-9-entity-clone" import gtsEntityUpdate from "./gts-A-entity-update" import gtsEntityDelete from "./gts-B-entity-delete" +import gtsEntityBatch from "./gts-C-entity-batch" /* the API class */ class GraphQLToolsSequelize extends aggregation( @@ -51,7 +52,8 @@ class GraphQLToolsSequelize extends aggregation( gtsEntityCreate, gtsEntityClone, gtsEntityUpdate, - gtsEntityDelete + gtsEntityDelete, + gtsEntityBatch ) { constructor (sequelize, options = {}) { super(sequelize, options)