Merge branch 'master' into improve_seed_data

This commit is contained in:
Grzegorz Leoniec 2018-10-23 12:23:19 +02:00 committed by GitHub
commit 9be0d4f688
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 4022 additions and 65 deletions

View File

@ -6,6 +6,7 @@
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "./node_modules/.bin/nodemon --exec babel-node src/index.js",
"start:debug": "./node_modules/.bin/nodemon --exec babel-node --inspect=0.0.0.0:9229 src/index.js",
"seedDb": "./node_modules/.bin/babel-node src/seed/seed-db.js"
},
"author": "Grzegorz Leoniec",
@ -19,14 +20,14 @@
"bcryptjs": "^2.4.3",
"dotenv": "^6.0.0",
"graphql-custom-directives": "^0.2.13",
"graphql-middleware": "^1.7.6",
"graphql-middleware": "1.7.6",
"graphql-tag": "^2.9.2",
"graphql-yoga": "^1.16.2",
"graphql-yoga": "1.16.2",
"jsonwebtoken": "^8.3.0",
"lodash": "^4.17.11",
"ms": "^2.1.1",
"neo4j-driver": "^1.6.1",
"neo4j-graphql-js": "^1.0.2",
"neo4j-graphql-js": "1.0.4",
"node-fetch": "^2.1.2",
"passport": "^0.4.0",
"passport-jwt": "^4.0.0",

View File

@ -4,17 +4,81 @@ import path from 'path'
import bcrypt from 'bcryptjs'
import zipObject from 'lodash/zipObject'
import generateJwt from './jwt/generateToken'
import values from 'lodash/values'
import { fixUrl } from './middleware/fixImageUrlsMiddleware'
export const typeDefs =
fs.readFileSync(process.env.GRAPHQL_SCHEMA || path.join(__dirname, "schema.graphql"))
.toString('utf-8')
const query = (cypher, session) => {
return new Promise((resolve, reject) => {
let data = []
session
.run(cypher)
.subscribe({
onNext: function (record) {
let item = {}
record.keys.forEach(key => {
item[key] = record.get(key)
})
data.push(item)
},
onCompleted: function () {
session.close()
resolve(data)
},
onError: function (error) {
reject(error)
}
})
})
}
const queryOne = (cypher, session) => {
return new Promise((resolve, reject) => {
query(cypher, session)
.then(res => {
resolve(res.length ? res.pop() : {})
})
.catch(err => {
reject(err)
})
})
}
export const resolvers = {
Query: {
isLoggedIn: (parent, args, { user }) => {
console.log(user)
isLoggedIn: (parent, args, { driver, user }) => {
return Boolean(user && user.id)
},
statistics: async (parent, args, { driver, user }) => {
return new Promise(async (resolve) => {
const session = driver.session()
const queries = {
countUsers: `MATCH (r:User) WHERE r.deleted <> true OR NOT exists(r.deleted) RETURN COUNT(r) AS countUsers`,
countPosts: `MATCH (r:Post) WHERE r.deleted <> true OR NOT exists(r.deleted) RETURN COUNT(r) AS countPosts`,
countComments: `MATCH (r:Comment) WHERE r.deleted <> true OR NOT exists(r.deleted) RETURN COUNT(r) AS countComments`,
countNotifications: `MATCH (r:Notification) WHERE r.deleted <> true OR NOT exists(r.deleted) RETURN COUNT(r) AS countNotifications`,
countOrganizations: `MATCH (r:Organization) WHERE r.deleted <> true OR NOT exists(r.deleted) RETURN COUNT(r) AS countOrganizations`,
countProjects: `MATCH (r:Project) WHERE r.deleted <> true OR NOT exists(r.deleted) RETURN COUNT(r) AS countProjects`,
countInvites: `MATCH (r:Invite) WHERE r.wasUsed <> true OR NOT exists(r.wasUsed) RETURN COUNT(r) AS countInvites`,
countFollows: `MATCH (:User)-[r:FOLLOWS]->(:User) RETURN COUNT(r) AS countFollows`,
countShouts: `MATCH (:User)-[r:SHOUTED]->(:Post) RETURN COUNT(r) AS countShouts`
}
let data = {
countUsers: (await queryOne(queries.countUsers, session)).countUsers,
countPosts: (await queryOne(queries.countPosts, session)).countPosts,
countComments: (await queryOne(queries.countComments, session)).countComments,
countNotifications: (await queryOne(queries.countNotifications, session)).countNotifications,
countOrganizations: (await queryOne(queries.countOrganizations, session)).countOrganizations,
countProjects: (await queryOne(queries.countProjects, session)).countProjects,
countInvites: (await queryOne(queries.countInvites, session)).countInvites,
countFollows: (await queryOne(queries.countFollows, session)).countFollows,
countShouts: (await queryOne(queries.countShouts, session)).countShouts
}
resolve(data)
})
}
// usersBySubstring: neo4jgraphql
},
@ -53,9 +117,11 @@ export const resolvers = {
token: generateJwt(u)
})
}
session.close()
throw new Error('Incorrect password.')
}
session.close()
throw new Error('No Such User exists.')
}
}

