From c935f3f6916efa584e697800f9dac0d9f825ee6e Mon Sep 17 00:00:00 2001 From: Faruk AYDIN Date: Sat, 5 Mar 2022 19:58:52 +0300 Subject: [PATCH] feat: Implement authentication with JWT --- packages/backend/.env-example | 1 + packages/backend/package.json | 4 + packages/backend/src/app.ts | 2 - packages/backend/src/config/app.ts | 2 + .../backend/src/graphql/mutations/login.ts | 27 ++-- packages/backend/src/graphql/types/auth.ts | 12 ++ .../backend/src/helpers/authentication.ts | 37 +++-- .../backend/src/helpers/graphql-instance.ts | 16 ++- yarn.lock | 129 +++++++++++++++++- 9 files changed, 199 insertions(+), 31 deletions(-) create mode 100644 packages/backend/src/graphql/types/auth.ts diff --git a/packages/backend/.env-example b/packages/backend/.env-example index db956b17..7bbadfc4 100644 --- a/packages/backend/.env-example +++ b/packages/backend/.env-example @@ -10,5 +10,6 @@ POSTGRES_USERNAME=automatish_development_user POSTGRES_PASSWORD= POSTGRES_ENABLE_SSL=false ENCRYPTION_KEY=sample-encryption-key +APP_SECRET_KEY=sample-app-secret-key REDIS_PORT=6379 REDIS_HOST=127.0.0.1 diff --git a/packages/backend/package.json b/packages/backend/package.json index 331cd5f6..eaa69b6e 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -32,8 +32,11 @@ "express-graphql": "^0.12.0", "flickr-sdk": "3.10.0", "googleapis": "89.0.0", + "graphql-middleware": "^6.1.15", + "graphql-shield": "^7.5.0", "graphql-type-json": "^0.3.2", "http-errors": "~1.6.3", + "jsonwebtoken": "^8.5.1", "knex": "^0.95.11", "lodash.get": "^4.4.2", "morgan": "^1.10.0", @@ -74,6 +77,7 @@ "@types/crypto-js": "^4.0.2", "@types/express": "^4.17.13", "@types/http-errors": "^1.8.1", + "@types/jsonwebtoken": "^8.5.8", "@types/lodash.get": "^4.4.6", "@types/morgan": "^1.9.3", "@types/node": "^16.10.2", diff --git a/packages/backend/src/app.ts b/packages/backend/src/app.ts index ca5e08dd..656fdb1b 100644 --- a/packages/backend/src/app.ts +++ b/packages/backend/src/app.ts @@ -10,7 +10,6 @@ import appAssetsHandler from './helpers/app-assets-handler'; import webUIHandler from './helpers/web-ui-handler'; import errorHandler from './helpers/error-handler'; import './config/database'; -import authentication from './helpers/authentication'; const app = express(); const port = appConfig.port; @@ -21,7 +20,6 @@ app.use(morgan); app.use(express.json()); app.use(express.urlencoded({ extended: false })); app.use(cors(corsOptions)); -app.use(authentication); app.use('/graphql', graphQLInstance); webUIHandler(app); diff --git a/packages/backend/src/config/app.ts b/packages/backend/src/config/app.ts index af87dfa0..7a2bf655 100644 --- a/packages/backend/src/config/app.ts +++ b/packages/backend/src/config/app.ts @@ -15,6 +15,7 @@ type AppConfig = { postgresEnableSsl: boolean; baseUrl?: string; encryptionKey: string; + appSecretKey: string; serveWebAppSeparately: boolean; redisHost: string; redisPort: number; @@ -33,6 +34,7 @@ const appConfig: AppConfig = { postgresPassword: process.env.POSTGRES_PASSWORD, postgresEnableSsl: process.env.POSTGRES_ENABLE_SSL === 'true' ? true : false, encryptionKey: process.env.ENCRYPTION_KEY, + appSecretKey: process.env.APP_SECRET_KEY, serveWebAppSeparately: process.env.SERVE_WEB_APP_SEPARATELY === 'true' ? true : false, redisHost: process.env.REDIS_HOST || '127.0.0.1', diff --git a/packages/backend/src/graphql/mutations/login.ts b/packages/backend/src/graphql/mutations/login.ts index 1f5810cb..284b3abb 100644 --- a/packages/backend/src/graphql/mutations/login.ts +++ b/packages/backend/src/graphql/mutations/login.ts @@ -1,30 +1,35 @@ import { GraphQLString, GraphQLNonNull } from 'graphql'; import User from '../../models/user'; -import userType from '../types/user'; +import authType from '../types/auth'; +import jwt from 'jsonwebtoken'; +import appConfig from '../../config/app'; type Params = { - email: string, - password: string -} + email: string; + password: string; +}; + const loginResolver = async (params: Params) => { const user = await User.query().findOne({ email: params.email, }); - if (user && await user.login(params.password)) { - return user; + if (user && (await user.login(params.password))) { + const token = jwt.sign({ userId: user.id }, appConfig.appSecretKey); + + return { token, user }; } - throw new Error('User could not be found.') -} + throw new Error('User could not be found.'); +}; const login = { - type: userType, + type: authType, args: { email: { type: GraphQLNonNull(GraphQLString) }, - password: { type: GraphQLNonNull(GraphQLString) } + password: { type: GraphQLNonNull(GraphQLString) }, }, - resolve: (_: any, params: any) => loginResolver(params) + resolve: (_: any, params: any) => loginResolver(params), }; export default login; diff --git a/packages/backend/src/graphql/types/auth.ts b/packages/backend/src/graphql/types/auth.ts new file mode 100644 index 00000000..4d76faac --- /dev/null +++ b/packages/backend/src/graphql/types/auth.ts @@ -0,0 +1,12 @@ +import { GraphQLObjectType, GraphQLString } from 'graphql'; +import UserType from './user'; + +const authType = new GraphQLObjectType({ + name: 'Auth', + fields: { + user: { type: UserType }, + token: { type: GraphQLString }, + }, +}); + +export default authType; diff --git a/packages/backend/src/helpers/authentication.ts b/packages/backend/src/helpers/authentication.ts index f636f8d3..d058b379 100644 --- a/packages/backend/src/helpers/authentication.ts +++ b/packages/backend/src/helpers/authentication.ts @@ -1,14 +1,33 @@ -import { Response, NextFunction } from 'express'; +import { rule, shield, allow } from 'graphql-shield'; +import jwt from 'jsonwebtoken'; import User from '../models/user'; -import RequestWithCurrentUser from '../types/express/request-with-current-user'; +import appConfig from '../config/app'; -const authentication = async (req: RequestWithCurrentUser, _res: Response, next: NextFunction): Promise => { - // We set authentication to use the sample user we created temporarily. - req.currentUser = await User.query().findOne({ - email: 'user@automatisch.com' - }).throwIfNotFound(); +const isAuthenticated = rule()(async (_parent, _args, req) => { + const token = req.headers['authorization']; - next() -} + if (token == null) return false; + + try { + const { userId } = jwt.verify(token, appConfig.appSecretKey) as { + userId: string; + }; + req.currentUser = await User.query().findById(userId).throwIfNotFound(); + + return true; + } catch (error) { + return false; + } +}); + +const authentication = shield({ + Query: { + '*': isAuthenticated, + }, + Mutation: { + '*': isAuthenticated, + login: allow, + }, +}); export default authentication; diff --git a/packages/backend/src/helpers/graphql-instance.ts b/packages/backend/src/helpers/graphql-instance.ts index 2afcaa1e..00ec68f9 100644 --- a/packages/backend/src/helpers/graphql-instance.ts +++ b/packages/backend/src/helpers/graphql-instance.ts @@ -1,18 +1,20 @@ import { graphqlHTTP } from 'express-graphql'; -import graphQLSchema from '../graphql/graphql-schema' +import graphQLSchema from '../graphql/graphql-schema'; import logger from '../helpers/logger'; +import { applyMiddleware } from 'graphql-middleware'; +import authentication from '../helpers/authentication'; const graphQLInstance = graphqlHTTP({ - schema: graphQLSchema, + schema: applyMiddleware(graphQLSchema, authentication), graphiql: true, customFormatErrorFn: (error) => { - logger.error(error.path + ' : ' + error.message + '\n' + error.stack) + logger.error(error.path + ' : ' + error.message + '\n' + error.stack); return { message: error.message, - locations: error.locations - } - } -}) + locations: error.locations, + }; + }, +}); export default graphQLInstance; diff --git a/yarn.lock b/yarn.lock index 7fd41e4c..525c1ed8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1363,6 +1363,13 @@ dependencies: regenerator-runtime "^0.13.4" +"@babel/runtime@^7.10.5": + version "7.17.2" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.2.tgz#66f68591605e59da47523c631416b18508779941" + integrity sha512-hzeyJyMA1YGdJTuWU0e/j4wKXrU4OMFvY2MSlaI9B7VQb0r5cxTE3EAIS2Q7Tn2RIcDkRvTA/v2JsAEhxe99uw== + dependencies: + regenerator-runtime "^0.13.4" + "@babel/template@^7.12.7", "@babel/template@^7.16.7", "@babel/template@^7.3.3": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.16.7.tgz#8d126c8701fde4d66b264b3eba3d96f07666d155" @@ -2087,6 +2094,54 @@ resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.2.tgz#30aa825f11d438671d585bd44e7fd564535fc210" integrity sha512-82cpyJyKRoQoRi+14ibCeGPu0CwypgtBAdBhq1WfvagpCZNKqwXbKwXllYSMG91DhmG4jt9gN8eP6lGOtozuaw== +"@graphql-tools/batch-execute@^8.3.2": + version "8.3.2" + resolved "https://registry.yarnpkg.com/@graphql-tools/batch-execute/-/batch-execute-8.3.2.tgz#8b5a731d5343f0147734f12d480aafde2a1b6eba" + integrity sha512-ICWqM+MvEkIPHm18Q0cmkvm134zeQMomBKmTRxyxMNhL/ouz6Nqld52/brSlaHnzA3fczupeRJzZ0YatruGBcQ== + dependencies: + "@graphql-tools/utils" "^8.6.2" + dataloader "2.0.0" + tslib "~2.3.0" + value-or-promise "1.0.11" + +"@graphql-tools/delegate@^8.5.1": + version "8.5.1" + resolved "https://registry.yarnpkg.com/@graphql-tools/delegate/-/delegate-8.5.1.tgz#3d146cc3bb74935116d3f4bddb3affdf14a9712d" + integrity sha512-/YPmVxitt57F8sH50pnfXASzOOjEfaUDkX48eF5q6f16+JBncej2zeu+Zm2c68q8MbIxhPlEGfpd0QZeqTvAxw== + dependencies: + "@graphql-tools/batch-execute" "^8.3.2" + "@graphql-tools/schema" "^8.3.2" + "@graphql-tools/utils" "^8.6.2" + dataloader "2.0.0" + graphql-executor "0.0.18" + tslib "~2.3.0" + value-or-promise "1.0.11" + +"@graphql-tools/merge@^8.2.3": + version "8.2.3" + resolved "https://registry.yarnpkg.com/@graphql-tools/merge/-/merge-8.2.3.tgz#a2861fec230ee7be9dc42d72fed2ac075c31669f" + integrity sha512-XCSmL6/Xg8259OTWNp69B57CPWiVL69kB7pposFrufG/zaAlI9BS68dgzrxmmSqZV5ZHU4r/6Tbf6fwnEJGiSw== + dependencies: + "@graphql-tools/utils" "^8.6.2" + tslib "~2.3.0" + +"@graphql-tools/schema@^8.3.2": + version "8.3.2" + resolved "https://registry.yarnpkg.com/@graphql-tools/schema/-/schema-8.3.2.tgz#5b949d7a2cc3936f73507d91cc609996f1266d11" + integrity sha512-77feSmIuHdoxMXRbRyxE8rEziKesd/AcqKV6fmxe7Zt+PgIQITxNDew2XJJg7qFTMNM43W77Ia6njUSBxNOkwg== + dependencies: + "@graphql-tools/merge" "^8.2.3" + "@graphql-tools/utils" "^8.6.2" + tslib "~2.3.0" + value-or-promise "1.0.11" + +"@graphql-tools/utils@^8.6.2": + version "8.6.2" + resolved "https://registry.yarnpkg.com/@graphql-tools/utils/-/utils-8.6.2.tgz#095408135f091aac68fe18a0a21b708e685500da" + integrity sha512-x1DG0cJgpJtImUlNE780B/dfp8pxvVxOD6UeykFH5rHes26S4kGokbgU8F1IgrJ1vAPm/OVBHtd2kicTsPfwdA== + dependencies: + tslib "~2.3.0" + "@graphql-typed-document-node/core@^3.0.0": version "3.1.1" resolved "https://registry.yarnpkg.com/@graphql-typed-document-node/core/-/core-3.1.1.tgz#076d78ce99822258cf813ecc1e7fa460fa74d052" @@ -4166,6 +4221,13 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4= +"@types/jsonwebtoken@^8.5.8": + version "8.5.8" + resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-8.5.8.tgz#01b39711eb844777b7af1d1f2b4cf22fda1c0c44" + integrity sha512-zm6xBQpFDIDM6o9r6HSgDeIcLy82TKWctCXEPbJJcXb5AKmi5BNNdLXneixK4lplX3PqIVcwLBCGE/kAGnlD4A== + dependencies: + "@types/node" "*" + "@types/lodash.get@^4.4.6": version "4.4.6" resolved "https://registry.yarnpkg.com/@types/lodash.get/-/lodash.get-4.4.6.tgz#0c7ac56243dae0f9f09ab6f75b29471e2e777240" @@ -4451,6 +4513,11 @@ dependencies: "@types/yargs-parser" "*" +"@types/yup@0.29.11": + version "0.29.11" + resolved "https://registry.yarnpkg.com/@types/yup/-/yup-0.29.11.tgz#d654a112973f5e004bf8438122bd7e56a8e5cd7e" + integrity sha512-9cwk3c87qQKZrT251EDoibiYRILjCmxBvvcb4meofCmx1vdnNcR9gyildy5vOHASpOKMsn42CugxUvcwK5eu1g== + "@typescript-eslint/eslint-plugin@^4.31.2": version "4.33.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.33.0.tgz#c24dc7c8069c7706bc40d99f6fa87edcb2005276" @@ -7180,6 +7247,11 @@ data-urls@^2.0.0: whatwg-mimetype "^2.3.0" whatwg-url "^8.0.0" +dataloader@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/dataloader/-/dataloader-2.0.0.tgz#41eaf123db115987e21ca93c005cd7753c55fe6f" + integrity sha512-YzhyDAwA4TaQIhM5go+vCLmU0UikghC/t9DTQYZR2M/UvZ1MdOhPezSDZcjj9uqQJOMqjLcpWtyW2iNINdlatQ== + date-fns@^2.16.1: version "2.28.0" resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.28.0.tgz#9570d656f5fc13143e50c975a3b6bbeb46cd08b2" @@ -9442,6 +9514,28 @@ graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.5 resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.9.tgz#041b05df45755e587a24942279b9d113146e1c96" integrity sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ== +graphql-executor@0.0.18: + version "0.0.18" + resolved "https://registry.yarnpkg.com/graphql-executor/-/graphql-executor-0.0.18.tgz#6aa4b39e1ca773e159c2a602621e90606df0109a" + integrity sha512-upUSl7tfZCZ5dWG1XkOvpG70Yk3duZKcCoi/uJso4WxJVT6KIrcK4nZ4+2X/hzx46pL8wAukgYHY6iNmocRN+g== + +graphql-middleware@^6.1.15: + version "6.1.15" + resolved "https://registry.yarnpkg.com/graphql-middleware/-/graphql-middleware-6.1.15.tgz#d59ccb6e21db5d1e22a8a00d332f277794990280" + integrity sha512-JiLuIM48EE3QLcr79K0VCCHqMt6c23esLlkZv2Nr9a/yHnv6eU9DKV9eXARl+wV9m4LkT9ZCg4cIamIa9vPidQ== + dependencies: + "@graphql-tools/delegate" "^8.5.1" + "@graphql-tools/schema" "^8.3.2" + +graphql-shield@^7.5.0: + version "7.5.0" + resolved "https://registry.yarnpkg.com/graphql-shield/-/graphql-shield-7.5.0.tgz#aa3af226946946dfadac33eccc6cbe7fec6e9000" + integrity sha512-T1A6OreOe/dHDk/1Qg3AHCrKLmTkDJ3fPFGYpSOmUbYXyDnjubK4J5ab5FjHdKHK5fWQRZNTvA0SrBObYsyfaw== + dependencies: + "@types/yup" "0.29.11" + object-hash "^2.0.3" + yup "^0.31.0" + graphql-tag@^2.12.3: version "2.12.6" resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-2.12.6.tgz#d441a569c1d2537ef10ca3d1633b48725329b5f1" @@ -11655,6 +11749,11 @@ locate-path@^6.0.0: dependencies: p-locate "^5.0.0" +lodash-es@^4.17.11: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee" + integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw== + lodash._reinterpolate@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d" @@ -12941,7 +13040,7 @@ object-assign@^4, object-assign@^4.1.0, object-assign@^4.1.1: resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= -object-hash@^2.2.0: +object-hash@^2.0.3, object-hash@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-2.2.0.tgz#5ad518581eefc443bd763472b8ff2e9c2c0d54a5" integrity sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw== @@ -14562,6 +14661,11 @@ propagate@^2.0.0: resolved "https://registry.yarnpkg.com/propagate/-/propagate-2.0.1.tgz#40cdedab18085c792334e64f0ac17256d38f9a45" integrity sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag== +property-expr@^2.0.4: + version "2.0.5" + resolved "https://registry.yarnpkg.com/property-expr/-/property-expr-2.0.5.tgz#278bdb15308ae16af3e3b9640024524f4dc02cb4" + integrity sha512-IJUkICM5dP5znhCckHSv30Q4b5/JA5enCtkRHYaOVOAocnH/1BQEYTC5NMfT3AVl/iXKdr3aqQbQn9DxyWknwA== + property-information@^5.0.0, property-information@^5.3.0: version "5.6.0" resolved "https://registry.yarnpkg.com/property-information/-/property-information-5.6.0.tgz#61675545fb23002f245c6540ec46077d4da3ed69" @@ -17190,6 +17294,11 @@ toidentifier@1.0.1: resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== +toposort@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/toposort/-/toposort-2.0.2.tgz#ae21768175d1559d48bef35420b2f4962f09c330" + integrity sha1-riF2gXXRVZ1IvvNUILL0li8JwzA= + totalist@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/totalist/-/totalist-1.1.0.tgz#a4d65a3e546517701e3e5c37a47a70ac97fe56df" @@ -17333,7 +17442,7 @@ tslib@^1.14.1, tslib@^1.8.1, tslib@^1.9.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== -tslib@^2, tslib@^2.0.0, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.3.0, tslib@^2.3.1: +tslib@^2, tslib@^2.0.0, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.3.0, tslib@^2.3.1, tslib@~2.3.0: version "2.3.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01" integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw== @@ -17898,6 +18007,11 @@ value-equal@^1.0.1: resolved "https://registry.yarnpkg.com/value-equal/-/value-equal-1.0.1.tgz#1e0b794c734c5c0cade179c437d356d931a34d6c" integrity sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw== +value-or-promise@1.0.11: + version "1.0.11" + resolved "https://registry.yarnpkg.com/value-or-promise/-/value-or-promise-1.0.11.tgz#3e90299af31dd014fe843fe309cefa7c1d94b140" + integrity sha512-41BrgH+dIbCFXClcSapVs5M6GkENd3gQOJpEfPDNa71LsUGMXDL0jMWpI/Rh7WhX+Aalfz2TTS3Zt5pUsbnhLg== + vary@^1, vary@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" @@ -18822,6 +18936,17 @@ yosay@^2.0.2: taketalk "^1.0.0" wrap-ansi "^2.0.0" +yup@^0.31.0: + version "0.31.1" + resolved "https://registry.yarnpkg.com/yup/-/yup-0.31.1.tgz#0954cb181161f397b804346037a04f8a4b31599e" + integrity sha512-Lf6648jDYOWR75IlWkVfwesPyW6oj+50NpxlKvsQlpPsB8eI+ndI7b4S1VrwbmeV9hIZDu1MzrlIL4W+gK1jPw== + dependencies: + "@babel/runtime" "^7.10.5" + lodash "^4.17.20" + lodash-es "^4.17.11" + property-expr "^2.0.4" + toposort "^2.0.2" + zen-observable-ts@^1.2.0: version "1.2.3" resolved "https://registry.yarnpkg.com/zen-observable-ts/-/zen-observable-ts-1.2.3.tgz#c2f5ccebe812faf0cfcde547e6004f65b1a6d769"