View File

@ -4,15 +4,14 @@ import ms from 'ms'
// Generate an Access Token for the given User ID
export default function generateJwt(user) {
console.log('generateJwt', user)
const token = jwt.sign(user, process.env.JWT_SECRET, {
expiresIn: ms('1d'),
issuer: process.env.GRAPHQL_URI,
audience: process.env.CLIENT_URI,
subject: user.id.toString()
})
jwt.verify(token, process.env.JWT_SECRET, (err, data) => {
console.log('token verification:', err, data)
})
// jwt.verify(token, process.env.JWT_SECRET, (err, data) => {
// console.log('token verification:', err, data)
// })
return token
}

View File

@ -18,7 +18,6 @@ export default () => {
return new Strategy(options,
(JWTPayload, next) => {
console.log('JWT Payload Received:', JWTPayload)
// usually this would be a database call:
// var user = users[_.findIndex(users, {id: JWTPayload.id})]
if (true) {

View File

@ -1,14 +1,17 @@
export const fixUrl = (url) => {
return url.replace('https://api-alpha.human-connection.org/uploads', 'http://localhost:3000/uploads')
return url.replace('https://api-alpha.human-connection.org', 'http://localhost:3000')
}
const fixImageURLs = (result, resolve, root, args, context, info) => {
if (result && typeof result === 'string' && result.indexOf('https://api-alpha.human-connection.org/uploads') === 0) {
const fixImageURLs = (result, recursive) => {
if (result && typeof result === 'string' && result.indexOf('https://api-alpha.human-connection.org') === 0) {
result = fixUrl(result)
} else if (result && Array.isArray(result)) {
result.forEach((res, index) => {
result[index] = fixImageURLs(result[index], true)
})
} else if (result && typeof result === 'object') {
Object.keys(result).forEach(key => {
result[key] = fixImageURLs(result[key])
result[key] = fixImageURLs(result[key], true)
})
}
return result
@ -17,11 +20,10 @@ const fixImageURLs = (result, resolve, root, args, context, info) => {
export default {
Mutation: async (resolve, root, args, context, info) => {
const result = await resolve(root, args, context, info)
return fixImageURLs(result, resolve, root, args, context, info)
return fixImageURLs(result)
},
Query: async (resolve, root, args, context, info) => {
let result = await resolve(root, args, context, info)
return fixImageURLs(result, resolve, root, args, context, info)
return fixImageURLs(result)
}
}

View File

@ -9,7 +9,7 @@ export default {
},
Query: async (resolve, root, args, context, info) => {
const result = await resolve(root, args, context, info)
if (result.password) {
if (result && result.password) {
result.password = '*****'
}
return result

View File

@ -4,17 +4,29 @@ export default {
if (typeof args.deleted !== 'boolean') {
args.deleted = false
}
const result = await resolve(root, args, context, info)
return result
},
Comment: async (resolve, root, args, context, info) => {
if (typeof args.deleted !== 'boolean') {
args.deleted = false
if (typeof args.disabled !== 'boolean') {
args.disabled = false
}
const result = await resolve(root, args, context, info)
return result
},
Comment: async (resolve, root, args, context, info) => {
// if (typeof args.deleted !== 'boolean') {
// args.deleted = false
// }
// if (typeof args.disabled !== 'boolean') {
// args.disabled = false
// }
const result = await resolve(root, args, context, info)
return result
},
User: async (resolve, root, args, context, info) => {
if (typeof args.deleted !== 'boolean') {
args.deleted = false
}
if (typeof args.disabled !== 'boolean') {
args.disabled = false
}
// console.log('ROOT', root)
// console.log('ARGS', args)
// console.log('CONTEXT', context)

View File

@ -1,5 +1,6 @@
type Query {
isLoggedIn: Boolean!
statistics: Statistics!
}
type Mutation {
login(email: String!, password: String!): LoggedInUser
@ -11,10 +12,22 @@ type LoggedInUser {
name: String!
avatar:String!
email: String!
role: String!,
role: String!
token: String!
}
type Statistics {
countUsers: Int!
countPosts: Int!
countComments: Int!
countNotifications: Int!
countOrganizations: Int!
countProjects: Int!
countInvites: Int!
countFollows: Int!
countShouts: Int!
}
enum VisibilityEnum {
Public
Friends
@ -44,13 +57,13 @@ type WrittenComment @relation(name: "WROTE") {
type User {
id: ID!
name: String @default(to: "Anonymus")
name: String
email: String
slug: String
password: String!
avatar: String
deleted: Boolean @default(to: false)
disabled: Boolean @default(to: false)
deleted: Boolean
disabled: Boolean
role: UserGroupEnum
friends: [User]! @relation(name: "FRIENDS", direction: "BOTH")
@ -62,11 +75,12 @@ type User {
followedBy: [User]! @relation(name: "FOLLOWS", direction: "IN")
followedByCount: Int! @cypher(statement: "MATCH (this)<-[:FOLLOWS]-(r:User) RETURN COUNT(r)")
contributions: [WrittenPost]!
#contributions: [WrittenPost]!
#contributions2(first: Int = 10, offset: Int = 0): [WrittenPost2]!
# @cypher(
# statement: "MATCH (this)-[w:WROTE]->(p:Post) RETURN p as Post, w.timestamp as timestamp"
# )
contributions: [Post]! @relation(name: "WROTE", direction: "OUT")
contributionsCount: Int! @cypher(statement: """
MATCH (this)-[:WROTE]->(r:Post)
WHERE (NOT exists(r.deleted) OR r.deleted = false)
@ -75,10 +89,10 @@ type User {
)
comments: [WrittenComment]!
commentsCount: Int! @cypher(statement: "MATCH (this)-[:WROTE]->(r:Comment) RETURN COUNT(r)")
commentsCount: Int! @cypher(statement: "MATCH (this)-[:WROTE]->(r:Comment) WHERE NOT r.deleted = true AND NOT r.disabled = true RETURN COUNT(r)")
shouted: [Post]! @relation(name: "SHOUTED", direction: "OUT")
shoutedCount: Int! @cypher(statement: "MATCH (this)-[:SHOUTED]->(r:Post) RETURN COUNT(r)")
shoutedCount: Int! @cypher(statement: "MATCH (this)-[:SHOUTED]->(r:Post) WHERE NOT r.deleted = true AND NOT r.disabled = true RETURN COUNT(r)")
organizationsCreated: [Organization] @relation(name: "CREATED_ORGA", direction: "OUT")
organizationsOwned: [Organization] @relation(name: "OWNING_ORGA", direction: "OUT")
@ -98,8 +112,10 @@ type Post {
contentExcerpt: String
image: String
visibility: VisibilityEnum
deleted: Boolean @default(to: false)
disabled: Boolean @default(to: false)
deleted: Boolean
disabled: Boolean
createdAt: String
updatedAt: String
relatedContributions: [Post]! @cypher(statement: """
MATCH (this)-[:TAGGED|CATEGORIZED]->(categoryOrTag)<-[:TAGGED|CATEGORIZED]-(post:Post)
@ -114,7 +130,7 @@ type Post {
commentsCount: Int! @cypher(statement: "MATCH (this)<-[:COMMENTS]-(r:Comment) RETURN COUNT(r)")
shoutedBy: [User]! @relation(name: "SHOUTED", direction: "IN")
shoutedCount: Int! @cypher(statement: "MATCH (this)<-[:SHOUTED]-(r:User) RETURN COUNT(r)")
shoutedCount: Int! @cypher(statement: "MATCH (this)<-[:SHOUTED]-(r:User) WHERE NOT r.deleted = true AND NOT r.disabled = true RETURN COUNT(r)")
}
type Comment {
@ -123,8 +139,8 @@ type Comment {
content: String!
contentExcerpt: String
post: Post @relation(name: "COMMENT", direction: "OUT")
deleted: Boolean @default(to: false)
disabled: Boolean @default(to: false)
deleted: Boolean
disabled: Boolean
}
type Category {
@ -147,12 +163,12 @@ type Badge {
}
enum BadgeTypeEnum {
Role
Crowdfunding
role
crowdfunding
}
enum BadgeStatusEnum {
Permanent
Temorary
permanent
temorary
}
type Organization {
@ -163,8 +179,8 @@ type Organization {
slug: String
description: String!
descriptionExcerpt: String
deleted: Boolean @default(to: false)
disabled: Boolean @default(to: false)
deleted: Boolean
disabled: Boolean
tags: [Tag]! @relation(name: "TAGGED", direction: "OUT")
categories: [Category]! @relation(name: "CATEGORIZED", direction: "OUT")
@ -176,5 +192,5 @@ type Tag {
taggedPosts: [Post]! @relation(name: "TAGGED", direction: "IN")
taggedOrganizations: [Organization]! @relation(name: "TAGGED", direction: "IN")
taggedCount: Int! @cypher(statement: "MATCH (this)<-[:TAGGED]-(r) RETURN COUNT(r)")
deleted: Boolean @default(to: false)
deleted: Boolean
}

View File

@ -4,28 +4,28 @@ export default `
mutation {
# Users
u1: CreateUser(id: "u1", name: "Peter Lustig", password: "1234", email: "admin@example.org", avatar: "${faker.internet.avatar()}", role: Admin) {
u1: CreateUser(id: "u1", name: "Peter Lustig", password: "1234", email: "admin@example.org", avatar: "${faker.internet.avatar()}", role: admin) {
id
name
email
avatar
role
}
u2: CreateUser(id: "u2", name: "Bob der Bausmeister", password: "1234", email: "moderator@example.org", avatar: "${faker.internet.avatar()}", role: Moderator) {
u2: CreateUser(id: "u2", name: "Bob der Bausmeister", password: "1234", email: "moderator@example.org", avatar: "${faker.internet.avatar()}", role: moderator) {
id
name
email
avatar
role
}
u3: CreateUser(id: "u3", name: "Jenny Rostock", password: "1234", email: "user@example.org", avatar: "${faker.internet.avatar()}", role: Admin) {
u3: CreateUser(id: "u3", name: "Jenny Rostock", password: "1234", email: "user@example.org", avatar: "${faker.internet.avatar()}", role: user) {
id
name
email
avatar
role
}
u4: CreateUser(id: "u4", name: "Angie Banjie", password: "1234", email: "Angie_Banjie@yahoo.com", avatar: "${faker.internet.avatar()}", role: User) {
u4: CreateUser(id: "u4", name: "Angie Banjie", password: "1234", email: "angie@example.org", avatar: "${faker.internet.avatar()}", role: user) {
id
name
email
@ -36,12 +36,12 @@ export default `
u1_blacklist_u4: AddUserBlacklisted(from: { id: "u1" }, to: { id: "u4" }) { from { id } }
# Badges
b1: CreateBadge(id: "b1", key: "indiegogo_en_racoon", type: Crowdfunding, status: Permanent, icon: "indiegogo_en_racoon") { id }
b2: CreateBadge(id: "b2", key: "indiegogo_en_rabbit", type: Crowdfunding, status: Permanent, icon: "indiegogo_en_rabbit") { id }
b3: CreateBadge(id: "b3", key: "indiegogo_en_wolf", type: Crowdfunding, status: Permanent, icon: "indiegogo_en_wolf") { id }
b4: CreateBadge(id: "b4", key: "indiegogo_en_bear", type: Crowdfunding, status: Permanent, icon: "indiegogo_en_bear") { id }
b5: CreateBadge(id: "b5", key: "indiegogo_en_turtle", type: Crowdfunding, status: Permanent, icon: "indiegogo_en_turtle") { id }
b6: CreateBadge(id: "b6", key: "indiegogo_en_rhino", type: Crowdfunding, status: Permanent, icon: "indiegogo_en_rhino") { id }
b1: CreateBadge(id: "b1", key: "indiegogo_en_racoon", type: Crowdfunding, status: permanent, icon: "indiegogo_en_racoon") { id }
b2: CreateBadge(id: "b2", key: "indiegogo_en_rabbit", type: Crowdfunding, status: permanent, icon: "indiegogo_en_rabbit") { id }
b3: CreateBadge(id: "b3", key: "indiegogo_en_wolf", type: Crowdfunding, status: permanent, icon: "indiegogo_en_wolf") { id }
b4: CreateBadge(id: "b4", key: "indiegogo_en_bear", type: Crowdfunding, status: permanent, icon: "indiegogo_en_bear") { id }
b5: CreateBadge(id: "b5", key: "indiegogo_en_turtle", type: Crowdfunding, status: permanent, icon: "indiegogo_en_turtle") { id }
b6: CreateBadge(id: "b6", key: "indiegogo_en_rhino", type: Crowdfunding, status: permanent, icon: "indiegogo_en_rhino") { id }
b1_u1: AddUserBadges(from: {id: "b1"}, to: {id: "u1"}) { from { id } }
b2_u1: AddUserBadges(from: {id: "b2"}, to: {id: "u1"}) { from { id } }
@ -72,7 +72,7 @@ export default `
p1: CreatePost(
id: "p1",
title: "Gedanken eines Polizisten zum Einsatz im Hambacher Forst",
content: "<p><strong>Diese Zukunftsstadt ist real und keine Computer-Animation s</strong>ondern sie ist das Lebenswerk des mittlerweile über 100 Jahre alten Futuristen und Architekten Jacque Fresco aus Florida. In 35 Jahren (seit seinem 13. Lebensjahr) hat dieser zusammen mit seiner Frau seinen futuristischen Traum von einer besonderen Zukunftsstadt auf 85.000 Quadratmetern realisiert. In den Gebäuden und Gärten befinden sich u.a. ein Forschungszentrum, Vortragsräume und unzählige seiner Modelle &amp; Architekturentwürfe.</p><br /><p>Sein zentrales Anliegen ist eine resourcenbasierte Wirtschaft und die Abschaffung von Geld und Privatbesitz. Mit Hilfe von Roboterarbeit und dem Bedingungslosen Grundeinkommen (da nach seiner Ansicht in den kommenden Jahren fast alle Jobs automatisiert werden), möchte er eine ökologische Landwirtschaft mit Permakulturen etc. und eine effiziente Energiegewinnung (ausschließlich durch regenerative Energien) schaffen. Wenige kompatible Formen in einer sparsamen Modulbauweise (in die u.a. bereits variable Service- und Reparaturelemente integriert sind) sollen insgesamt eine soziale &amp; ökologische Utopie im Einklang mit der Natur ermöglichen.</p><br /><p>Nachfolgend der Direkt-Link auf den interessanten Artikel von Zoltan Istvan, der den Architekten und seine Frau in Florida besuchen durfte und seinen Artikel Ende 2016 auf „MOTHERBOARD“ veröffentlicht hatte:</p><br /><p>https://motherboard.vice.com/de/article/vv34nb/ich-habe-die-zukunft-besucht-in-der-wir-ohne-geld-steuern-und-besitz-leben </p><br /><p>Da soll noch jemand behaupten, es gäbe keine Utopien mehr bzw. keine Futuristen, die ihre kreativen und zukunftsfähigen Ideen (auch in ganz großem Stil) selbst in die Tat umsetzen. LG @all :) </p><br /><p><strong>Wir sind eine Menschheitsfamilie. • Wir sind eins. • Wir sind HUMAN CONNECTION</strong> ❤️</p>",
content: "<p><strong>Diese Zukunftsstadt ist real und keine Computer-Animation</strong> sondern sie ist das Lebenswerk des mittlerweile über 100 Jahre alten Futuristen und Architekten Jacque Fresco aus Florida. In 35 Jahren (seit seinem 13. Lebensjahr) hat dieser zusammen mit seiner Frau seinen futuristischen Traum von einer besonderen Zukunftsstadt auf 85.000 Quadratmetern realisiert. In den Gebäuden und Gärten befinden sich u.a. ein Forschungszentrum, Vortragsräume und unzählige seiner Modelle &amp; Architekturentwürfe.</p><br /><p>Sein zentrales Anliegen ist eine resourcenbasierte Wirtschaft und die Abschaffung von Geld und Privatbesitz. Mit Hilfe von Roboterarbeit und dem Bedingungslosen Grundeinkommen (da nach seiner Ansicht in den kommenden Jahren fast alle Jobs automatisiert werden), möchte er eine ökologische Landwirtschaft mit Permakulturen etc. und eine effiziente Energiegewinnung (ausschließlich durch regenerative Energien) schaffen. Wenige kompatible Formen in einer sparsamen Modulbauweise (in die u.a. bereits variable Service- und Reparaturelemente integriert sind) sollen insgesamt eine soziale &amp; ökologische Utopie im Einklang mit der Natur ermöglichen.</p><br /><p>Nachfolgend der Direkt-Link auf den interessanten Artikel von Zoltan Istvan, der den Architekten und seine Frau in Florida besuchen durfte und seinen Artikel Ende 2016 auf „MOTHERBOARD“ veröffentlicht hatte:</p><br /><p>https://motherboard.vice.com/de/article/vv34nb/ich-habe-die-zukunft-besucht-in-der-wir-ohne-geld-steuern-und-besitz-leben </p><br /><p>Da soll noch jemand behaupten, es gäbe keine Utopien mehr bzw. keine Futuristen, die ihre kreativen und zukunftsfähigen Ideen (auch in ganz großem Stil) selbst in die Tat umsetzen. LG @all :) </p><br /><p><strong>Wir sind eine Menschheitsfamilie. • Wir sind eins. • Wir sind HUMAN CONNECTION</strong> ❤️</p>",
image: "https://picsum.photos/1280/1024?image=352",
visibility: Public,
disabled: false,

3862
yarn-error.log Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1904,11 +1904,11 @@ graphql-middleware@1.6.6:
dependencies:
graphql-tools "^3.0.5"
graphql-middleware@^1.7.6:
version "1.7.7"
resolved "https://registry.yarnpkg.com/graphql-middleware/-/graphql-middleware-1.7.7.tgz#0a7a7193a873c4769401df2aef4ffb9c6ca97f43"
graphql-middleware@1.7.6:
version "1.7.6"
resolved "https://registry.yarnpkg.com/graphql-middleware/-/graphql-middleware-1.7.6.tgz#f226bf6671f3d82a9378f8b335804c8e44d21733"
dependencies:
graphql-tools "^4.0.1"
graphql-tools "^4.0.0"
graphql-playground-html@1.5.5:
version "1.5.5"
@ -1960,9 +1960,9 @@ graphql-tools@^3.0.0, graphql-tools@^3.0.4, graphql-tools@^3.0.5:
iterall "^1.1.3"
uuid "^3.1.0"
graphql-tools@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/graphql-tools/-/graphql-tools-4.0.1.tgz#c995a4e25c2967d108c975e508322d12969c8c0e"
graphql-tools@^4.0.0:
version "4.0.2"
resolved "https://registry.yarnpkg.com/graphql-tools/-/graphql-tools-4.0.2.tgz#9da22974cc6bf6524ed4f4af35556fd15aa6516d"
dependencies:
apollo-link "^1.2.3"
apollo-utilities "^1.0.1"
@ -1970,7 +1970,7 @@ graphql-tools@^4.0.1:
iterall "^1.1.3"
uuid "^3.1.0"
graphql-yoga@^1.16.2:
graphql-yoga@1.16.2:
version "1.16.2"
resolved "https://registry.yarnpkg.com/graphql-yoga/-/graphql-yoga-1.16.2.tgz#083293a9cecab6283e883c5a482c5c920fa66585"
dependencies:
@ -2709,7 +2709,7 @@ neo4j-driver@^1.6.1:
babel-runtime "^6.18.0"
uri-js "^4.2.1"
neo4j-graphql-js@^1.0.2:
neo4j-graphql-js@1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/neo4j-graphql-js/-/neo4j-graphql-js-1.0.4.tgz#250bd44024f1505c726d2fc4ab27a35a1fc5330b"
dependencies: