├── .dockerignore ├── src ├── rock │ ├── models │ │ ├── binary-files │ │ │ ├── queries.js │ │ │ ├── mutations.js │ │ │ ├── schema.js │ │ │ ├── index.js │ │ │ ├── resolver.js │ │ │ ├── tables.js │ │ │ └── model.js │ │ ├── campuses │ │ │ ├── queries.js │ │ │ ├── index.js │ │ │ ├── schema.js │ │ │ ├── model.js │ │ │ ├── tables.js │ │ │ └── resolver.js │ │ ├── likes │ │ │ ├── mutations.js │ │ │ ├── queries.js │ │ │ ├── schema.js │ │ │ ├── index.js │ │ │ ├── resolver.js │ │ │ ├── __tests__ │ │ │ │ └── resolver.spec.js │ │ │ └── model.js │ │ ├── feeds │ │ │ ├── index.js │ │ │ ├── __tests__ │ │ │ │ ├── queries.js │ │ │ │ └── __snapshots__ │ │ │ │ │ ├── queries.js.snap │ │ │ │ │ └── resolver.spec.js.snap │ │ │ ├── queries.js │ │ │ └── resolver.js │ │ ├── finances │ │ │ ├── __tests__ │ │ │ │ ├── queries.js │ │ │ │ └── __snapshots__ │ │ │ │ │ └── queries.js.snap │ │ │ ├── util │ │ │ │ ├── __tests__ │ │ │ │ │ ├── language.js │ │ │ │ │ ├── __snapshots__ │ │ │ │ │ │ ├── submit-transaction.js.snap │ │ │ │ │ │ └── language.js.snap │ │ │ │ │ ├── statement.js │ │ │ │ │ └── formatTransaction.js │ │ │ │ ├── logError.js │ │ │ │ ├── language.js │ │ │ │ └── nmi.js │ │ │ ├── index.js │ │ │ ├── model.js │ │ │ ├── queries.js │ │ │ ├── models │ │ │ │ ├── FinancialAccount.js │ │ │ │ ├── FinancialBatch.js │ │ │ │ ├── __tests__ │ │ │ │ │ ├── __snapshots__ │ │ │ │ │ │ └── Transaction.js.snap │ │ │ │ │ └── FinancialBatch.js │ │ │ │ └── ScheduledTransaction.js │ │ │ ├── mutations.js │ │ │ └── schema.js │ │ ├── groups │ │ │ ├── __tests__ │ │ │ │ ├── mutations.js │ │ │ │ └── __snapshots__ │ │ │ │ │ └── mutations.js.snap │ │ │ ├── mutations.js │ │ │ ├── index.js │ │ │ ├── queries.js │ │ │ └── schema.js │ │ ├── people │ │ │ ├── __tests__ │ │ │ │ ├── mutations.js │ │ │ │ └── __snapshots__ │ │ │ │ │ └── mutations.js.snap │ │ │ ├── queries.js │ │ │ ├── mutations.js │ │ │ ├── index.js │ │ │ ├── schema.js │ │ │ └── tables.js │ │ ├── system │ │ │ ├── queries.js │ │ │ ├── __tests__ │ │ │ │ ├── __snapshots__ │ │ │ │ │ └── resolver.spec.js.snap │ │ │ │ ├── fieldTypeResolvers.js │ │ │ │ └── resolver.spec.js │ │ │ ├── index.js │ │ │ ├── schema.js │ │ │ ├── resolver.js │ │ │ └── fieldTypeResolvers.js │ │ └── index.js │ └── index.js ├── util │ ├── scalars │ │ └── Date │ │ │ ├── schema.js │ │ │ ├── index.js │ │ │ └── resolver.js │ ├── index.js │ ├── node │ │ ├── index.js │ │ ├── schema.js │ │ ├── resolver.js │ │ ├── __test__ │ │ │ ├── queries.integration.js │ │ │ ├── resolver.spec.js │ │ │ └── model.spec.js │ │ └── model.js │ ├── cache │ │ ├── index.js │ │ ├── defaults.js │ │ ├── __test__ │ │ │ ├── mutations.integration.js │ │ │ ├── index.spec.js │ │ │ └── memory-cache.spec.js │ │ └── memory-cache.js │ ├── model.js │ ├── __tests__ │ │ └── heighliner.spec.js │ └── heighliner.js ├── apollos │ ├── models │ │ └── users │ │ │ ├── queries.js │ │ │ ├── README.md │ │ │ ├── index.js │ │ │ ├── makeNewGuid.js │ │ │ ├── mutations.js │ │ │ ├── schema.js │ │ │ ├── __tests__ │ │ │ ├── queries.integration.js │ │ │ └── model.spec.js │ │ │ ├── api.js │ │ │ └── resolver.js │ ├── README.md │ ├── index.js │ ├── __tests__ │ │ └── mongo.spec.js │ └── mongo.js ├── expression-engine │ ├── models │ │ ├── ee │ │ │ ├── index.js │ │ │ ├── snippets.js │ │ │ ├── sites.js │ │ │ ├── playa.js │ │ │ ├── tags.js │ │ │ ├── matrix.js │ │ │ ├── model.js │ │ │ └── __tests__ │ │ │ │ └── model.spec.js │ │ ├── navigation │ │ │ ├── queries.js │ │ │ ├── schema.js │ │ │ ├── index.js │ │ │ ├── resolver.js │ │ │ ├── tables.js │ │ │ └── model.js │ │ ├── files │ │ │ ├── index.js │ │ │ ├── resolver.js │ │ │ ├── schema.js │ │ │ └── tables.js │ │ ├── content │ │ │ ├── README.md │ │ │ ├── index.js │ │ │ ├── __tests__ │ │ │ │ ├── __snapshots__ │ │ │ │ │ └── model.js.snap │ │ │ │ └── images.spec.js │ │ │ ├── queries.js │ │ │ ├── images.js │ │ │ ├── schema.js │ │ │ └── tables.js │ │ └── index.js │ ├── README.md │ ├── index.js │ └── mysql.js ├── esv │ ├── models │ │ └── scripture │ │ │ ├── queries.js │ │ │ ├── schema.js │ │ │ ├── __tests__ │ │ │ ├── model.js │ │ │ └── resolver.js │ │ │ ├── resolver.js │ │ │ ├── index.js │ │ │ └── model.js │ ├── __tests__ │ │ ├── __snapshots__ │ │ │ └── fetch.js.snap │ │ └── fetch.js │ ├── index.js │ └── fetch.js ├── __mocks__ │ ├── redis.js │ └── bull.js ├── google-site-search │ ├── models │ │ ├── geolocate │ │ │ ├── queries.js │ │ │ ├── index.js │ │ │ ├── model.js │ │ │ ├── schema.js │ │ │ └── resolver.js │ │ └── search │ │ │ ├── queries.js │ │ │ ├── index.js │ │ │ ├── model.js │ │ │ ├── schema.js │ │ │ ├── __test__ │ │ │ └── queries.integration.js │ │ │ └── resolver.js │ ├── README.md │ ├── index.js │ ├── fetch.js │ └── __test__ │ │ └── fetch.spec.js ├── constants.js ├── routes │ ├── graphiql.js │ ├── engine.js │ ├── errors.js │ ├── util.js │ ├── cache.js │ └── datadog.js ├── server.js └── __tests__ │ └── server.integration.js ├── .travis ├── QA.md ├── heighliner.enc ├── run_for_coverage ├── ssh-config └── run_on_pull_requests ├── .gitignore ├── .babelrc ├── scripts ├── preprocessor.js ├── graphql-transformer.js └── commit.js ├── .eslintrc ├── docker-compose.yml ├── .env.example ├── .vscode ├── launch.json └── settings.json ├── Dockerfile ├── webpack.config.js ├── .travis.yml ├── README.md └── package.json /.dockerignore: -------------------------------------------------------------------------------- 1 | data 2 | scripts 3 | tmp 4 | 5 | -------------------------------------------------------------------------------- /src/rock/models/binary-files/queries.js: -------------------------------------------------------------------------------- 1 | export default []; 2 | -------------------------------------------------------------------------------- /src/util/scalars/Date/schema.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | ` 3 | scalar Date 4 | ` 5 | ]; 6 | -------------------------------------------------------------------------------- /src/apollos/models/users/queries.js: -------------------------------------------------------------------------------- 1 | export default ["currentUser: User", "topics: [String]"]; 2 | -------------------------------------------------------------------------------- /src/expression-engine/models/ee/index.js: -------------------------------------------------------------------------------- 1 | import { EE } from "./model"; 2 | 3 | export { EE }; 4 | -------------------------------------------------------------------------------- /.travis/QA.md: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | # QA Checklist 4 | 5 | - [ ] do something 6 | - [ ] do something else 7 | -------------------------------------------------------------------------------- /.travis/heighliner.enc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NewSpring/Heighliner/HEAD/.travis/heighliner.enc -------------------------------------------------------------------------------- /src/esv/models/scripture/queries.js: -------------------------------------------------------------------------------- 1 | export default ["scripture(query: String!): ESVScripture"]; 2 | -------------------------------------------------------------------------------- /src/rock/models/binary-files/mutations.js: -------------------------------------------------------------------------------- 1 | export default ["attachPhotoIdToUser(id: ID!): Boolean"]; 2 | -------------------------------------------------------------------------------- /src/rock/models/campuses/queries.js: -------------------------------------------------------------------------------- 1 | export default ["campuses(id: Int, name: String): [Campus]"]; 2 | -------------------------------------------------------------------------------- /src/expression-engine/models/navigation/queries.js: -------------------------------------------------------------------------------- 1 | export default ["navigation(nav: String!): [Navigation]"]; 2 | -------------------------------------------------------------------------------- /src/rock/models/likes/mutations.js: -------------------------------------------------------------------------------- 1 | export default ["toggleLike(nodeId: String!): LikesMutationResponse"]; 2 | -------------------------------------------------------------------------------- /src/__mocks__/redis.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line 2 | import redis from "redis-mock"; 3 | export default redis; 4 | -------------------------------------------------------------------------------- /src/__mocks__/bull.js: -------------------------------------------------------------------------------- 1 | export default jest.fn(() => ({ 2 | process: jest.fn(() => {}), 3 | add: jest.fn(() => {}) 4 | })); 5 | -------------------------------------------------------------------------------- /src/esv/models/scripture/schema.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | ` 3 | type ESVScripture { 4 | html: String 5 | } 6 | ` 7 | ]; 8 | -------------------------------------------------------------------------------- /src/google-site-search/models/geolocate/queries.js: -------------------------------------------------------------------------------- 1 | export default ["geolocate(origin: String, destinations: String): GGeoLocate"]; 2 | -------------------------------------------------------------------------------- /src/util/scalars/Date/index.js: -------------------------------------------------------------------------------- 1 | export { default as resolver } from "./resolver"; 2 | export { default as schema } from "./schema"; 3 | -------------------------------------------------------------------------------- /src/util/scalars/Date/resolver.js: -------------------------------------------------------------------------------- 1 | import GraphQLDate from "graphql-date"; 2 | 3 | export default { 4 | Date: GraphQLDate 5 | }; 6 | -------------------------------------------------------------------------------- /src/rock/models/binary-files/schema.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | ` 3 | type BinaryFile implements Node { 4 | id: ID! 5 | } 6 | ` 7 | ]; 8 | -------------------------------------------------------------------------------- /.travis/run_for_coverage: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | yarn coverage 5 | coveralls < ./coverage/lcov.info || true # ignore coveralls error 6 | 7 | -------------------------------------------------------------------------------- /src/google-site-search/models/search/queries.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | "search(query: String!, first: Int = 10, after: Int = 1, site: String): SSSearch" 3 | ]; 4 | -------------------------------------------------------------------------------- /src/rock/models/feeds/index.js: -------------------------------------------------------------------------------- 1 | import resolvers from "./resolver"; 2 | import queries from "./queries"; 3 | 4 | export default { 5 | resolvers, 6 | queries 7 | }; 8 | -------------------------------------------------------------------------------- /src/rock/models/finances/__tests__/queries.js: -------------------------------------------------------------------------------- 1 | import queries from "../queries"; 2 | 3 | it("has all needed queries", () => { 4 | expect(queries).toMatchSnapshot(); 5 | }); 6 | -------------------------------------------------------------------------------- /src/rock/models/likes/queries.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | `recentlyLiked( 3 | limit: Int = 20, 4 | skip: Int = 0, 5 | cache: Boolean = true 6 | ): [Node]` 7 | ]; 8 | -------------------------------------------------------------------------------- /src/util/index.js: -------------------------------------------------------------------------------- 1 | import { parseGlobalId, createGlobalId } from "./node"; 2 | import { Heighliner } from "./model"; 3 | 4 | export { createGlobalId, parseGlobalId, Heighliner }; 5 | -------------------------------------------------------------------------------- /src/rock/models/finances/util/__tests__/language.js: -------------------------------------------------------------------------------- 1 | import codes from "../language"; 2 | 3 | it("should match expected codes", () => { 4 | expect(codes).toMatchSnapshot(); 5 | }); 6 | -------------------------------------------------------------------------------- /src/rock/models/groups/__tests__/mutations.js: -------------------------------------------------------------------------------- 1 | import mutations from "../mutations"; 2 | 3 | it("should contain mutations", () => { 4 | expect(mutations).toMatchSnapshot(); 5 | }); 6 | -------------------------------------------------------------------------------- /src/rock/models/people/__tests__/mutations.js: -------------------------------------------------------------------------------- 1 | import mutations from "../mutations"; 2 | 3 | it("should have all needed mutations", () => { 4 | expect(mutations).toMatchSnapshot(); 5 | }); 6 | -------------------------------------------------------------------------------- /src/rock/models/system/queries.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | `definedValues( 3 | id: Int!, 4 | limit: Int = 20, 5 | skip: Int = 0, 6 | all: Boolean = false, 7 | ): [DefinedValue]` 8 | ]; 9 | -------------------------------------------------------------------------------- /src/rock/models/groups/mutations.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | `requestGroupInfo( 3 | groupId: ID!, 4 | message: String!, 5 | communicationPreference: String!, 6 | ): GroupsMutationResponse` 7 | ]; 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | data/* 2 | /.remote 3 | .env 4 | .env.prod 5 | .env.dev 6 | npm-debug.log 7 | coverage 8 | tmp 9 | .tmp 10 | typings 11 | lib 12 | dist 13 | .nyc_output 14 | node_modules 15 | coverage.lcov 16 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-0"], 3 | "plugins": ["transform-runtime", "transform-react-jsx"], 4 | "env": { 5 | "development": { 6 | "sourceMaps": "inline" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.travis/ssh-config: -------------------------------------------------------------------------------- 1 | Host github.com 2 | User git 3 | IdentityFile ~/.ssh/id_rsa 4 | StrictHostKeyChecking no 5 | PasswordAuthentication no 6 | CheckHostIP no 7 | BatchMode yes 8 | -------------------------------------------------------------------------------- /src/rock/models/feeds/__tests__/queries.js: -------------------------------------------------------------------------------- 1 | import Queries from "../queries"; 2 | 3 | describe("Feeds Queries", () => { 4 | it("has all needed queries", () => { 5 | expect(Queries).toMatchSnapshot(); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /src/rock/models/feeds/queries.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | `userFeed( 3 | filters: [String]! 4 | limit: Int = 20, 5 | skip: Int = 0, 6 | status: String = "open", 7 | cache: Boolean = true 8 | ): [Node]` 9 | ]; 10 | -------------------------------------------------------------------------------- /src/rock/models/likes/schema.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | ` 3 | type LikesMutationResponse implements MutationResponse { 4 | error: String 5 | success: Boolean! 6 | code: Int 7 | like: Node 8 | } 9 | ` 10 | ]; 11 | -------------------------------------------------------------------------------- /src/util/node/index.js: -------------------------------------------------------------------------------- 1 | import schema from "./schema"; 2 | import resolver from "./resolver"; 3 | import model, { createGlobalId, parseGlobalId } from "./model"; 4 | 5 | export { schema, resolver, model, createGlobalId, parseGlobalId }; 6 | -------------------------------------------------------------------------------- /src/util/node/schema.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | ` 3 | interface Node { 4 | id: ID! 5 | } 6 | 7 | interface MutationResponse { 8 | error: String 9 | success: Boolean! 10 | code: Int 11 | } 12 | ` 13 | ]; 14 | -------------------------------------------------------------------------------- /scripts/preprocessor.js: -------------------------------------------------------------------------------- 1 | // fileTransformer.js 2 | // const path = require("path"); 3 | const fs = require("fs"); 4 | 5 | module.exports = { 6 | process(src) { 7 | console.log(src); 8 | return src; 9 | }, 10 | }; 11 | 12 | -------------------------------------------------------------------------------- /src/esv/models/scripture/__tests__/model.js: -------------------------------------------------------------------------------- 1 | // { ESV } errors out for some reason 2 | import ESV from "../model"; 3 | 4 | it("should expose the get method", () => { 5 | const esv = new ESV.ESV(); 6 | expect(esv.get).toBeDefined(); 7 | }); 8 | -------------------------------------------------------------------------------- /src/rock/models/people/queries.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | "people(email: String): [Person]", 3 | 4 | "person(guid: ID): Person", 5 | 6 | "currentPerson(cache: Boolean = true): Person", 7 | 8 | "currentFamily: [GroupMember]" 9 | ]; 10 | -------------------------------------------------------------------------------- /src/esv/models/scripture/resolver.js: -------------------------------------------------------------------------------- 1 | export default { 2 | Query: { 3 | scripture(_, { query }, { models }) { 4 | return models.ESV.get(query); 5 | } 6 | }, 7 | 8 | ESVScripture: { 9 | html: data => data 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /src/util/node/resolver.js: -------------------------------------------------------------------------------- 1 | 2 | export default { 3 | Query: { 4 | node: (_, { id }, { models }) => models.Node.get(id), 5 | }, 6 | Node: { 7 | __resolveType: ({ __type }, _, { schema }) => schema.getType(__type), 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /src/rock/models/system/__tests__/__snapshots__/resolver.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Attribute has id 1`] = `"6e705ae05557e2a6d5121b432daf07ab"`; 4 | 5 | exports[`AttributeValue has id 1`] = `"6e705ae05557e2a6d5121b432daf07ab"`; 6 | -------------------------------------------------------------------------------- /src/expression-engine/models/files/index.js: -------------------------------------------------------------------------------- 1 | import { gql } from "../../../util"; 2 | import schema from "./schema"; 3 | import resolvers from "./resolver"; 4 | import models from "./model"; 5 | 6 | export default { 7 | schema, 8 | resolvers, 9 | models 10 | }; 11 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/prefer-default-export 2 | export const FOLLOWABLE_TOPICS = [ 3 | "Articles", 4 | "Devotionals", 5 | "Events", // These are forced 6 | "Music", 7 | "News", 8 | "Series", 9 | "Sermons", 10 | "Stories", 11 | "Studies" 12 | ]; 13 | -------------------------------------------------------------------------------- /scripts/graphql-transformer.js: -------------------------------------------------------------------------------- 1 | // fileTransformer.js 2 | const path = require("path"); 3 | const fs = require("fs"); 4 | 5 | module.exports = { 6 | process(src) { 7 | return fs.readSync(src, "utf8"); 8 | }, 9 | getCacheKey(fileData, filename) { 10 | return filename; 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /src/expression-engine/models/navigation/schema.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | ` 3 | type Navigation implements Node { 4 | id: ID! 5 | text: String 6 | link: String 7 | absoluteLink: String 8 | sort: Int 9 | image: String 10 | children: [Navigation] 11 | } 12 | ` 13 | ]; 14 | -------------------------------------------------------------------------------- /src/apollos/models/users/README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | NewSpring Church 4 | 5 |

6 | 7 | User Type 8 | ======================= 9 | -------------------------------------------------------------------------------- /src/apollos/README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | NewSpring Church 4 | 5 |

6 | 7 | Apollos Application Library 8 | ======================= 9 | -------------------------------------------------------------------------------- /src/expression-engine/README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | NewSpring Church 4 | 5 |

6 | 7 | Expression Engine 8 | ======================= 9 | -------------------------------------------------------------------------------- /src/esv/models/scripture/index.js: -------------------------------------------------------------------------------- 1 | import { gql } from "../../../util"; 2 | import schema from "./schema"; 3 | import resolvers from "./resolver"; 4 | import models from "./model"; 5 | import queries from "./queries"; 6 | 7 | export default { 8 | schema, 9 | resolvers, 10 | models, 11 | queries 12 | }; 13 | -------------------------------------------------------------------------------- /src/expression-engine/models/content/README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | NewSpring Church 4 | 5 |

6 | 7 | Connector Type 8 | ======================= 9 | -------------------------------------------------------------------------------- /src/google-site-search/README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | NewSpring Church 4 | 5 |

6 | 7 | Apollos Application Library 8 | ======================= 9 | -------------------------------------------------------------------------------- /src/google-site-search/models/search/index.js: -------------------------------------------------------------------------------- 1 | import { gql } from "../../../util"; 2 | import schema from "./schema"; 3 | import resolvers from "./resolver"; 4 | import models from "./model"; 5 | import queries from "./queries"; 6 | 7 | export default { 8 | schema, 9 | resolvers, 10 | models, 11 | queries 12 | }; 13 | -------------------------------------------------------------------------------- /src/rock/models/campuses/index.js: -------------------------------------------------------------------------------- 1 | import schema from "./schema"; 2 | import resolvers from "./resolver"; 3 | import models from "./model"; 4 | import queries from "./queries"; 5 | // import mocks from "./mocks"; 6 | 7 | export default { 8 | schema, 9 | resolvers, 10 | models, 11 | queries 12 | // mocks, 13 | }; 14 | -------------------------------------------------------------------------------- /src/rock/models/people/mutations.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | "setPhoneNumber(phoneNumber: String!): PhoneNumberMutationResponse", 3 | "saveDeviceRegistrationId(registrationId: String!, uuid: String!): DeviceRegistrationMutationResponse", 4 | "setPersonAttribute(key: String!, value: String!): AttributeValueMutationResponse" 5 | ]; 6 | -------------------------------------------------------------------------------- /src/apollos/models/users/index.js: -------------------------------------------------------------------------------- 1 | import schema from "./schema"; 2 | import resolvers from "./resolver"; 3 | import models from "./model"; 4 | import queries from "./queries"; 5 | import mutations from "./mutations"; 6 | 7 | export default { 8 | schema, 9 | resolvers, 10 | models, 11 | queries, 12 | mutations 13 | }; 14 | -------------------------------------------------------------------------------- /src/expression-engine/models/navigation/index.js: -------------------------------------------------------------------------------- 1 | import { gql } from "../../../util"; 2 | import schema from "./schema"; 3 | import resolvers from "./resolver"; 4 | import models from "./model"; 5 | import queries from "./queries"; 6 | 7 | export default { 8 | schema, 9 | resolvers, 10 | models, 11 | queries 12 | }; 13 | -------------------------------------------------------------------------------- /src/google-site-search/models/geolocate/index.js: -------------------------------------------------------------------------------- 1 | import { gql } from "../../../util"; 2 | import schema from "./schema"; 3 | import resolvers from "./resolver"; 4 | import models from "./model"; 5 | import queries from "./queries"; 6 | 7 | export default { 8 | schema, 9 | resolvers, 10 | models, 11 | queries 12 | }; 13 | -------------------------------------------------------------------------------- /src/rock/models/groups/index.js: -------------------------------------------------------------------------------- 1 | import schema from "./schema"; 2 | import resolvers from "./resolver"; 3 | import models from "./model"; 4 | import queries from "./queries"; 5 | import mutations from "./mutations"; 6 | 7 | export default { 8 | schema, 9 | resolvers, 10 | models, 11 | queries, 12 | mutations 13 | }; 14 | -------------------------------------------------------------------------------- /src/rock/models/likes/index.js: -------------------------------------------------------------------------------- 1 | import schema from "./schema"; 2 | import models from "./model"; 3 | import mutations from "./mutations"; 4 | import resolvers from "./resolver"; 5 | import queries from "./queries"; 6 | 7 | export default { 8 | schema, 9 | models, 10 | mutations, 11 | resolvers, 12 | queries 13 | }; 14 | -------------------------------------------------------------------------------- /src/rock/models/binary-files/index.js: -------------------------------------------------------------------------------- 1 | import schema from "./schema"; 2 | import resolvers from "./resolver"; 3 | import models from "./model"; 4 | import queries from "./queries"; 5 | import mutations from "./mutations"; 6 | 7 | export default { 8 | schema, 9 | resolvers, 10 | models, 11 | queries, 12 | mutations 13 | }; 14 | -------------------------------------------------------------------------------- /src/rock/models/groups/__tests__/__snapshots__/mutations.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`should contain mutations 1`] = ` 4 | Array [ 5 | "requestGroupInfo( 6 | groupId: ID!, 7 | message: String!, 8 | communicationPreference: String!, 9 | ): GroupsMutationResponse", 10 | ] 11 | `; 12 | -------------------------------------------------------------------------------- /src/rock/models/people/index.js: -------------------------------------------------------------------------------- 1 | 2 | import schema from "./schema"; 3 | import resolvers from "./resolver"; 4 | import models from "./model"; 5 | import mutations from "./mutations"; 6 | import queries from "./queries"; 7 | 8 | export default { 9 | schema, 10 | resolvers, 11 | models, 12 | queries, 13 | mutations, 14 | }; 15 | -------------------------------------------------------------------------------- /src/routes/graphiql.js: -------------------------------------------------------------------------------- 1 | import { graphiqlExpress } from "apollo-server-express"; 2 | 3 | export default app => { 4 | if (process.env.SENTRY_ENVIRONMENT !== "production") { 5 | app.use("/view", graphiqlExpress({ endpointURL: "/graphql" })); 6 | app.use("/graphql/view", graphiqlExpress({ endpointURL: "/graphql" })); 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /src/rock/models/finances/util/__tests__/__snapshots__/submit-transaction.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`logs an error if missing a schedule in Rock 1`] = ` 4 | Array [ 5 | " 6 | Scheduled Transaction is missing for person 1 with 7 | GatewayScheduleId of 19 8 | ", 9 | ] 10 | `; 11 | -------------------------------------------------------------------------------- /src/apollos/models/users/makeNewGuid.js: -------------------------------------------------------------------------------- 1 | function s4() { 2 | return Math.floor((1 + Math.random()) * 0x10000) 3 | .toString(16) 4 | .substring(1); 5 | } 6 | 7 | function makeNewGuid() { 8 | const guid = `${s4()}${s4()}-${s4()}-${s4()}-${s4()}-${s4()}${s4()}${s4()}`; 9 | return guid.toUpperCase(); 10 | } 11 | 12 | export default makeNewGuid; 13 | -------------------------------------------------------------------------------- /src/rock/models/system/index.js: -------------------------------------------------------------------------------- 1 | import models, { Rock } from "./model"; 2 | import schema from "./schema"; 3 | import resolvers from "./resolver"; 4 | import queries from "./queries"; 5 | // import mocks from "./mocks"; 6 | 7 | export default { 8 | schema, 9 | resolvers, 10 | models, 11 | queries 12 | // mocks, 13 | }; 14 | 15 | export { Rock }; 16 | -------------------------------------------------------------------------------- /src/esv/__tests__/__snapshots__/fetch.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`ESV \`ESVFetchConnector\` should format the query 1`] = `"http://www.esvapi.org/v2/rest/passageQuery?key=test-key&passage=john 3:16&include-headings=false&include-passage-references=false&include-footnotes=false&include-audio-link=false&include-short-copyright=false"`; 4 | -------------------------------------------------------------------------------- /src/expression-engine/models/content/index.js: -------------------------------------------------------------------------------- 1 | import { gql } from "../../../util"; 2 | import schema from "./schema"; 3 | import resolvers from "./resolver"; 4 | import models from "./model"; 5 | import tables from "./tables"; 6 | import queries from "./queries"; 7 | 8 | export default { 9 | tables, 10 | schema, 11 | resolvers, 12 | models, 13 | queries 14 | }; 15 | -------------------------------------------------------------------------------- /src/rock/models/feeds/__tests__/__snapshots__/queries.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Feeds Queries has all needed queries 1`] = ` 4 | Array [ 5 | "userFeed( 6 | filters: [String]! 7 | limit: Int = 20, 8 | skip: Int = 0, 9 | status: String = \\"open\\", 10 | cache: Boolean = true 11 | ): [Node]", 12 | ] 13 | `; 14 | -------------------------------------------------------------------------------- /src/rock/models/finances/index.js: -------------------------------------------------------------------------------- 1 | import schema from "./schema"; 2 | import resolvers from "./resolver"; 3 | import models from "./model"; 4 | import queries from "./queries"; 5 | import mutations from "./mutations"; 6 | // import mocks from "./mocks"; 7 | 8 | export default { 9 | schema, 10 | resolvers, 11 | models, 12 | queries, 13 | mutations 14 | // mocks, 15 | }; 16 | -------------------------------------------------------------------------------- /src/rock/models/finances/model.js: -------------------------------------------------------------------------------- 1 | import Transaction from "./models/Transaction"; 2 | import ScheduledTransaction from "./models/ScheduledTransaction"; 3 | import SavedPayment from "./models/SavedPayment"; 4 | import FinancialAccount from "./models/FinancialAccount"; 5 | 6 | export default { 7 | Transaction, 8 | ScheduledTransaction, 9 | SavedPayment, 10 | FinancialAccount 11 | }; 12 | -------------------------------------------------------------------------------- /src/esv/index.js: -------------------------------------------------------------------------------- 1 | import { connect } from "./fetch"; 2 | 3 | import { createApplication } from "../util/heighliner"; 4 | 5 | import Scripture from "./models/scripture"; 6 | 7 | export const { schema, resolvers, models, queries, mocks } = createApplication([ 8 | Scripture 9 | ]); 10 | 11 | export default { 12 | models, 13 | resolvers, 14 | mocks, 15 | schema, 16 | connect 17 | }; 18 | -------------------------------------------------------------------------------- /src/apollos/index.js: -------------------------------------------------------------------------------- 1 | import { connect } from "./mongo"; 2 | import Users from "./models/users"; 3 | 4 | import { createApplication } from "../util/heighliner"; 5 | 6 | export const { 7 | schema, 8 | resolvers, 9 | models, 10 | queries, 11 | mutations 12 | } = createApplication([Users]); 13 | 14 | export default { 15 | models, 16 | resolvers, 17 | schema, 18 | connect, 19 | mutations 20 | }; 21 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": ["airbnb-base","prettier"], 4 | "env": { 5 | "node": true, 6 | "es6": true, 7 | "jest": true 8 | }, 9 | "globals": { 10 | }, 11 | "rules": { 12 | "quotes": [2, "double"], 13 | "no-underscore-dangle": 0, 14 | "class-methods-use-this": 0, 15 | "import/no-extraneous-dependencies": [1], 16 | "no-useless-escape": 0 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/routes/engine.js: -------------------------------------------------------------------------------- 1 | import { Engine } from "apollo-engine"; 2 | 3 | const apolloEngine = new Engine({ 4 | engineConfig: { 5 | apiKey: process.env.ENGINE_API_KEY 6 | }, 7 | graphqlPort: process.env.PORT 8 | }); 9 | 10 | if (process.env.NODE_ENV === "production") apolloEngine.start(); 11 | 12 | export default app => { 13 | if (process.env.NODE_ENV === "production") { 14 | app.use(apolloEngine.expressMiddleware()); 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /src/google-site-search/models/search/model.js: -------------------------------------------------------------------------------- 1 | import { GoogleConnector } from "../../fetch"; 2 | 3 | class SSearch extends GoogleConnector { 4 | cx = process.env.SEARCH_CX; 5 | key = process.env.SEARCH_KEY; 6 | url = process.env.SEARCH_URL; 7 | 8 | query(query) { 9 | const endpoint = `${this.url}key=${this.key}&cx=${this.cx}&q=${query}`; 10 | return this.get(endpoint); 11 | } 12 | } 13 | 14 | export default { 15 | SSearch 16 | }; 17 | -------------------------------------------------------------------------------- /src/google-site-search/index.js: -------------------------------------------------------------------------------- 1 | import { connect } from "./fetch"; 2 | 3 | import { createApplication } from "../util/heighliner"; 4 | 5 | import Search from "./models/search"; 6 | import Geolcate from "./models/geolocate"; 7 | 8 | export const { schema, resolvers, models, queries, mocks } = createApplication([ 9 | Search, 10 | Geolcate 11 | ]); 12 | 13 | export default { 14 | models, 15 | resolvers, 16 | mocks, 17 | schema, 18 | connect 19 | }; 20 | -------------------------------------------------------------------------------- /src/rock/models/people/__tests__/__snapshots__/mutations.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`should have all needed mutations 1`] = ` 4 | Array [ 5 | "setPhoneNumber(phoneNumber: String!): PhoneNumberMutationResponse", 6 | "saveDeviceRegistrationId(registrationId: String!, uuid: String!): DeviceRegistrationMutationResponse", 7 | "setPersonAttribute(key: String!, value: String!): AttributeValueMutationResponse", 8 | ] 9 | `; 10 | -------------------------------------------------------------------------------- /src/google-site-search/models/geolocate/model.js: -------------------------------------------------------------------------------- 1 | import { GoogleConnector } from "../../fetch"; 2 | 3 | class GGeolocate extends GoogleConnector { 4 | key = process.env.GOOGLE_GEO_LOCATE; 5 | url = 6 | "https://maps.googleapis.com/maps/api/distancematrix/json?units=imperial&"; 7 | 8 | query(query) { 9 | const endpoint = `${this.url}${query}&key=${this.key}`; 10 | return this.get(endpoint); 11 | } 12 | } 13 | 14 | export default { 15 | GGeolocate 16 | }; 17 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | # mySQL: 2 | # image: mysql:5.5 3 | # volumes: 4 | # - ./scripts/mysql/:/etc/mysql/conf.d 5 | # ports: 6 | # - "3306:3306" 7 | # environment: 8 | # MYSQL_ROOT_PASSWORD: password 9 | # MYSQL_DATABASE: ee_local 10 | # MYSQL_USER: root 11 | 12 | # mongoDB: 13 | # image: mongo:latest 14 | # ports: 15 | # - "27017:27017" 16 | 17 | redis: 18 | image: redis 19 | ports: 20 | - "6379:6379" 21 | volumes: 22 | - ./data/redis:/data 23 | -------------------------------------------------------------------------------- /src/google-site-search/models/search/schema.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | ` 3 | type SSSearchResult { 4 | id: ID 5 | title: String 6 | htmlTitle: String 7 | link: String 8 | displayLink: String 9 | description: String 10 | htmlDescription: String 11 | type: String 12 | section: String 13 | image: String 14 | } 15 | 16 | type SSSearch { 17 | total: Int 18 | next: Int 19 | previous: Int 20 | items: [SSSearchResult] 21 | } 22 | ` 23 | ]; 24 | -------------------------------------------------------------------------------- /src/rock/models/system/schema.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | ` 3 | type DefinedValue implements Node { 4 | id: ID! 5 | _id: Int 6 | value: String 7 | description: String 8 | } 9 | type Attribute implements Node { 10 | id: ID! 11 | key: String! 12 | description: String! 13 | order: Int 14 | values: [AttributeValue] 15 | } 16 | type AttributeValue implements Node { 17 | attribute: Attribute 18 | id: ID! 19 | value: String 20 | } 21 | ` 22 | ]; 23 | -------------------------------------------------------------------------------- /src/esv/models/scripture/__tests__/resolver.js: -------------------------------------------------------------------------------- 1 | import casual from "casual"; 2 | import Resolver from "../resolver"; 3 | 4 | const sampleData = casual.description; 5 | 6 | it("`Query` exposes scripture function", () => { 7 | const { Query } = Resolver; 8 | expect(Query.scripture).toBeTruthy(); 9 | }); 10 | 11 | it("`ESVScripture` returns html from data", () => { 12 | const { ESVScripture } = Resolver; 13 | const html = ESVScripture.html(sampleData); 14 | expect(html).toEqual(sampleData); 15 | }); 16 | -------------------------------------------------------------------------------- /src/google-site-search/models/geolocate/schema.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | ` 3 | type GGeoLocate { 4 | destination_addresses: [String] 5 | origin_addresses: [String] 6 | rows: [GGeoRow] 7 | status: String 8 | } 9 | 10 | type GGeoRow { 11 | elements: [GGeoElement] 12 | } 13 | 14 | type GGeoElement { 15 | distance: GGeoValue 16 | duration: GGeoValue 17 | status: String 18 | } 19 | 20 | type GGeoValue { 21 | text: String 22 | value: Int 23 | } 24 | ` 25 | ]; 26 | -------------------------------------------------------------------------------- /src/expression-engine/index.js: -------------------------------------------------------------------------------- 1 | import { connect } from "./mysql"; 2 | 3 | import { createApplication } from "../util/heighliner"; 4 | 5 | import Content from "./models/content"; 6 | import Files from "./models/files"; 7 | import Navigation from "./models/navigation"; 8 | 9 | export const { models, resolvers, mocks, schema, queries } = createApplication([ 10 | Content, 11 | Files, 12 | Navigation 13 | ]); 14 | 15 | export default { 16 | models, 17 | resolvers, 18 | mocks, 19 | schema, 20 | connect 21 | }; 22 | -------------------------------------------------------------------------------- /src/rock/models/groups/queries.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | `groups( 3 | attributes: [String], 4 | limit: Int = 20, 5 | offset: Int = 0, 6 | query: String, 7 | clientIp: String, 8 | campuses: [String], 9 | campus: String, 10 | latitude: Float, 11 | longitude: Float, 12 | zip: String, 13 | schedules: [Int], 14 | ): GroupSearch`, 15 | 16 | // XXX clientIp and campuses are depracated 17 | // 18 | // XXX should this take a group type id? 19 | "groupAttributes: [DefinedValue]" 20 | ]; 21 | -------------------------------------------------------------------------------- /src/routes/errors.js: -------------------------------------------------------------------------------- 1 | import raven from "raven"; 2 | 3 | export default app => { 4 | if (process.env.NODE_ENV === "production") { 5 | // The request handler must be the first item 6 | app.use(raven.middleware.express.requestHandler(process.env.SENTRY)); 7 | } 8 | }; 9 | 10 | export const errors = app => { 11 | // The error handler must be before any other error middleware 12 | if (process.env.NODE_ENV === "production") { 13 | app.use(raven.middleware.express.errorHandler(process.env.SENTRY)); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /src/esv/models/scripture/model.js: -------------------------------------------------------------------------------- 1 | import { ESVFetchConnector } from "../../fetch"; 2 | 3 | import { defaultCache } from "../../../util/cache"; 4 | 5 | class ESV extends ESVFetchConnector { 6 | __type = "ESV"; 7 | 8 | constructor({ cache } = { cache: defaultCache }) { 9 | super(); 10 | this.cache = cache; 11 | } 12 | 13 | async get(query) { 14 | return await this.cache.get(`${this.__type}:${query}`, () => 15 | this.getFromAPI(query) 16 | ); 17 | } 18 | } 19 | 20 | export default { 21 | ESV 22 | }; 23 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Databases 2 | REDIS_HOST= 3 | MONGO_URL= 4 | MYSQL_HOST= 5 | MYSQL_USER= 6 | MYSQL_PASSWORD= 7 | MYSQL_DB= 8 | MYSQL_SSL= 9 | MSSQL_HOST= 10 | MSSQL_USER= 11 | MSSQL_PASSWORD= 12 | MSSQL_DB= 13 | MSSQL_INSTANCE= 14 | 15 | # App 16 | PORT=8888 17 | 18 | # Externals 19 | ROCK_URL= 20 | ROCK_TOKEN= 21 | 22 | SEARCH_URL= 23 | SEARCH_KEY= 24 | SEARCH_CX= 25 | 26 | SENTRY= 27 | SLACK= 28 | 29 | GOOGLE_GEO_LOCATE= 30 | DATADOG_API_KEY= 31 | ENGINE_API_KEY= 32 | 33 | # DEBUG=metrics 34 | DATADOG_APP_KEY= 35 | TRACER_APP_KEY= 36 | 37 | NMI_GATEWAY= 38 | NMI_KEY= 39 | -------------------------------------------------------------------------------- /src/util/cache/index.js: -------------------------------------------------------------------------------- 1 | import { InMemoryCache } from "./memory-cache"; 2 | import { RedisCache, connect as RedisConnect } from "./redis"; 3 | 4 | import { defaultCache, resolvers, mutations } from "./defaults"; 5 | 6 | export async function createCache(monitor) { 7 | const datadog = monitor && monitor.datadog; 8 | const REDIS = await RedisConnect({ datadog }); 9 | return REDIS ? new RedisCache() : new InMemoryCache(); 10 | } 11 | 12 | export { 13 | InMemoryCache, 14 | RedisCache, 15 | RedisConnect, 16 | defaultCache, 17 | resolvers, 18 | mutations 19 | }; 20 | -------------------------------------------------------------------------------- /src/expression-engine/models/content/__tests__/__snapshots__/model.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`findByCampusName should pass correct value for channelTitles 1`] = ` 4 | Object { 5 | "entry_date": Object { 6 | "$lte": literal { 7 | "val": "UNIX_TIMESTAMP(NOW())", 8 | }, 9 | }, 10 | "expiration_date": Object { 11 | "$or": Array [ 12 | Object { 13 | "$eq": 0, 14 | }, 15 | Object { 16 | "$gt": literal { 17 | "val": "UNIX_TIMESTAMP(NOW())", 18 | }, 19 | }, 20 | ], 21 | }, 22 | } 23 | `; 24 | -------------------------------------------------------------------------------- /src/rock/models/binary-files/resolver.js: -------------------------------------------------------------------------------- 1 | export default { 2 | Mutation: { 3 | async attachPhotoIdToUser(_, props, { models, user }) { 4 | try { 5 | // NOTE: context person could be cached 6 | // therefore we need to query again 7 | const person = await models.User.getUserProfile(user.PersonId); 8 | return models.BinaryFile.attachPhotoIdToUser({ 9 | personId: person.Id, 10 | previousPhotoId: person.PhotoId, 11 | newPhotoId: props.id 12 | }); 13 | } catch (err) { 14 | throw err; 15 | } 16 | } 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to find out which attributes exist for node debugging 3 | // Use hover for the description of the existing attributes 4 | // For further information visit https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Launch Program", 11 | "program": "${workspaceFolder}/src/server.js", 12 | "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/babel-node", 13 | "env": { 14 | "PORT": "8888" 15 | } 16 | }, 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /src/rock/models/campuses/schema.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | ` 3 | type Location implements Node { 4 | id: ID! 5 | name: String 6 | street1: String 7 | street2: String 8 | city: String 9 | state: String 10 | country: String 11 | zip: String 12 | latitude: Float 13 | longitude: Float 14 | distance: Float 15 | } 16 | 17 | type Campus implements Node { 18 | id: ID! 19 | entityId: Int! 20 | name: String 21 | shortCode: String 22 | guid: String 23 | services: [String] 24 | url: String 25 | locationId: ID 26 | location: Location 27 | } 28 | ` 29 | ]; 30 | -------------------------------------------------------------------------------- /src/expression-engine/models/files/resolver.js: -------------------------------------------------------------------------------- 1 | import { createGlobalId } from "../../../util"; 2 | 3 | export default { 4 | File: { 5 | id: ({ file_id }, _, $, { parentType }) => 6 | createGlobalId(file_id, parentType.name), 7 | file: ({ fileName }) => fileName || null, 8 | label: ({ fileLabel }) => fileLabel || null, 9 | 10 | url: ({ url }) => url, 11 | 12 | // deprecated 13 | s3: ({ s3 }) => s3, 14 | cloudfront: ({ cloudfront }) => cloudfront || null, 15 | 16 | fileName: ({ fileName }) => fileName || null, 17 | fileType: ({ fileType }) => fileType || null, 18 | fileLabel: ({ fileLabel }) => fileLabel || null 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /src/apollos/models/users/mutations.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | "loginUser(email: String!, password: String!): LoginMutationResponse", 3 | "registerUser(email: String!, password: String!, firstName: String!, lastName: String!): LoginMutationResponse", 4 | "logoutUser: Boolean", 5 | "changeUserPassword(oldPassword: String!, newPassword: String!): Boolean", 6 | "forgotUserPassword(email: String!, sourceURL: String): Boolean", 7 | "resetUserPassword(token: String!, newPassword: String!): LoginMutationResponse", 8 | "toggleTopic(topic: String!): Boolean", 9 | "updateProfile(input: UserProfileInput): Boolean", 10 | "updateHomeAddress(input: HomeAddressInput): Boolean" 11 | ]; 12 | -------------------------------------------------------------------------------- /src/util/cache/defaults.js: -------------------------------------------------------------------------------- 1 | import { createGlobalId } from "../node/model"; 2 | 3 | export const defaultCache = { 4 | get: (id, lookup) => Promise.resolve().then(lookup), 5 | set: id => Promise.resolve().then(() => true), 6 | del() {}, 7 | encode: (obj, prefix) => `${prefix}${JSON.stringify(obj)}` 8 | }; 9 | 10 | export const resolvers = { 11 | Mutation: { 12 | cache(_, { id, type }, { cache, models }) { 13 | if (type && id) id = createGlobalId(id, type); 14 | return Promise.resolve() 15 | .then(() => cache.del(id)) 16 | .then(() => models.Node.get(id)); 17 | } 18 | } 19 | }; 20 | 21 | export const mutations = ["cache(id: ID!, type: String): Node"]; 22 | -------------------------------------------------------------------------------- /.travis/run_on_pull_requests: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | openssl aes-256-cbc -K $encrypted_de5ad5555d6e_key -iv $encrypted_de5ad5555d6e_iv -in .travis/heighliner.enc -out ~/.ssh/id_rsa -d 5 | chmod 600 ~/.ssh/id_rsa 6 | eval `ssh-agent -s` 7 | ssh-add ~/.ssh/id_rsa 8 | 9 | BUILD_TAG="GH$TRAVIS_PULL_REQUEST-B$TRAVIS_BUILD_NUMBER" 10 | REPO=`git config remote.origin.url` 11 | SSH_REPO=${REPO/https:\/\/github.com\//git@github.com:} 12 | 13 | git config user.name "Travis CI" 14 | git config user.email "$COMMIT_AUTHOR_EMAIL" 15 | 16 | # create a new new tag (GH###-B#) 17 | git tag $BUILD_TAG 18 | 19 | # push tag to github 20 | git push -q $SSH_REPO refs/tags/$BUILD_TAG 21 | 22 | echo "Pushed Tag: $BUILD_TAG to Github!" 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/expression-engine/models/files/schema.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | ` 3 | # interface File { 4 | # id: ID! 5 | # file: String! 6 | # type: String 7 | # label: String 8 | 9 | # s3: String 10 | # cloudfront: String 11 | 12 | # # deprecated 13 | # fileName: String! 14 | # fileType: String 15 | # fileLabel: String 16 | # } 17 | 18 | type File implements Node { 19 | id: ID! 20 | file: String! 21 | label: String 22 | size: String 23 | 24 | url: String 25 | 26 | # deprecated 27 | s3: String 28 | cloudfront: String 29 | 30 | duration: String 31 | title: String 32 | 33 | # deprecated 34 | fileName: String! 35 | fileType: String 36 | fileLabel: String 37 | } 38 | ` 39 | ]; 40 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "editor.tabSize": 2, 4 | "editor.rulers": [100], 5 | "files.trimTrailingWhitespace": true, 6 | "files.insertFinalNewline": true, 7 | "editor.wrappingColumn": 100, 8 | "files.exclude": { 9 | // "**/.git": true, 10 | // "**/.DS_Store": true, 11 | // "node_modules": true, 12 | // "lib": true, 13 | // "Dockerfile": true, 14 | // "data": true, 15 | // "coverage": true, 16 | // ".babelrc": true, 17 | // ".gitignore": true, 18 | // ".eslintrc": true, 19 | // ".env*": true, 20 | // "**/*.yml": true, 21 | // "scripts": true, 22 | // "npm-debug.log": true, 23 | // "webpack.config.js": true, 24 | // "yarn.lock": true 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/expression-engine/models/ee/snippets.js: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-shadowed-variable */ 2 | 3 | import { INTEGER, STRING } from "sequelize"; 4 | 5 | import { MySQLConnector } from "../../mysql"; 6 | 7 | const snippetsSchema = { 8 | snippet_id: { type: INTEGER, primaryKey: true }, 9 | site_id: { type: STRING }, 10 | snippet_name: { type: STRING }, 11 | snippet_contents: { type: STRING } 12 | }; 13 | 14 | let Snippets; 15 | export { Snippets, snippetsSchema }; 16 | 17 | export function connect() { 18 | Snippets = new MySQLConnector("exp_snippets", snippetsSchema); 19 | 20 | return { 21 | Snippets 22 | }; 23 | } 24 | 25 | // export function bind({ 26 | // ChannelData, 27 | // Sites, 28 | // }): void { 29 | 30 | // }; 31 | 32 | export default { 33 | connect 34 | // bind, 35 | }; 36 | -------------------------------------------------------------------------------- /src/rock/models/finances/queries.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | "savedPayments(limit: Int = 20, skip: Int = 0, cache: Boolean = true): [SavedPayment]", 3 | "savedPayment(id: ID!): SavedPayment", 4 | 5 | `transactions( 6 | limit: Int = 20, 7 | skip: Int = 0, 8 | cache: Boolean = true, 9 | start: String, 10 | end: String, 11 | people: [Int] = [], 12 | ): [Transaction]`, 13 | 14 | `scheduledTransactions( 15 | limit: Int = 20, skip: Int = 0, cache: Boolean = true, isActive: Boolean = true 16 | ): [ScheduledTransaction]`, 17 | 18 | `accounts( 19 | allFunds: Boolean = false, name: String, isActive: Boolean = true, isPublic: Boolean = true, isTaxDeductable: Boolean = true 20 | ): [FinancialAccount]`, 21 | 22 | "accountFromCashTag(cashTag: String!): FinancialAccount" 23 | ]; 24 | -------------------------------------------------------------------------------- /src/routes/util.js: -------------------------------------------------------------------------------- 1 | import bodyParser from "body-parser"; 2 | import cors from "cors"; 3 | 4 | export default app => { 5 | const sites = /^http(s?):\/\/.*.?(newspring|newspringfuse|newspringnetwork|apollos.netlify|newspring.github).(com|cc|io|dev)\/?$/; 6 | const local = /^http(s?):\/\/localhost:\d*$/; 7 | 8 | const corsOptions = { 9 | origin: (origin, callback) => { 10 | const originIsWhitelisted = sites.test(origin) || local.test(origin); 11 | callback(null, originIsWhitelisted); 12 | }, 13 | credentials: true 14 | }; 15 | 16 | app.use(cors(corsOptions)); 17 | 18 | app.use( 19 | bodyParser.urlencoded({ 20 | extended: true 21 | }) 22 | ); 23 | 24 | app.use(bodyParser.json()); 25 | 26 | app.get("/util/ping", (req, res) => { 27 | res.status(200).end(); 28 | }); 29 | }; 30 | -------------------------------------------------------------------------------- /src/expression-engine/models/navigation/resolver.js: -------------------------------------------------------------------------------- 1 | import { createGlobalId } from "../../../util"; 2 | 3 | export default { 4 | Query: { 5 | navigation: (_, { nav }, { models }) => models.Navigation.find({ nav }) 6 | }, 7 | 8 | Navigation: { 9 | id: ({ id }, _, $, { parentType }) => createGlobalId(id, parentType.name), 10 | text: ({ text }) => text, 11 | link: ({ link }) => link, 12 | absoluteLink: ({ link, url }) => `${url}${link.substring(1, link.length)}`, 13 | sort: ({ sort }) => sort, 14 | image: ({ image }) => image, 15 | children: ({ children, id }, _, { models }) => { 16 | // tslint:disable-line 17 | if (children) return children; 18 | 19 | // XXX hookup up find by parent method 20 | return null; 21 | // return models.Navigation.findByParent(id); 22 | } 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /src/rock/index.js: -------------------------------------------------------------------------------- 1 | import { connect } from "./mssql"; 2 | 3 | import { createApplication } from "../util/heighliner"; 4 | 5 | import People from "./models/people"; 6 | import Finances from "./models/finances"; 7 | import Campuses from "./models/campuses"; 8 | import System from "./models/system"; 9 | import Groups from "./models/groups"; 10 | import BinaryFiles from "./models/binary-files"; 11 | import Feeds from "./models/feeds"; 12 | import Likes from "./models/likes"; 13 | 14 | export const { 15 | mutations, 16 | queries, 17 | models, 18 | resolvers, 19 | mocks, 20 | schema 21 | } = createApplication([ 22 | People, 23 | Finances, 24 | Campuses, 25 | System, 26 | Groups, 27 | BinaryFiles, 28 | Feeds, 29 | Likes 30 | ]); 31 | 32 | export default { 33 | models, 34 | mutations, 35 | resolvers, 36 | mocks, 37 | schema, 38 | connect 39 | }; 40 | -------------------------------------------------------------------------------- /src/rock/models/finances/__tests__/__snapshots__/queries.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`has all needed queries 1`] = ` 4 | Array [ 5 | "savedPayments(limit: Int = 20, skip: Int = 0, cache: Boolean = true): [SavedPayment]", 6 | "savedPayment(id: ID!): SavedPayment", 7 | "transactions( 8 | limit: Int = 20, 9 | skip: Int = 0, 10 | cache: Boolean = true, 11 | start: String, 12 | end: String, 13 | people: [Int] = [], 14 | ): [Transaction]", 15 | "scheduledTransactions( 16 | limit: Int = 20, skip: Int = 0, cache: Boolean = true, isActive: Boolean = true 17 | ): [ScheduledTransaction]", 18 | "accounts( 19 | allFunds: Boolean = false, name: String, isActive: Boolean = true, isPublic: Boolean = true, isTaxDeductable: Boolean = true 20 | ): [FinancialAccount]", 21 | "accountFromCashTag(cashTag: String!): FinancialAccount", 22 | ] 23 | `; 24 | -------------------------------------------------------------------------------- /src/expression-engine/models/index.js: -------------------------------------------------------------------------------- 1 | import { merge } from "lodash"; 2 | 3 | import channels from "./../models/content/tables"; 4 | import assets from "./../models/files/tables"; 5 | import matrix from "./ee/matrix"; 6 | import playa from "./ee/playa"; 7 | import tags from "./ee/tags"; 8 | import sites from "./ee/sites"; 9 | import snippets from "./ee/snippets"; 10 | import navee from "./../models/navigation/tables"; 11 | 12 | const tables = { 13 | assets, 14 | channels, 15 | matrix, 16 | playa, 17 | sites, 18 | tags, 19 | navee, 20 | snippets 21 | }; 22 | 23 | export function createTables() { 24 | let createdTables = {}; 25 | 26 | for (const table in tables) { 27 | createdTables = merge(createdTables, tables[table].connect()); 28 | } 29 | 30 | for (const table in tables) { 31 | if (tables[table].bind) tables[table].bind(createdTables); 32 | } 33 | 34 | return createdTables; 35 | } 36 | -------------------------------------------------------------------------------- /src/rock/models/likes/resolver.js: -------------------------------------------------------------------------------- 1 | const MutationReponseResolver = { 2 | error: ({ error }) => error, 3 | success: ({ success, error }) => success || !error, 4 | code: ({ code }) => code 5 | }; 6 | 7 | export default { 8 | Query: { 9 | recentlyLiked(_, { limit, skip, cache }, { models, user }) { 10 | const userId = user ? user._id : null; 11 | return models.Like.getRecentlyLiked( 12 | { limit, skip, cache }, 13 | userId, 14 | models.Node 15 | ); 16 | } 17 | }, 18 | Mutation: { 19 | toggleLike(_, { nodeId }, { models, person }) { 20 | if (!person) throw new Error("User is not logged in!"); 21 | if (!nodeId) throw new Error("EntryId is missing!"); 22 | return models.Like.toggleLike(nodeId, person.PrimaryAliasId, models.Node); 23 | } 24 | }, 25 | LikesMutationResponse: { 26 | ...MutationReponseResolver, 27 | like: ({ like }) => like 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /src/google-site-search/models/geolocate/resolver.js: -------------------------------------------------------------------------------- 1 | export default { 2 | Query: { 3 | geolocate(_, { origin, destinations }, { models }) { 4 | const query = `origins=${encodeURI(origin)}&destinations=${encodeURI( 5 | destinations 6 | )}`; 7 | return models.GGeolocate.query(query); 8 | } 9 | }, 10 | 11 | GGeoLocate: { 12 | destination_addresses: ({ destination_addresses }) => destination_addresses, 13 | origin_addresses: ({ origin_addresses }) => origin_addresses, 14 | rows: ({ rows }) => rows, 15 | status: ({ status }) => status 16 | }, 17 | 18 | GGeoRow: { 19 | elements: ({ elements }) => elements 20 | }, 21 | 22 | GGeoElement: { 23 | distance: ({ distance }) => distance, 24 | duration: ({ duration }) => duration, 25 | status: ({ status }) => status 26 | }, 27 | 28 | GGeoValue: { 29 | text: ({ text }) => text, 30 | value: ({ value }) => value 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /src/google-site-search/fetch.js: -------------------------------------------------------------------------------- 1 | import fetch from "isomorphic-fetch"; 2 | // import DataLoader from "dataloader"; 3 | 4 | export const connect = address => 5 | new Promise(cb => { 6 | cb(true); 7 | }); 8 | 9 | export class GoogleConnector { 10 | count = 0; 11 | 12 | get(endpoint) { 13 | const label = `GoogleConnector${this.getCount()}`; 14 | const headers = { 15 | "user-agent": "Heighliner", 16 | "Content-Type": "application/json" 17 | }; 18 | 19 | const options = { method: "GET", headers }; 20 | console.time(label); // tslint:disable-line 21 | // XXX we can't cache google site search legally 22 | return fetch(endpoint, options) 23 | .then(x => (x.json ? x.json() : x.text())) 24 | .then(x => { 25 | console.timeEnd(label); 26 | return x; 27 | }); // tslint:disable-line 28 | } 29 | 30 | getCount() { 31 | this.count++; 32 | return this.count; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/server.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | 3 | import util from "./routes/util"; 4 | import engine from "./routes/engine"; 5 | import graphiql from "./routes/graphiql"; 6 | import sentry, { errors } from "./routes/errors"; 7 | import setupDatadog from "./routes/datadog"; 8 | import createApp from "./routes/graphql"; 9 | import cacheUtils from "./routes/cache"; 10 | 11 | const app = express(); 12 | 13 | engine(app); 14 | util(app); 15 | graphiql(app); 16 | sentry(app); 17 | const datadog = setupDatadog(app); 18 | cacheUtils(app, { datadog }); 19 | createApp(app, { datadog }); 20 | errors(app); 21 | 22 | // Listen for incoming HTTP requests 23 | const listener = app.listen(process.env.PORT || 80, () => { 24 | let host = listener.address().address; 25 | if (host === "::") host = "localhost"; 26 | const port = listener.address().port; 27 | // eslint-disable-next-line 28 | console.log("Listening at http://%s%s", host, port === 80 ? "" : `:${port}`); 29 | }); 30 | -------------------------------------------------------------------------------- /src/rock/models/system/__tests__/fieldTypeResolvers.js: -------------------------------------------------------------------------------- 1 | import Resolver from "../fieldTypeResolvers"; 2 | import Moment from "moment"; 3 | 4 | jest.mock("moment", () => () => ({ 5 | toString: () => "harambe" 6 | })); 7 | 8 | describe("Date", () => { 9 | const date = Resolver["Rock.Field.Types.DateFieldType"]; 10 | 11 | it("uses default value if no value passed", () => { 12 | expect(date(null, "2016-05-28")).toEqual("2016-05-28"); 13 | }); 14 | 15 | // uses moment mock 16 | it("uses value if present", () => { 17 | expect(date("2016-05-28", "2099-01-01")).toEqual("harambe"); 18 | }); 19 | }); 20 | 21 | describe("SelectSingle", () => { 22 | const singleSelect = Resolver["Rock.Field.Types.SelectSingleFieldType"]; 23 | 24 | it("uses default if no value", () => { 25 | expect(singleSelect(null, "hai")).toEqual("hai"); 26 | }); 27 | 28 | it("uses value if present", () => { 29 | expect(singleSelect("baramhe", "hai")).toEqual("baramhe"); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /src/expression-engine/models/ee/sites.js: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-shadowed-variable */ 2 | 3 | import { INTEGER, STRING } from "sequelize"; 4 | import { unserialize } from "php-unserialize"; 5 | 6 | import { MySQLConnector, Tables } from "../../mysql"; 7 | 8 | const siteSchema = { 9 | site_id: { type: INTEGER, primaryKey: true }, 10 | site_label: { type: STRING }, 11 | site_name: { type: STRING }, 12 | site_pages: { type: STRING } 13 | }; 14 | 15 | let Sites; 16 | export { Sites, siteSchema }; 17 | 18 | export function connect() { 19 | Sites = new MySQLConnector("exp_sites", siteSchema); 20 | 21 | // helper to parse through the sites pages module 22 | Sites.parsePage = function(page) { 23 | return unserialize(new Buffer(page, "base64").toString()); 24 | }; 25 | 26 | return { 27 | Sites 28 | }; 29 | } 30 | 31 | // export function bind({ 32 | // ChannelData, 33 | // Sites, 34 | // }) { 35 | 36 | // }; 37 | 38 | export default { 39 | connect 40 | // bind, 41 | }; 42 | -------------------------------------------------------------------------------- /src/google-site-search/__test__/fetch.spec.js: -------------------------------------------------------------------------------- 1 | // import casual from "casual"; 2 | 3 | import { GoogleConnector } from "../fetch"; 4 | 5 | // test("`connect` should fail without any env vars", async (t) => { 6 | // const SS = await connect(casual.url); 7 | // t.falsy(SS); 8 | // }); 9 | 10 | // test("`connect` should fail without all env vars", async (t) => { 11 | // process.env.SEARCH_URL = casual.url; 12 | // process.env.SEARCH_KEY = casual.word; 13 | 14 | // const SS = await connect(casual.url); 15 | // t.falsy(SS); 16 | // }); 17 | 18 | // test("`connect` should return true if proper env vars", async (t) => { 19 | // process.env.SEARCH_URL = casual.url; 20 | // process.env.SEARCH_KEY = casual.word; 21 | // process.env.SEARCH_CX = casual.word; 22 | 23 | // const SS = await connect(casual.url); 24 | // t.truthy(SS); 25 | // }); 26 | 27 | it("should expose get function", async () => { 28 | const testFetcher = new GoogleConnector(); 29 | expect(testFetcher.get).toBeTruthy(); 30 | }); 31 | -------------------------------------------------------------------------------- /src/rock/models/index.js: -------------------------------------------------------------------------------- 1 | import { merge } from "lodash"; 2 | 3 | import people from "./people/tables"; 4 | import finances from "./finances/tables"; 5 | import system from "./system/tables"; 6 | import campuses from "./campuses/tables"; 7 | import groups from "./groups/tables"; 8 | import binaryFiles from "./binary-files/tables"; 9 | 10 | const tables = { 11 | people, 12 | finances, 13 | system, 14 | campuses, 15 | groups, 16 | binaryFiles 17 | }; 18 | 19 | export function createTables() { 20 | let createdTables = {}; 21 | 22 | for (const table in tables) { 23 | try { 24 | createdTables = merge(createdTables, tables[table].connect()); 25 | } catch (e) { 26 | console.error(e); 27 | } 28 | } 29 | 30 | for (const table in tables) { 31 | try { 32 | if (tables[table].bind) tables[table].bind(createdTables); 33 | } catch (e) { 34 | console.error(`in binding ${table}`); 35 | console.error(e); 36 | } 37 | } 38 | 39 | return createdTables; 40 | } 41 | -------------------------------------------------------------------------------- /src/expression-engine/models/content/queries.js: -------------------------------------------------------------------------------- 1 | // XXX implement pagination instead of skip 2 | // use `after` for ^^ 3 | export default [ 4 | `content( 5 | channel: String!, 6 | collection: ID, 7 | limit: Int = 20, 8 | skip: Int = 0, 9 | status: String = "open", 10 | cache: Boolean = true 11 | ): [Content]`, 12 | 13 | `feed( 14 | excludeChannels: [String], 15 | limit: Int = 20, 16 | skip: Int = 0, 17 | status: String = "open", 18 | cache: Boolean = true 19 | ): [Content]`, 20 | 21 | // XXX deprecated tagName 22 | `taggedContent( 23 | includeChannels: [String], 24 | excludedIds: [String], 25 | tagName: String, 26 | tags: [String], 27 | limit: Int = 20, 28 | skip: Int = 0, 29 | status: String = "open", 30 | cache: Boolean = true 31 | ): [Content]`, 32 | 33 | "lowReorderSets(setName: String!): [Content]", 34 | 35 | "live: LiveFeed", 36 | 37 | `contentWithUrlTitle( 38 | channel: String!, 39 | urlTitle: String!, 40 | ): String` 41 | ]; 42 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # FROM mhart/alpine-node:base 2 | # FROM mhart/alpine-node:base-0.10 3 | FROM mhart/alpine-node:8 4 | 5 | WORKDIR /src 6 | ADD . . 7 | 8 | # If you have native dependencies, you'll need extra tools 9 | # Install required APKs needed for building, install node modules, fix phantom, then cleanup. 10 | RUN apk add --update git python build-base curl bash && \ 11 | echo "Fixing PhantomJS" && \ 12 | curl -Ls "https://github.com/dustinblackman/phantomized/releases/download/2.1.1/dockerized-phantomjs.tar.gz" | tar xz -C / && \ 13 | echo "Installing node modules" && \ 14 | sed -i '/postinstall/d' package.json && \ 15 | npm install --production && \ 16 | apk del git python build-base curl && \ 17 | rm -Rf /etc/ssl/certs/* && \ 18 | apk add ca-certificates && \ 19 | rm -rf /usr/share/man /tmp/* /var/tmp/* /var/cache/apk/* /root/.npm /root/.node-gyp 20 | 21 | # If you had native dependencies you can now remove build tools 22 | # RUN apk del make gcc g++ python && \ 23 | # rm -rf /tmp/* /var/cache/apk/* /root/.npm /root/.node-gyp 24 | 25 | EXPOSE 80 26 | CMD ["node", "./lib/server.js"] 27 | -------------------------------------------------------------------------------- /src/expression-engine/models/ee/playa.js: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-shadowed-variable */ 2 | 3 | import { INTEGER, STRING } from "sequelize"; 4 | 5 | import { MySQLConnector, Tables } from "../../mysql"; 6 | 7 | const playaSchema = { 8 | rel_id: { type: INTEGER, primaryKey: true }, 9 | parent_entry_id: { type: INTEGER }, 10 | parent_field_id: { type: INTEGER }, 11 | parent_col_id: { type: INTEGER }, 12 | parent_row_id: { type: INTEGER }, 13 | parent_var_id: { type: INTEGER }, 14 | parent_element_id: { type: STRING }, 15 | child_entry_id: { type: INTEGER }, 16 | rel_order: { type: INTEGER } 17 | }; 18 | let Playa; 19 | export { Playa, playaSchema }; 20 | 21 | export function connect() { 22 | Playa = new MySQLConnector("exp_playa_relationships", playaSchema); 23 | 24 | return { 25 | Playa 26 | }; 27 | } 28 | 29 | export function bind({ ChannelData, Playa }) { 30 | // get access to matrix from channel data 31 | ChannelData.model.hasMany(Playa.model, { foreignKey: "entry_id" }); 32 | Playa.model.belongsTo(ChannelData.model, { foreignKey: "entry_id" }); 33 | } 34 | 35 | export default { 36 | connect, 37 | bind 38 | }; 39 | -------------------------------------------------------------------------------- /src/util/node/__test__/queries.integration.js: -------------------------------------------------------------------------------- 1 | // import express from "express"; 2 | // import { apolloExpress } from "apollo-server"; 3 | // import { tester } from "graphql-tester"; 4 | // import { create } from "graphql-tester/lib/main/servers/express"; 5 | // import bodyParser from "body-parser"; 6 | // import { createApp } from "../../../schema"; 7 | 8 | // let Heighliner; 9 | // beforeEach(async () => { 10 | // const app = express(); 11 | // const { graphql } = await createApp(); 12 | 13 | // app.use(bodyParser.json()); 14 | 15 | // app.use("/graphql", apolloExpress(graphql)); 16 | 17 | // Heighliner = tester({ 18 | // server: create(app), 19 | // url: "/graphql", 20 | // contentType: "application/json", 21 | // }); 22 | // }); 23 | 24 | // XXX figure out how to mock node resolver 25 | xit("Valid queries should return success", async () => { 26 | const response = await Heighliner( 27 | JSON.stringify({ 28 | query: `{ 29 | node(id: "VXNlcjptdGhlMmhUQWhQU0dESEpuZQ=="){ 30 | id 31 | } 32 | }` 33 | }) 34 | ); 35 | 36 | // t.true(response.success); 37 | expect(response.status).toEqual(200); 38 | }); 39 | -------------------------------------------------------------------------------- /src/util/cache/__test__/mutations.integration.js: -------------------------------------------------------------------------------- 1 | // import express from "express"; 2 | // import { apolloExpress } from "apollo-server"; 3 | // import { tester } from "graphql-tester"; 4 | // import { create } from "graphql-tester/lib/main/servers/express"; 5 | // import bodyParser from "body-parser"; 6 | // import { createApp } from "../../../schema"; 7 | 8 | // let Heighliner; 9 | // beforeEach(async () => { 10 | // const app = express(); 11 | // const { graphql } = await createApp(); 12 | 13 | // app.use(bodyParser.json()); 14 | 15 | // app.use("/graphql", apolloExpress(graphql)); 16 | 17 | // Heighliner = tester({ 18 | // server: create(app), 19 | // url: "/graphql", 20 | // contentType: "application/json", 21 | // }); 22 | // }); 23 | 24 | xit("Valid queries should return success", async () => { 25 | const response = await Heighliner( 26 | JSON.stringify({ 27 | query: ` 28 | mutation ClearCache { 29 | cache(id:"VXNlcjpyWE5iRXlIWmhycENUdHpOZw=="){ 30 | id 31 | } 32 | } 33 | ` 34 | }) 35 | ); 36 | 37 | expect(response.success).toBeTruthy(); 38 | expect(response.status).toEqual(200); 39 | }); 40 | -------------------------------------------------------------------------------- /src/util/model.js: -------------------------------------------------------------------------------- 1 | import { flatten, isNil } from "lodash"; 2 | import { defaultCache } from "./cache"; 3 | import { createGlobalId } from "./node/model"; 4 | 5 | export class Heighliner { 6 | constructor({ cache } = { cache: defaultCache }) { 7 | this.cache = cache; 8 | } 9 | 10 | async getFromId(id, globalId) { 11 | return Promise.reject(new Error("Not implemented on this model")); 12 | } 13 | 14 | async clearCacheFromRequest({ body }) { 15 | return Promise.reject(new Error(`Caching not implement on ${body.type}`)); 16 | } 17 | 18 | async getFromIds(data = []) { 19 | if (!data || !data.length) return Promise.resolve([]); 20 | return Promise.all( 21 | data.map(x => 22 | this.getFromId(x[this.id], createGlobalId(x[this.id], this.__type)) 23 | ) 24 | ) 25 | .then(x => flatten(x)) 26 | .then(x => 27 | x 28 | .filter(y => !isNil(y)) 29 | .map(z => { 30 | const item = z; 31 | item.__type = this.__type; 32 | return item; 33 | }) 34 | ); 35 | } 36 | 37 | debug(data) { 38 | console.log("DEBUG:", data); // tslint:disable-line 39 | return data; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/expression-engine/models/content/images.js: -------------------------------------------------------------------------------- 1 | import { assign, filter, includes } from "lodash"; 2 | 3 | const allResizings = ["xlarge", "large", "medium", "small", "xsmall"]; 4 | const sourceBucket = "ns.images"; 5 | const resizeBucket = "resizings"; 6 | 7 | const generateFilename = (filename, size) => { 8 | if (!filename) return null; 9 | 10 | const parts = filename.split("."); 11 | parts.splice(parts.length - 1, 0, size); 12 | let result = parts.join("."); 13 | result = result.replace(sourceBucket, resizeBucket); 14 | 15 | return result; 16 | }; 17 | 18 | const addResizings = (images, options = { sizes: null, ratios: [] }) => { 19 | const result = []; 20 | const resizings = options.sizes || allResizings; 21 | 22 | if (options.ratios && options.ratios.length > 0) { 23 | images = filter(images, image => includes(options.ratios, image.fileLabel)); 24 | } 25 | 26 | images.map(image => { 27 | resizings.map(resize => { 28 | const resizedImage = assign({}, image); 29 | resizedImage.url = generateFilename(resizedImage.url, resize); 30 | resizedImage.size = resize; 31 | result.push(resizedImage); 32 | }); 33 | }); 34 | 35 | return result; 36 | }; 37 | 38 | export { addResizings }; 39 | -------------------------------------------------------------------------------- /src/rock/models/finances/util/logError.js: -------------------------------------------------------------------------------- 1 | import Raven from "raven"; 2 | 3 | let sentry; 4 | 5 | if (process.env.SENTRY) { 6 | sentry = new Raven.Client(process.env.SENTRY); 7 | } 8 | 9 | const report = ({ data, attemptsMade = 1 }, error) => { 10 | if (!sentry) { 11 | if (!process.env.CI) console.error("ERROR", error); 12 | return; 13 | } 14 | 15 | // don't log to sentry or slack on every attempt 16 | // only log of the first event if possible 17 | if (!attemptsMade || attemptsMade !== 1) return; 18 | 19 | if (data && data.Person) { 20 | sentry.setUserContext({ id: data.Person.Id }); 21 | if (data.Person.Email) sentry.setUserContext({ email: data.Person.Email }); 22 | } 23 | 24 | sentry.captureException(error, { extra: { data, attemptsMade } }); 25 | 26 | // only log to slack the first time an error has happened 27 | if (process.env.SLACK) { 28 | const message = { 29 | username: "Heighliner", 30 | icon_emoji: ":feelsbulbman:", 31 | text: `ATTENTION: ${error.message}`, 32 | channel: "systems" 33 | }; 34 | 35 | fetch(process.env.SLACK, { 36 | method: "POST", 37 | body: JSON.stringify(message) 38 | }); 39 | } 40 | }; 41 | 42 | export default report; 43 | -------------------------------------------------------------------------------- /src/routes/cache.js: -------------------------------------------------------------------------------- 1 | import { createModels } from "./graphql"; 2 | import { createCache } from "../util/cache"; 3 | 4 | export default (app, monitor) => { 5 | app.post("/util/cache/flush", async (req, res) => { 6 | const cache = await createCache(monitor); 7 | cache.clearAll(); 8 | res.end(); 9 | }); 10 | 11 | app.post("/graphql/cache", async (req, res) => { 12 | const cache = await createCache(monitor); 13 | const models = createModels({ cache }); 14 | 15 | const { type, id } = req.body; 16 | if (!type || !id) { 17 | res.status(500).send({ error: "Missing `id` or `type` for request" }); 18 | return; 19 | } 20 | let clearingCache; 21 | for (const model in models) { 22 | const Model = models[model]; 23 | if (!Model.cacheTypes) continue; 24 | if (Model.cacheTypes.indexOf(type) === -1) continue; 25 | clearingCache = true; 26 | // XXX should we hold off the res until this responds? 27 | Model.clearCacheFromRequest(req); 28 | } 29 | if (!clearingCache) { 30 | res.status(404).send({ error: `No model found for ${type}` }); 31 | return; 32 | } 33 | 34 | res.status(200).send({ message: `Cache cleared for ${type} ${id}` }); 35 | }); 36 | }; 37 | -------------------------------------------------------------------------------- /src/esv/__tests__/fetch.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | import casual from "casual"; 4 | 5 | import { ESVFetchConnector, connect } from "../fetch"; 6 | 7 | describe("ESV", () => { 8 | it("`connect` should fail without any env vars", async () => { 9 | delete process.env.ESV_KEY; 10 | 11 | const ESV = await connect(); 12 | expect(ESV).toBeFalsy(); 13 | }); 14 | 15 | it("`connect` should be fine with esv key", async () => { 16 | process.env.ESV_KEY = casual.word; 17 | 18 | const ESV = await connect(); 19 | expect(ESV).toBeTruthy(); 20 | }); 21 | 22 | it("`ESVFetchConnector` should export getFromAPI function", () => { 23 | const testFetcher = new ESVFetchConnector(); 24 | expect(testFetcher.getFromAPI).toBeTruthy(); 25 | }); 26 | 27 | it("`ESVFetchConnector` should format the query", () => { 28 | process.env.ESV_KEY = "test-key"; 29 | 30 | const testFetcher = new ESVFetchConnector(); 31 | expect(testFetcher.getRequest("john 3:16")).toMatchSnapshot(); 32 | }); 33 | 34 | it("`ESVFetchConnector` should keep track of connectors", () => { 35 | const testFetcher = new ESVFetchConnector(); 36 | expect(testFetcher.getCount()).toEqual(1); 37 | expect(testFetcher.getCount()).toEqual(2); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /src/expression-engine/models/ee/tags.js: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-shadowed-variable */ 2 | 3 | import { INTEGER, STRING } from "sequelize"; 4 | 5 | import { MySQLConnector } from "../../mysql"; 6 | 7 | const tagSchema = { 8 | tag_id: { type: INTEGER, primaryKey: true }, 9 | tag_name: { type: STRING }, 10 | entry_date: { type: INTEGER }, 11 | clicks: { type: INTEGER } 12 | }; 13 | 14 | const tagEntriesSchema = { 15 | tag_id: { type: INTEGER, primaryKey: true }, 16 | entry_id: { type: INTEGER } 17 | }; 18 | 19 | let Tags; 20 | let TagEntries; 21 | export { Tags, tagSchema, TagEntries, tagEntriesSchema }; 22 | 23 | export function connect() { 24 | Tags = new MySQLConnector("exp_tag_tags", tagSchema); 25 | TagEntries = new MySQLConnector("exp_tag_entries", tagEntriesSchema); 26 | 27 | return { 28 | Tags, 29 | TagEntries 30 | }; 31 | } 32 | 33 | export function bind({ ChannelData, Tags, TagEntries }) { 34 | ChannelData.model.hasMany(TagEntries.model, { foreignKey: "entry_id" }); 35 | TagEntries.model.belongsTo(ChannelData.model, { foreignKey: "entry_id" }); 36 | 37 | Tags.model.hasMany(TagEntries.model, { foreignKey: "tag_id" }); 38 | TagEntries.model.belongsTo(Tags.model, { foreignKey: "tag_id" }); 39 | } 40 | 41 | export default { 42 | connect, 43 | bind 44 | }; 45 | -------------------------------------------------------------------------------- /src/util/node/__test__/resolver.spec.js: -------------------------------------------------------------------------------- 1 | import casual from "casual"; 2 | import Resolver from "../resolver"; 3 | 4 | const sampleData = { 5 | _id: casual.word, 6 | __type: "Test" 7 | }; 8 | 9 | it("Node should only have a __resolveType on the resolver", () => { 10 | const { Node } = Resolver; 11 | 12 | expect(Node.__resolveType).toBeTruthy(); 13 | expect(Object.keys(Node).length).toEqual(1); 14 | expect(Object.keys(Node)[0]).toEqual("__resolveType"); 15 | }); 16 | 17 | it("Node should return the type from the data passed to it", () => { 18 | const { Node } = Resolver; 19 | 20 | const schema = { 21 | getType(type) { 22 | expect(type).toEqual(sampleData.__type); 23 | return type; 24 | } 25 | }; 26 | 27 | const __type = Node.__resolveType(sampleData, null, { schema }); 28 | expect(__type).toEqual(sampleData.__type); 29 | }); 30 | 31 | it("Query node should return the data via the `Node` class", () => { 32 | const { Query } = Resolver; 33 | 34 | const fakeId = casual.word; 35 | const models = { 36 | Node: { 37 | get(id) { 38 | expect(id).toEqual(fakeId); 39 | return sampleData; 40 | } 41 | } 42 | }; 43 | 44 | const data = Query.node(null, { id: fakeId }, { models }); 45 | expect(data).toEqual(sampleData); 46 | }); 47 | -------------------------------------------------------------------------------- /src/rock/models/system/resolver.js: -------------------------------------------------------------------------------- 1 | import { createGlobalId } from "../../../util"; 2 | 3 | export default { 4 | Query: { 5 | definedValues: (_, { limit, id, skip, all }, { models }) => { 6 | const query = { offset: skip }; 7 | if (!all) query.limit = limit; 8 | 9 | return models.Rock.getDefinedValuesByTypeId(id, query); 10 | } 11 | }, 12 | 13 | DefinedValue: { 14 | id: ({ Id }, _, $, { parentType }) => createGlobalId(Id, parentType.name), 15 | _id: ({ Id }) => Id, 16 | value: ({ Value }) => Value, 17 | description: ({ Description }) => Description 18 | }, 19 | 20 | Attribute: { 21 | id: ({ Id }, _, $, { parentType }) => createGlobalId(Id, parentType.name), 22 | key: ({ Key }) => Key, 23 | description: ({ Description }) => Description, 24 | order: ({ Order }) => Order, 25 | values: ({ Id, EntityId }, _, { models, ...rest }) => 26 | models.Rock.getAttributeValuesFromAttributeId( 27 | Id, 28 | { models, ...rest }, 29 | EntityId 30 | ) 31 | }, 32 | 33 | AttributeValue: { 34 | attribute: ({ AttributeId }, _, { models }) => 35 | models.Rock.getAttributeFromId(AttributeId), 36 | id: ({ Id }, _, $, { parentType }) => createGlobalId(Id, parentType.name), 37 | value: ({ Value }) => Value 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /src/rock/models/groups/schema.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | ` 3 | type GroupMember implements Node { 4 | id: ID! 5 | role: String 6 | person: Person 7 | status: Int 8 | } 9 | 10 | type GroupLocation implements Node { 11 | id: ID! 12 | location: Location 13 | } 14 | 15 | # XXX abstract 16 | type GroupSchedule implements Node { 17 | id: ID! 18 | name: String 19 | description: String 20 | start: String 21 | end: String 22 | day: String 23 | time: String 24 | iCal: String 25 | } 26 | 27 | type Group implements Node { 28 | active: Boolean 29 | ageRange: [Int] 30 | campus: Campus 31 | demographic: String 32 | description: String 33 | distance: Float 34 | entityId: Int 35 | id: ID! 36 | guid: String 37 | kidFriendly: Boolean 38 | name: String 39 | photo: String 40 | tags: [DefinedValue] 41 | type: String 42 | groupType: Int 43 | schedule: GroupSchedule 44 | members: [GroupMember] 45 | locations: [GroupLocation] 46 | isLiked: Boolean 47 | } 48 | 49 | type GroupSearch { 50 | count: Int 51 | results: [Group] 52 | } 53 | 54 | type GroupsMutationResponse implements MutationResponse { 55 | error: String 56 | success: Boolean! 57 | code: Int 58 | } 59 | 60 | ` 61 | ]; 62 | -------------------------------------------------------------------------------- /src/apollos/models/users/schema.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | ` 3 | type Hashes { 4 | when: String! 5 | hashedToken: ID! 6 | } 7 | 8 | type UserTokens { 9 | tokens: [Hashes] 10 | } 11 | 12 | type UserRock { 13 | id: Int 14 | alias: Int 15 | } 16 | 17 | type UserService { 18 | rock: UserRock 19 | resume: UserTokens 20 | } 21 | 22 | type UserEmail { 23 | address: String! 24 | verified: Boolean 25 | } 26 | 27 | type User implements Node { 28 | id: ID! 29 | # We should investigate how best to represent dates 30 | createdAt: String! 31 | services: UserService @deprecated(reason: "This is a private server-only field") 32 | emails: [UserEmail] @deprecated(reason: "Use email instead") 33 | email: String 34 | followedTopics: [String] 35 | } 36 | 37 | type LoginMutationResponse { 38 | id: ID! 39 | token: String 40 | } 41 | 42 | input UserProfileInput { 43 | NickName: String 44 | FirstName: String 45 | LastName: String 46 | Email: String 47 | BirthMonth: String 48 | BirthDay: String 49 | BirthYear: String 50 | Campus: ID 51 | } 52 | 53 | input HomeAddressInput { 54 | Street1: String 55 | Street2: String 56 | City: String 57 | State: String 58 | PostalCode: String 59 | } 60 | ` 61 | ]; 62 | -------------------------------------------------------------------------------- /src/__tests__/server.integration.js: -------------------------------------------------------------------------------- 1 | // import express from "express"; 2 | // import { apolloExpress } from "apollo-server"; 3 | // import { tester } from "graphql-tester"; 4 | // import { create } from "graphql-tester/lib/main/servers/express"; 5 | // import bodyParser from "body-parser"; 6 | // import { createApp } from "../schema"; 7 | 8 | // let Heighliner; 9 | // beforeEach(async () => { 10 | // const app = express(); 11 | // const { graphql } = await createApp(); 12 | 13 | // app.use(bodyParser.json()); 14 | 15 | // app.use("/graphql", apolloExpress(graphql)); 16 | 17 | // Heighliner = tester({ 18 | // server: create(app), 19 | // url: "/graphql", 20 | // contentType: "application/json", 21 | // }); 22 | // }); 23 | 24 | xit("Valid queries should return success", () => 25 | Heighliner(JSON.stringify({ query: "{ currentUser { id } }" })).then( 26 | response => { 27 | expect(response.success).toBeTruthy(); 28 | expect(response.status).toEqual(200); 29 | expect(response.data).toBeTruthy(); 30 | } 31 | )); 32 | 33 | xit("Invalid queries should fail", () => 34 | Heighliner(JSON.stringify({ query: "{ foobar { id } }" })).then(response => { 35 | expect(response.success).toBeFalsy; 36 | expect(response.status).toEqual(400); 37 | expect(response.errors).toBeTruthy(); 38 | })); 39 | -------------------------------------------------------------------------------- /src/esv/fetch.js: -------------------------------------------------------------------------------- 1 | import fetch from "isomorphic-fetch"; 2 | 3 | export function connect() { 4 | return Promise.resolve(!!process.env.ESV_KEY); 5 | } 6 | 7 | export class ESVFetchConnector { 8 | baseUrl = "http://www.esvapi.org/v2/rest/passageQuery"; 9 | key = process.env.ESV_KEY; 10 | count = 0; 11 | 12 | getFromAPI(query) { 13 | const label = `ESVFetchConnector${this.getCount()}`; 14 | 15 | const request = this.getRequest(query); 16 | 17 | const headers = { 18 | "user-agent": "Heighliner", 19 | "Content-Type": "application/text" 20 | }; 21 | 22 | const options = { method: "GET", headers }; 23 | 24 | console.time(label); 25 | 26 | return fetch(request, options) 27 | .then(x => x.text()) 28 | .then(x => { 29 | console.timeEnd(label); 30 | return x; 31 | }); 32 | } 33 | 34 | getRequest(query) { 35 | let request = `${this.baseUrl}?key=${this.key}`; 36 | request += `&passage=${query}`; 37 | request += "&include-headings=false"; 38 | request += "&include-passage-references=false"; 39 | request += "&include-footnotes=false"; 40 | request += "&include-audio-link=false"; 41 | request += "&include-short-copyright=false"; 42 | return request; 43 | } 44 | 45 | getCount() { 46 | this.count++; 47 | return this.count; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const nodeExternals = require("webpack-node-externals"); 2 | const DotenvPlugin = require("webpack-dotenv-plugin"); 3 | const NpmInstallPlugin = require("npm-install-webpack-plugin"); 4 | const { getIfUtils, removeEmpty } = require("webpack-config-utils"); 5 | const webpack = require("webpack"); 6 | const path = require("path"); 7 | 8 | const { ifProduction, ifNotProduction } = getIfUtils(process.env.NODE_ENV); 9 | 10 | module.exports = { 11 | entry: "./src/server.js", 12 | target: "node", 13 | output: { 14 | path: path.resolve(__dirname, "./lib"), 15 | filename: "server.js", 16 | }, 17 | recordsPath: path.resolve(__dirname, "lib/_records"), 18 | plugins: removeEmpty([ 19 | ifProduction(new webpack.optimize.DedupePlugin()), 20 | ifProduction(new webpack.optimize.UglifyJsPlugin({ 21 | compress: { 22 | screw_ie8: true, 23 | warnings: false, 24 | }, 25 | })), 26 | ifNotProduction(new NpmInstallPlugin()), 27 | ifNotProduction(new webpack.HotModuleReplacementPlugin()), 28 | new webpack.NoEmitOnErrorsPlugin(), 29 | ifNotProduction(ifNotProduction() && new DotenvPlugin({ sample: "./.env.example" })), 30 | ]), 31 | module: { 32 | loaders: [ 33 | { 34 | test: /\.js$/, 35 | exclude: /(node_modules|bower_components)/, 36 | loader: "babel-loader", 37 | }, 38 | ], 39 | }, 40 | externals: [nodeExternals()], 41 | }; 42 | -------------------------------------------------------------------------------- /src/util/node/model.js: -------------------------------------------------------------------------------- 1 | 2 | import Crypto from "crypto"; 3 | const secret = process.env.SECRET || "LZEVhlgzFZKClu1r"; 4 | 5 | export default class Node { 6 | 7 | constructor(context) { 8 | this.models = context.models; 9 | } 10 | 11 | // XXX what do we want to do about errors here? 12 | async get(encodedId) { 13 | const { __type, id } = parseGlobalId(encodedId); 14 | 15 | if (!this.models || !this.models[__type] || !this.models[__type].getFromId) { 16 | return Promise.reject(`No model found using ${__type}`); 17 | } 18 | 19 | try { 20 | const data = await (this.models[__type].getFromId(id, encodedId)); 21 | if(!data) return null; 22 | data.__type = __type; 23 | return data; 24 | } catch (e) { 25 | return Promise.reject(e.message); 26 | } 27 | } 28 | 29 | } 30 | 31 | export function createGlobalId(id, type) { 32 | const cipher = Crypto.createCipher("aes192", secret); 33 | 34 | let encrypted = cipher.update(`${type}:${id}`, "utf8", "hex"); 35 | encrypted += cipher.final("hex"); 36 | 37 | return encodeURI(encrypted); 38 | } 39 | 40 | export function parseGlobalId(encodedId) { 41 | const decipher = Crypto.createDecipher("aes192", secret); 42 | 43 | let decrypted = decipher.update(decodeURI(encodedId), "hex", "utf8"); 44 | decrypted += decipher.final("utf8"); 45 | 46 | const [__type, id] = decrypted.toString().split(":"); 47 | return { __type, id }; 48 | } 49 | -------------------------------------------------------------------------------- /src/util/__tests__/heighliner.spec.js: -------------------------------------------------------------------------------- 1 | import { 2 | createQueries, 3 | createMutations, 4 | // loadApplications, // XXX test this 5 | // createSchema, // XXX test this 6 | } from "../heighliner"; 7 | 8 | xit("`createQueries` should return an array with `type Query`", () => { 9 | const queries = createQueries([]); 10 | expect(/type Query/.test(queries.join(" "))).toBeTruthy(); 11 | }); 12 | 13 | xit("`createQueries` should include the node interface", () => { 14 | const queries = createQueries([]); 15 | expect(/node\(id: ID!\): Node/.test(queries.join(" "))).toBeTruthy(); 16 | }); 17 | 18 | xit("`createQueries` should allow passing in new queries", () => { 19 | const queries = createQueries(["foo: Node"]); 20 | expect(/foo: Node/.test(queries.join(" "))).toBeTruthy(); 21 | }); 22 | 23 | xit("`createMutations` should return an array with `type Mutation`", () => { 24 | const mutations = createMutations([]); 25 | expect(/type Mutation/.test(mutations.join(" "))).toBeTruthy(); 26 | }); 27 | 28 | xit("`createMutations` should include the cache interface", () => { 29 | const mutations = createMutations([]); 30 | expect(/cache\(id: ID!, type: String\): Node/.test(mutations.join(" "))).toBeTruthy(); 31 | }); 32 | 33 | xit("`createMutations` should allow passing in new mutations", () => { 34 | const mutations = createMutations(["foo(id: String): Node"]); 35 | expect(/foo\(id: String\): Node/.test(mutations.join(" "))).toBeTruthy(); 36 | }); 37 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required # force to run in container 2 | language: node_js 3 | node_js: 4 | - '6' 5 | cache: 6 | yarn: true 7 | directories: 8 | - $HOME/.npm 9 | - $HOME/.yarn-cache 10 | - node_modules 11 | before_install: 12 | - echo 'America/New_York' | sudo tee /etc/timezone 13 | - sudo dpkg-reconfigure --frontend noninteractive tzdata 14 | - chmod +x $TRAVIS_BUILD_DIR/.travis/run_on_pull_requests 15 | - chmod +x $TRAVIS_BUILD_DIR/.travis/run_for_coverage 16 | - npm install -g coveralls 17 | script: 18 | - npm i 19 | # - yarn danger 20 | - yarn test 21 | after_success: 22 | - 'if [ "$TRAVIS_EVENT_TYPE" == "cron" ]; then bash ./.travis/run_for_coverage; fi' 23 | - 'if [ "$TRAVIS_PULL_REQUEST" != "false" ]; then bash ./.travis/run_on_pull_requests; fi' 24 | notifications: 25 | slack: 26 | secure: enlvPiXHu5nK5VQORUioMTj5eCHBUpriMDevnWSYPYQ9nGJgaPROOEeki9I4LkF+Y6BDI+8ijWwwg8f3bdZKclSzbf0ZOw8bE8THc9sJfBPmbYX86TvvnuLBXB/OHY0lP58lLd+MZI7BycNlg8SKaUICrYApbOEq8PlVaokCnTTDgn2Y4kwpWQBkMKBckIxP6HxI9sREzoG7DFPcD/wCoMS0M8h7bw/cfUukQEczwsOS0AcmEcLRUHQW+5n7qCRtVAO6MQ5WHgE29q37nk2iXf36c6sBJWePvB9Kp/0r7gRHGqC8Kr352LF99O3Lu55ZcjoTiqngQVDPkcghkMnKSIIh5P5KSkk55n15qFBqc3L/KCMnyJApcNyMaGiRmW/PMQ9YT9lg0nmUdG+WTvXrsTv4hIqJ4w5UqXuJS6XQEKcykIr2ozqZEaYngSIUrmHGUiFQSFPpZ5wky3lY0XeEgq0fE+pm0epevJ4xwm3YAPllBbCr+QOzHg4lD0g4kw3ufQLOIEIZRefs0LSFO6O7MsKEC1qGc0Do7EpTX6D7WueP9sdidmNjyyt/NGE8rAK8I+At+O/HyKTfrq6buYuYVW2fn3fTRxuHXOkM7aehoQQrDAxN/I7/CCh+rwdYqq/q3Iji979iqXC9goASANYYg/PIHdVwiN0/OXXvP+h+aR0= 27 | -------------------------------------------------------------------------------- /src/rock/models/feeds/__tests__/__snapshots__/resolver.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Feed Query should return a list of user's likes with correct filter 1`] = ` 4 | Array [ 5 | Object { 6 | "__type": "Content", 7 | "channelName": "articles", 8 | "content": Object { 9 | "images": Array [ 10 | Object { 11 | "label": "2:1", 12 | "url": "//drhztd8q3iayu.cloudfront.net/newspring/editorial/articles/newspring.blog.hero.monasterypews.large.jpg", 13 | }, 14 | ], 15 | }, 16 | "id": "16c44ac3fe07af726455feac35ab2be9", 17 | "title": "One place where everyone is welcome", 18 | }, 19 | ] 20 | `; 21 | 22 | exports[`Feed Query should return array of combined Transaction and SavedPayment data 1`] = ` 23 | Array [ 24 | Object { 25 | "__type": "Transaction", 26 | "date": "today", 27 | "details": "Details Object", 28 | "entityId": 3, 29 | "id": 2, 30 | "payment": "Payment Object", 31 | "person": "12345-ABCDE", 32 | "schedule": "Schedule Object", 33 | "status": "sample status", 34 | "summary": "sample summary", 35 | }, 36 | Object { 37 | "Name": "my saved payment", 38 | "__type": "SavedPayment", 39 | "code": "1234567812345678", 40 | "date": "today", 41 | "entityId": 5, 42 | "expirationMonth": "123456", 43 | "expirationYear": "12345678", 44 | "guid": "1234567890", 45 | "id": 4, 46 | "payment": "Payment Object", 47 | }, 48 | ] 49 | `; 50 | -------------------------------------------------------------------------------- /src/rock/models/people/schema.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | ` 3 | type PhoneNumber implements Node { 4 | id: ID! 5 | countryCode: String 6 | description: String 7 | canText: Boolean! 8 | rawNumber: String! 9 | number: String! 10 | person: Person 11 | } 12 | 13 | type Person implements Node { 14 | id: ID! 15 | entityId: Int! 16 | guid: String! 17 | firstName: String! 18 | lastName: String! 19 | nickName: String 20 | phoneNumbers: [PhoneNumber] 21 | photo: String 22 | age: String 23 | birthDate: String 24 | birthDay: Int 25 | birthMonth: Int 26 | birthYear: Int 27 | email: String 28 | impersonationParameter(expireDateTime: String, pageId: Int, usageLimit: Int): String 29 | campus(cache: Boolean = true): Campus 30 | home(cache: Boolean = true): Location 31 | roles(cache: Boolean = true): [Group] 32 | attributes(key: String): [Attribute] 33 | groups(cache: Boolean = true, groupTypeIds: [Int] = []): [Group] 34 | followedTopics: [String] 35 | } 36 | 37 | type PhoneNumberMutationResponse implements MutationResponse { 38 | error: String 39 | success: Boolean! 40 | code: Int 41 | } 42 | 43 | type DeviceRegistrationMutationResponse implements MutationResponse { 44 | error: String 45 | success: Boolean! 46 | code: Int 47 | } 48 | 49 | type AttributeValueMutationResponse implements MutationResponse { 50 | error: String 51 | success: Boolean! 52 | code: Int 53 | } 54 | ` 55 | ]; 56 | -------------------------------------------------------------------------------- /src/google-site-search/models/search/__test__/queries.integration.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import { apolloExpress } from "apollo-server-express"; 3 | import { tester } from "graphql-tester"; 4 | import { create } from "graphql-tester/lib/main/servers/express"; 5 | import bodyParser from "body-parser"; 6 | import { createApp } from "../../../../schema"; 7 | 8 | let Heighliner; 9 | beforeEach(async () => { 10 | const app = express(); 11 | const { graphql } = await createApp(); 12 | 13 | app.use(bodyParser.json()); 14 | 15 | app.use("/graphql", apolloExpress(graphql)); 16 | 17 | Heighliner = tester({ 18 | server: create(app), 19 | url: "/graphql", 20 | contentType: "application/json" 21 | }); 22 | }); 23 | 24 | xit("Valid queries should return success", async () => { 25 | const response = await Heighliner( 26 | JSON.stringify({ 27 | query: ` 28 | query GetSearch { 29 | search(query: "hey", first: 1, after: 0, site: "example.com") { 30 | total 31 | next 32 | previous 33 | items { 34 | id 35 | title 36 | htmlTitle 37 | link 38 | displayLink 39 | description 40 | htmlDescription 41 | type 42 | section 43 | image 44 | } 45 | } 46 | } 47 | ` 48 | }) 49 | ); 50 | 51 | expect(response.success).toBeTruthy(); 52 | expect(response.status).toEqual(200); 53 | expect(response.data).toBeTruthy(); 54 | }); 55 | -------------------------------------------------------------------------------- /src/rock/models/finances/util/language.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 100: "Transaction approved", 3 | 200: "We're sorry, your transaction was declined by our payment processer", 4 | 201: "It looks like this may be a fraudlent charge. Please contact your paymenr provider", 5 | 202: "We're sorry, your account has insufficient funds", 6 | 203: "We're sorry, this transaction is over your account limit", 7 | 204: "We're sorry, this transaction was not allowed", 8 | 220: "It looks like your payment information is incorrect", 9 | 221: "We couldn't find your dard issuer", 10 | 222: "Your card number is invalid", 11 | 223: "Your card has expired", 12 | 224: "Your expiration date is invalid", 13 | 225: "Your security code is invalid", 14 | // eslint-disable-next-line max-len 15 | 240: "Please call your account provider for more information as to why this transaction could not be processed", 16 | 250: "Please call our finance team", 17 | 251: "This card has been reported as lost", 18 | 252: "This card has been reported as stolen", 19 | 253: "This card is not valid", 20 | 260: "result-text", 21 | 400: "There was an error with our payment processor, you were not charged, please try again", 22 | 410: "We had an issue processing transactions, please try again", 23 | 411: "Looks like we are having connection issues with our processor, please try again later", 24 | // eslint-disable-next-line max-len 25 | 430: "This looks like a duplicate transaction, for your safety, we have declined the transaction. Please try again in five minutes" 26 | }; 27 | -------------------------------------------------------------------------------- /src/apollos/models/users/__tests__/queries.integration.js: -------------------------------------------------------------------------------- 1 | // import express from "express"; 2 | // import { apolloExpress } from "apollo-server"; 3 | // import { tester } from "graphql-tester"; 4 | // import { create } from "graphql-tester/lib/main/servers/express"; 5 | // import bodyParser from "body-parser"; 6 | // import { createApp } from "../../../../schema"; 7 | 8 | // let Heighliner; 9 | // beforeEach(async () => { 10 | // const app = express(); 11 | // const { graphql } = await createApp(); 12 | 13 | // app.use(bodyParser.json()); 14 | 15 | // app.use("/graphql", apolloExpress(graphql)); 16 | 17 | // Heighliner = tester({ 18 | // server: create(app), 19 | // url: "/graphql", 20 | // contentType: "application/json", 21 | // }); 22 | // }); 23 | 24 | xit("Valid queries should return success", async () => { 25 | const response = await Heighliner( 26 | JSON.stringify({ 27 | query: ` 28 | query CurrentUser { 29 | currentUser { 30 | id 31 | createdAt 32 | emails { 33 | address 34 | } 35 | services { 36 | rock { 37 | id 38 | alias 39 | } 40 | resume { 41 | tokens { 42 | when 43 | hashedToken 44 | } 45 | } 46 | } 47 | } 48 | } 49 | ` 50 | }) 51 | ); 52 | 53 | expect(response.success).toBeTruthy(); 54 | expect(response.status).toEqual(200); 55 | expect(response.data).toBeTruthy(); 56 | }); 57 | -------------------------------------------------------------------------------- /src/rock/models/binary-files/tables.js: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-shadowed-variable */ 2 | 3 | import { INTEGER, STRING, BOOLEAN } from "sequelize"; 4 | 5 | import { MSSQLConnector } from "../../mssql"; 6 | 7 | const binaryFileSchema = { 8 | Id: { type: INTEGER, primaryKey: true }, 9 | BinaryFileTypeId: { type: INTEGER }, 10 | Description: { type: STRING }, 11 | FileName: { type: STRING }, 12 | IsSystem: { type: BOOLEAN }, 13 | IsTemporary: { type: BOOLEAN }, 14 | MimeType: { type: STRING }, 15 | Path: { type: STRING }, 16 | StorageEntitySettings: { type: String }, 17 | StorageEntityTypeId: { type: INTEGER } 18 | }; 19 | 20 | const binaryFileTypeSchema = { 21 | Id: { type: INTEGER, primaryKey: true }, 22 | AllowCaching: { type: BOOLEAN }, 23 | Description: { type: STRING }, 24 | IsSystem: { type: BOOLEAN }, 25 | Name: { type: STRING }, 26 | StorageEntityTypeId: { type: INTEGER } 27 | }; 28 | 29 | let BinaryFile; 30 | let BinaryFileType; 31 | export { BinaryFile, binaryFileSchema, BinaryFileType, binaryFileTypeSchema }; 32 | 33 | export function connect() { 34 | BinaryFile = new MSSQLConnector("BinaryFile", binaryFileSchema); 35 | BinaryFileType = new MSSQLConnector("BinaryFileType", binaryFileTypeSchema); 36 | 37 | return { 38 | BinaryFile, 39 | BinaryFileType 40 | }; 41 | } 42 | 43 | export function bind({ BinaryFile }) { 44 | BinaryFile.model.belongsTo(BinaryFileType.model, { 45 | foreignKey: "BinaryFileTypeId", 46 | targetKey: "Id" 47 | }); 48 | } 49 | 50 | export default { 51 | connect, 52 | bind 53 | }; 54 | -------------------------------------------------------------------------------- /src/util/cache/memory-cache.js: -------------------------------------------------------------------------------- 1 | import Crypto from "crypto"; 2 | 3 | export class InMemoryCache { 4 | constructor(cache = {}, secret = "InMemoryCache") { 5 | // XXX this is really only used for testing purposes 6 | this.cache = cache; 7 | this.secret = secret; 8 | } 9 | 10 | get(id, lookup, { ttl, cache } = { ttl: 86400, cache: true }) { 11 | let fromCache = false; 12 | return new Promise(done => { 13 | const data = this.cache[id]; 14 | if ((!data || !cache) && lookup) return lookup().then(done); 15 | 16 | fromCache = true; 17 | return done(data); 18 | }).then(data => { 19 | if (data && !fromCache) { 20 | // async the save 21 | process.nextTick(() => { 22 | this.set(id, data, ttl); 23 | }); 24 | } 25 | 26 | return data; 27 | }); 28 | } 29 | 30 | set(id, data, ttl = 86400) { 31 | return new Promise(done => { 32 | // XXX this should technically never fail 33 | try { 34 | // save to cache 35 | this.cache[id] = data; 36 | 37 | // clear cache 38 | setTimeout(() => { 39 | delete this.cache[id]; 40 | }, ttl * 60); 41 | 42 | return done(true); 43 | } catch (e) { 44 | return done(false); 45 | } 46 | }); 47 | } 48 | 49 | del(id) { 50 | delete this.cache[id]; 51 | } 52 | 53 | encode(obj, prefix = "") { 54 | const cipher = Crypto.createHmac("sha256", this.secret); 55 | const str = `${prefix}${JSON.stringify(obj)}`; 56 | return cipher.update(str, "utf-8").digest("hex"); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/rock/models/campuses/model.js: -------------------------------------------------------------------------------- 1 | import { merge } from "lodash"; 2 | import { defaultCache } from "../../../util/cache"; 3 | import { createGlobalId } from "../../../util"; 4 | 5 | import { 6 | Campus as CampusTable, 7 | Location as LocationTable // XXX move to its own model 8 | } from "./tables"; 9 | 10 | import { Rock } from "../system"; 11 | 12 | export class Campus extends Rock { 13 | __type = "Campus"; 14 | 15 | constructor({ cache } = { cache: defaultCache }) { 16 | super(); 17 | this.cache = cache; 18 | } 19 | 20 | async getFromId(id, globalId) { 21 | globalId = globalId || createGlobalId(`${id}`, this.__type); 22 | return this.cache.get(globalId, () => 23 | CampusTable.findOne({ where: { Id: id } }) 24 | ); 25 | } 26 | 27 | async findByLocationId(id, globalId) { 28 | globalId = globalId || createGlobalId(`${id}`, "Location"); 29 | return this.cache.get(globalId, () => 30 | LocationTable.findOne({ where: { Id: id } }) 31 | ); 32 | } 33 | 34 | // async findByPersonId(id) { 35 | // return 36 | // } 37 | 38 | async find(query) { 39 | query = merge({ IsActive: true }, query); 40 | for (const key in query) { 41 | if (!query[key]) delete query[key]; 42 | } 43 | return this.cache 44 | .get(this.cache.encode(query), () => 45 | CampusTable.find({ 46 | where: query, 47 | attributes: ["Id"] 48 | }) 49 | ) 50 | .then(this.getFromIds.bind(this)) 51 | .then(x => x.filter(y => y.Name !== "Central")); 52 | } 53 | } 54 | 55 | export default { 56 | Campus 57 | }; 58 | -------------------------------------------------------------------------------- /src/rock/models/system/__tests__/resolver.spec.js: -------------------------------------------------------------------------------- 1 | import Resolver from "../resolver"; 2 | 3 | describe("Attribute", () => { 4 | const { Attribute } = Resolver; 5 | 6 | it("has id", () => { 7 | expect( 8 | Attribute.id({ Id: 4 }, null, null, { parentType: { name: "boi" } }) 9 | ).toMatchSnapshot(); 10 | }); 11 | it("has key", () => { 12 | expect(Attribute.key({ Key: 2 })).toEqual(2); 13 | }); 14 | it("has description", () => { 15 | expect(Attribute.description({ Description: "boi" })).toEqual("boi"); 16 | }); 17 | it("has order", () => { 18 | expect(Attribute.order({ Order: 2 })).toEqual(2); 19 | }); 20 | it("has values", () => { 21 | const models = { Rock: { getAttributeValuesFromAttributeId: jest.fn() } }; 22 | Attribute.values({ Id: 2, EntityId: 43 }, null, { models }); 23 | expect(models.Rock.getAttributeValuesFromAttributeId).toBeCalledWith( 24 | 2, 25 | { models }, 26 | 43 27 | ); 28 | }); 29 | }); 30 | 31 | describe("AttributeValue", () => { 32 | const { AttributeValue } = Resolver; 33 | 34 | it("has attribute", () => { 35 | const models = { Rock: { getAttributeFromId: jest.fn() } }; 36 | AttributeValue.attribute({ AttributeId: 5 }, null, { models }); 37 | expect(models.Rock.getAttributeFromId).toBeCalledWith(5); 38 | }); 39 | it("has id", () => { 40 | expect( 41 | AttributeValue.id({ Id: 4 }, null, null, { parentType: { name: "boi" } }) 42 | ).toMatchSnapshot(); 43 | }); 44 | it("has value", () => { 45 | expect(AttributeValue.value({ Value: "yo" })).toEqual("yo"); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /src/rock/models/campuses/tables.js: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-shadowed-variable */ 2 | 3 | import { INTEGER, STRING, BOOLEAN, GEOGRAPHY } from "sequelize"; 4 | 5 | import { MSSQLConnector } from "../../mssql"; 6 | 7 | const campusSchema = { 8 | Id: { type: INTEGER, primaryKey: true }, 9 | Name: { type: STRING }, 10 | Guid: { type: STRING }, 11 | ShortCode: { type: STRING }, 12 | Url: { type: STRING }, 13 | LocationID: { type: INTEGER }, 14 | PhoneNumber: { type: STRING }, 15 | Description: { type: STRING }, 16 | ServiceTimes: { type: STRING }, 17 | IsActive: { type: BOOLEAN } 18 | }; 19 | 20 | const locationSchema = { 21 | Id: { type: INTEGER, primaryKey: true }, 22 | Name: { type: STRING }, 23 | IsActive: { type: BOOLEAN }, 24 | LocationTypeValueId: { type: INTEGER }, 25 | GeoPoint: { type: GEOGRAPHY }, 26 | GeoFence: { type: GEOGRAPHY }, 27 | Street1: { type: STRING }, 28 | Street2: { type: STRING }, 29 | City: { type: STRING }, 30 | State: { type: STRING }, 31 | Country: { type: STRING }, 32 | PostalCode: { type: STRING } 33 | }; 34 | 35 | let Campus; 36 | let Location; 37 | export { Campus, campusSchema, Location, locationSchema }; 38 | 39 | export function connect() { 40 | Campus = new MSSQLConnector("Campus", campusSchema); 41 | Location = new MSSQLConnector("Location", locationSchema); 42 | 43 | return { 44 | Campus, 45 | Location 46 | }; 47 | } 48 | 49 | export function bind({ Campus, Location }) { 50 | Campus.model.belongsTo(Location.model, { 51 | foreignKey: "LocationId", 52 | targetKey: "Id" 53 | }); 54 | } 55 | 56 | export default { 57 | connect, 58 | bind 59 | }; 60 | -------------------------------------------------------------------------------- /src/rock/models/finances/util/__tests__/__snapshots__/language.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`should match expected codes 1`] = ` 4 | Object { 5 | "100": "Transaction approved", 6 | "200": "We're sorry, your transaction was declined by our payment processer", 7 | "201": "It looks like this may be a fraudlent charge. Please contact your paymenr provider", 8 | "202": "We're sorry, your account has insufficient funds", 9 | "203": "We're sorry, this transaction is over your account limit", 10 | "204": "We're sorry, this transaction was not allowed", 11 | "220": "It looks like your payment information is incorrect", 12 | "221": "We couldn't find your dard issuer", 13 | "222": "Your card number is invalid", 14 | "223": "Your card has expired", 15 | "224": "Your expiration date is invalid", 16 | "225": "Your security code is invalid", 17 | "240": "Please call your account provider for more information as to why this transaction could not be processed", 18 | "250": "Please call our finance team", 19 | "251": "This card has been reported as lost", 20 | "252": "This card has been reported as stolen", 21 | "253": "This card is not valid", 22 | "260": "result-text", 23 | "400": "There was an error with our payment processor, you were not charged, please try again", 24 | "410": "We had an issue processing transactions, please try again", 25 | "411": "Looks like we are having connection issues with our processor, please try again later", 26 | "430": "This looks like a duplicate transaction, for your safety, we have declined the transaction. Please try again in five minutes", 27 | } 28 | `; 29 | -------------------------------------------------------------------------------- /src/rock/models/finances/models/FinancialAccount.js: -------------------------------------------------------------------------------- 1 | import { merge, isUndefined } from "lodash"; 2 | import { createGlobalId } from "../../../../util"; 3 | 4 | import { FinancialAccount as FinancialAccountTable } from "../tables"; 5 | 6 | import { Rock } from "../../system"; 7 | 8 | export default class FinancialAccount extends Rock { 9 | __type = "FinancialAccount"; 10 | 11 | async getFromId(id, globalId) { 12 | globalId = globalId || createGlobalId(id, this.__type); 13 | return this.cache.get(globalId, () => 14 | FinancialAccountTable.findOne({ where: { Id: id } }).then(x => { 15 | // if this is a children fund, lets get the parent 16 | if (!x.ParentAccountId) return x; 17 | 18 | return FinancialAccountTable.findOne({ 19 | where: { Id: x.ParentAccountId } 20 | }); 21 | }) 22 | ); 23 | } 24 | 25 | async find(where, { all }) { 26 | for (const key in where) { 27 | if (isUndefined(where[key])) delete where[key]; 28 | } 29 | // defaults 30 | where = merge( 31 | { 32 | ParentAccountId: null, 33 | PublicDescription: { 34 | $and: { 35 | $ne: "", 36 | $not: null 37 | } 38 | }, 39 | IsTaxDeductible: true 40 | }, 41 | where 42 | ); 43 | 44 | if (all) { 45 | where = { ParentAccountId: null, IsTaxDeductible: true }; 46 | } 47 | 48 | return await this.cache.get(this.cache.encode(where), () => 49 | FinancialAccountTable.find({ 50 | where, 51 | attributes: ["Id"], 52 | order: ["Order"] 53 | }).then(this.getFromIds.bind(this)) 54 | ); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/routes/datadog.js: -------------------------------------------------------------------------------- 1 | import Metrics from "datadog-metrics"; 2 | 3 | export default app => { 4 | let dogstatsd; 5 | if (process.env.DATADOG_API_KEY && process.env.NODE_ENV === "production") { 6 | dogstatsd = new Metrics.BufferedMetricsLogger({ 7 | apiKey: process.env.DATADOG_API_KEY, 8 | appKey: process.env.DATADOG_APP_KEY, 9 | prefix: `heighliner.${process.env.SENTRY_ENVIRONMENT}.`, 10 | flushIntervalSeconds: 15 11 | }); 12 | 13 | setInterval(() => { 14 | const memUsage = process.memoryUsage(); 15 | dogstatsd.gauge("memory.rss", memUsage.rss); 16 | dogstatsd.gauge("memory.heapTotal", memUsage.heapTotal); 17 | dogstatsd.gauge("memory.heapUsed", memUsage.heapUsed); 18 | }, 5000); 19 | } 20 | 21 | // datadog 22 | if (dogstatsd) { 23 | app.use((req, res, next) => { 24 | if (!req._startTime) req._startTime = new Date(); 25 | const end = res.end; 26 | res.end = (chunk, encoding) => { 27 | res.end = end; 28 | res.end(chunk, encoding); 29 | const baseUrl = req.baseUrl; 30 | const statTags = [`route:${baseUrl}${req.path}`]; 31 | 32 | statTags.push(`method:${req.method.toLowerCase()}`); 33 | statTags.push(`protocol:${req.protocol}`); 34 | statTags.push(`path:${baseUrl}${req.path}`); 35 | statTags.push(`response_code:${res.statusCode}`); 36 | 37 | dogstatsd.increment(`response_code.${res.statusCode}`, 1, statTags); 38 | dogstatsd.increment("response_code.all", 1, statTags); 39 | 40 | const now = new Date() - req._startTime; 41 | dogstatsd.histogram("response_time", now, statTags); 42 | }; 43 | 44 | next(); 45 | }); 46 | } 47 | return dogstatsd; 48 | }; 49 | -------------------------------------------------------------------------------- /src/rock/models/finances/mutations.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | `syncTransactions( 3 | condition: String, 4 | transaction_type: String, 5 | action_type: String, 6 | transaction_id: String, 7 | order_id: String, 8 | last_name: String, 9 | email: String, 10 | start_date: String, 11 | end_date: String, 12 | personId: Int, 13 | gateway: String = "${process.env.NMI_GATEWAY}", 14 | ): [Transaction]`, 15 | 16 | `createOrder( 17 | data: String! 18 | id: ID 19 | instant: Boolean = false, 20 | gateway: String = "${process.env.NMI_GATEWAY}", 21 | url: String 22 | ): OrderMutationResponse`, 23 | 24 | `completeOrder( 25 | token: ID! 26 | scheduleId: ID 27 | platform: String 28 | accountName: String 29 | gateway: String = "${process.env.NMI_GATEWAY}", 30 | ): CompleteOrderMutationResponse`, 31 | 32 | `validate( 33 | token: ID! 34 | gateway: String = "${process.env.NMI_GATEWAY}", 35 | ): ValidateMutationResponse`, 36 | 37 | `cancelSavedPayment( 38 | id: ID 39 | entityId: Int 40 | gateway: String = "${process.env.NMI_GATEWAY}", 41 | ): SavePaymentMutationResponse`, 42 | 43 | `savePayment( 44 | token: ID! 45 | accountName: String 46 | gateway: String = "${process.env.NMI_GATEWAY}", 47 | ): SavePaymentMutationResponse`, 48 | 49 | `updateSavedPayment( 50 | entityId: Int 51 | name: String! 52 | ): SavePaymentMutationResponse`, 53 | 54 | `cancelSchedule( 55 | id: ID 56 | entityId: Int 57 | gateway: String = "${process.env.NMI_GATEWAY}", 58 | ): ScheduledTransactionMutationResponse`, 59 | 60 | `transactionStatement( 61 | limit: Int 62 | skip: Int 63 | people: [Int] 64 | start: String 65 | end: String 66 | ): StatementMutationResponse` 67 | ]; 68 | -------------------------------------------------------------------------------- /src/expression-engine/models/navigation/tables.js: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-shadowed-variable */ 2 | 3 | import { INTEGER, STRING } from "sequelize"; 4 | 5 | import { MySQLConnector } from "../../mysql"; 6 | 7 | const naveeSchema = { 8 | navee_id: { type: INTEGER, primaryKey: true }, 9 | navigation_id: { type: INTEGER }, 10 | site_id: { type: INTEGER }, 11 | entry_id: { type: INTEGER }, 12 | channel_id: { type: INTEGER }, 13 | parent: { type: INTEGER }, 14 | text: { type: STRING }, 15 | link: { type: STRING }, 16 | sort: { type: INTEGER }, 17 | custom: { type: STRING }, 18 | type: { type: STRING } 19 | }; 20 | 21 | const naveeNavSchema = { 22 | navigation_id: { type: INTEGER, primaryKey: true }, 23 | site_id: { type: INTEGER }, 24 | nav_title: { type: INTEGER }, 25 | nav_name: { type: INTEGER } 26 | }; 27 | 28 | let Navee; 29 | let NaveeNav; 30 | export { Navee, naveeSchema, NaveeNav, naveeNavSchema }; 31 | 32 | export function connect() { 33 | Navee = new MySQLConnector("exp_navee", naveeSchema); 34 | NaveeNav = new MySQLConnector("exp_navee_navs", naveeNavSchema); 35 | 36 | return { 37 | Navee, 38 | NaveeNav 39 | }; 40 | } 41 | 42 | export function bind({ Sites, Navee, NaveeNav }) { 43 | NaveeNav.model.hasOne(Navee.model, { foreignKey: "navigation_id" }); 44 | Navee.model.belongsTo(NaveeNav.model, { foreignKey: "navigation_id" }); 45 | 46 | NaveeNav.model.belongsTo(Sites.model, { 47 | foreignKey: "site_id", 48 | targetKey: "site_id" 49 | }); 50 | Sites.model.hasOne(NaveeNav.model, { foreignKey: "site_id" }); 51 | 52 | Navee.model.belongsTo(Sites.model, { 53 | foreignKey: "site_id", 54 | targetKey: "site_id" 55 | }); 56 | Sites.model.hasOne(Navee.model, { foreignKey: "site_id" }); 57 | } 58 | 59 | export default { 60 | connect, 61 | bind 62 | }; 63 | -------------------------------------------------------------------------------- /src/expression-engine/models/ee/matrix.js: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-shadowed-variable */ 2 | 3 | import { INTEGER } from "sequelize"; 4 | 5 | import { MySQLConnector, Tables } from "../../mysql"; 6 | 7 | const matrixSchema = { 8 | row_id: { type: INTEGER, primaryKey: true }, 9 | site_id: { type: INTEGER }, 10 | entry_id: { type: INTEGER }, 11 | field_id: { type: INTEGER } 12 | }; 13 | 14 | const matrixColSchema = { 15 | col_id: { type: INTEGER, primaryKey: true }, 16 | site_id: { type: INTEGER }, 17 | field_id: { type: INTEGER } 18 | }; 19 | 20 | let Matrix; 21 | let MatrixCol; 22 | export { Matrix, matrixSchema, MatrixCol, matrixColSchema }; 23 | 24 | export function connect() { 25 | Matrix = new MySQLConnector("exp_matrix_data", matrixSchema); 26 | MatrixCol = new MySQLConnector("exp_matrix_cols", matrixColSchema); 27 | 28 | return { 29 | Matrix, 30 | MatrixCol 31 | }; 32 | } 33 | 34 | export function bind({ ChannelData, Matrix, MatrixCol, AssetsSelections }) { 35 | // Matrix.model.belongsTo(ChannelData.model, { foreignKey: "entry_id" }); 36 | // Matrix.model.belongsTo(AssetsSelections.model, { foreignKey: "row_id" }); 37 | 38 | // MatrixCol.model.belongsTo(AssetsSelections.model, { foreignKey: "col_id" }); 39 | 40 | // // get access to matrix from channel data 41 | ChannelData.model.hasMany(Matrix.model, { foreignKey: "entry_id" }); 42 | Matrix.model.belongsTo(ChannelData.model, { foreignKey: "entry_id" }); 43 | 44 | // make it possible to get files out of matrix 45 | Matrix.model.hasMany(AssetsSelections.model, { foreignKey: "row_id" }); 46 | MatrixCol.model.hasOne(AssetsSelections.model, { foreignKey: "col_id" }); 47 | 48 | AssetsSelections.model.belongsTo(Matrix.model, { foreignKey: "row_id" }); 49 | AssetsSelections.model.belongsTo(MatrixCol.model, { foreignKey: "col_id" }); 50 | } 51 | 52 | export default { 53 | connect, 54 | bind 55 | }; 56 | -------------------------------------------------------------------------------- /src/expression-engine/models/ee/model.js: -------------------------------------------------------------------------------- 1 | import { Heighliner } from "../../../util"; 2 | 3 | export class EE extends Heighliner { 4 | __type = "RockSystem"; 5 | id = "entry_id"; 6 | 7 | getDate(day, month, year) { 8 | if (!day || !month || !year) 9 | throw new Error("Missing information from `getDate`"); 10 | return `${new Date(Number(year), Number(month) - 1, Number(day))}`; 11 | } 12 | 13 | getDateFromUnix(timestamp) { 14 | return timestamp ? `${new Date(timestamp * 1000)}` : null; 15 | } 16 | 17 | contentImages(markup) { 18 | if (!markup) return []; 19 | 20 | const images = markup.match(/src=".*\.(jpg|jpeg|png)"/gim); 21 | if (!images) return []; 22 | 23 | return images 24 | .filter(x => x.slice(5, -1) !== "") 25 | .map(image => ({ 26 | fileLabel: "inline", 27 | s3: image.slice(5, -1), 28 | url: image.slice(5, -1) 29 | })); 30 | } 31 | 32 | splitByNewLines(tags) { 33 | if (!tags) return []; 34 | 35 | return tags.replace("\\n", ",").split("\n"); 36 | } 37 | 38 | getSeries(series) { 39 | if (!series) return false; 40 | 41 | // format: [dddd] [some-thing] Series Title 42 | // match[1]: series id 43 | // match[2]: series slug 44 | // match[3]: series name 45 | const seriesRegex = /\[(\d*)\] \[(.*)\] (.*)/g; 46 | return seriesRegex.exec(series); 47 | } 48 | 49 | cleanMarkup(markup) { 50 | if (!markup) return false; 51 | 52 | const parsed = markup.match(/src="{assets_\d*.*}"/gim); 53 | if (!parsed) return markup; 54 | 55 | // remove {assets_IDSTRING:} and make protocal relative 56 | markup = markup.replace(/{assets_\d*.*?}/gim, link => { 57 | link = link.trim().substring(0, link.length - 1); 58 | link = link.replace(/{assets_\d*:/gim, ""); 59 | return link; 60 | }); 61 | 62 | // make all links protocal relative 63 | return markup.replace(/https*:\/\//g, "//"); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Heighliner 2 | ========== 3 | 4 | [![NewSpring 5 | Church](https://img.shields.io/badge/NEWSPRING_CHURCH-Heighliner-6BAC43.svg?style=flat&logoWidth=17&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd%2BUAAAABGdBTUEAALGPC/xhBQAAAeFJREFUSA29lU0rRFEYx%2B81k/eFUpO3hUmREpGEYrKQ2VGslLKwkw/iC/AFbKVsvSTFIMVydsSCGHsvmev3THOv6cy5Z17c66nfnDnP23/Oveecsa0yzHGcYdIWYAo6oQMikIFXuIETOLJt%2B4mxOkNoCdJQrn2RuANDFSlSEIcUVGtZCjchWlKYpARkIAg7p0mLryjBBHxCkHZMs9oiUZzyGINamfqDt3WCf3lnqoA6/8Yx4Ikykd0Ytu2LoC0fKKUZ%2BuS7xpbx3Wv8rkt2Yi9MwCrI%2BdSZg7NNxEZKLK1fV63z0WcMng39VmoonNcVV%2BPjlrmibt1QmxTBaUOCNsQK6mASutUERHfx%2BV1vPSIo92LZhkgDyddwBnfM1zTFDxqfuGIVC1Ikr%2BB3i1vWhnRS7EOZu9OcoN%2BucpPUsUtxNCtz07RJVvhmytDEpKbQcker0GH6LsUZU0LQMRG8DbqpqZ8InpoSgo6J4CFkg27s1y/KQX3kLO2RsOiT1Eo8VhBTd2VEiUtq8f9fvoF7eY8zT%2BV9oQ7ySC1WecGwFapSvrl3hngs9fguYTBM4dwKRYBVvjPMgtz4oZknKAqIvjDMwA7IH%2Bb/GY94FA4gUPPeod9SUGsnloQ5iIMcEaERKrYfBD49JTL9FwYAAAAASUVORK5CYII%3D)](https://newspring.cc) 6 | [![Coverage Status](https://coveralls.io/repos/github/NewSpring/Heighliner/badge.svg?branch=master)](https://coveralls.io/github/NewSpring/Heighliner?branch=master) [![Build Status](https://travis-ci.org/NewSpring/Heighliner.svg?branch=master)](https://travis-ci.org/NewSpring/Heighliner) [![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](http://commitizen.github.io/cz-cli/) 7 | 8 | 9 | An Apollo GraphQL server for NewSpring Church 10 | 11 | ### Setup 12 | 13 | Install Docker 14 | 15 | ``` 16 | brew cask install docker 17 | ``` 18 | 19 | Start Docker app. 20 | 21 | ![Docker](https://d1wuojemv4s7aw.cloudfront.net/items/1d0Z0Q3i403G2P1R1f2t/Screen%20Shot%202018-10-23%20at%201.29.27%20PM.png?X-CloudApp-Visitor-Id=3192497&v=4b7a77cc) 22 | 23 | Navgate to the Heighliner directory and create container. 24 | 25 | ``` 26 | docker-compose up 27 | ``` 28 | 29 | Set up yarn 30 | 31 | ``` 32 | yarn 33 | yarn build 34 | ``` 35 | 36 | ### Running the project 37 | 38 | ``` 39 | yarn start 40 | ``` 41 | -------------------------------------------------------------------------------- /src/rock/models/finances/models/FinancialBatch.js: -------------------------------------------------------------------------------- 1 | import uuid from "node-uuid"; 2 | import moment from "moment"; 3 | 4 | import { createGlobalId } from "../../../../util"; 5 | 6 | import { FinancialBatch as FinancialBatchTable } from "../tables"; 7 | 8 | import { Rock } from "../../system"; 9 | 10 | export default class FinancialBatch extends Rock { 11 | __type = "FinancialBatch"; 12 | 13 | async getFromId(id, globalId) { 14 | globalId = globalId || createGlobalId(id, this.__type); 15 | return this.cache.get(globalId, () => 16 | FinancialBatchTable.findOne({ where: { Id: id } }) 17 | ); 18 | } 19 | 20 | async findOrCreate({ 21 | prefix = "Online Giving", 22 | suffix = "", 23 | currencyType, 24 | date 25 | }) { 26 | let paymentType = ""; 27 | if (currencyType) paymentType = currencyType; 28 | 29 | const batchName = `${prefix} ${paymentType} ${suffix}`.trim(); 30 | 31 | const batch = await FinancialBatchTable.find({ 32 | where: { 33 | Status: 1, 34 | BatchStartDateTime: { $lte: date }, 35 | BatchEndDateTime: { $gt: date }, 36 | Name: batchName 37 | } 38 | }); 39 | 40 | if (batch.length) return batch[0]; 41 | 42 | // 4pm => 12:00 am => 11:59 pm day before 43 | const BatchStartDateTime = moment(date) 44 | .startOf("day") 45 | // .subtract(1, "minute") 46 | .toISOString(); 47 | 48 | // 4pm => 12:00 next day => 11:59 pm 49 | const BatchEndDateTime = moment(date) 50 | .endOf("day") // .subtract(1, "minute") 51 | .toISOString(); 52 | 53 | const newBatch = { 54 | Guid: uuid.v4(), 55 | Name: batchName, 56 | Status: 1, 57 | ControlAmount: 0, 58 | // XXX this is actually stored on the payment gateway if we want to make 59 | // it a dynamic value 60 | BatchStartDateTime, 61 | BatchEndDateTime 62 | }; 63 | 64 | const batchId = await FinancialBatchTable.post(newBatch); 65 | return FinancialBatchTable.findOne({ where: { Id: batchId } }); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/rock/models/likes/__tests__/resolver.spec.js: -------------------------------------------------------------------------------- 1 | import Resolver from "../resolver"; 2 | 3 | const mockUser = { PrimaryAliasId: "12345" }; 4 | const mockModels = { 5 | Like: { 6 | toggleLike: jest.fn(), 7 | getRecentlyLiked: jest.fn() 8 | }, 9 | Node: {} 10 | }; 11 | 12 | describe("Likes Mutation", () => { 13 | afterEach(() => { 14 | mockModels.Like.toggleLike.mockReset(); 15 | }); 16 | 17 | it("should return empty array with improper input", () => { 18 | const toggleLike = Resolver.Mutation.toggleLike; 19 | 20 | expect(() => { 21 | toggleLike(null, { nodeId: "1234" }, {}); 22 | }).toThrow(); 23 | }); 24 | 25 | it("should call toggleLike with the correct args", () => { 26 | const toggleLike = Resolver.Mutation.toggleLike; 27 | 28 | const res = toggleLike( 29 | null, 30 | { nodeId: "1234" }, 31 | { models: mockModels, person: mockUser } 32 | ); 33 | expect(mockModels.Like.toggleLike).toHaveBeenCalledWith( 34 | "1234", 35 | "12345", 36 | {} 37 | ); 38 | }); 39 | }); 40 | 41 | describe("getRecentLikes", () => { 42 | afterEach(() => { 43 | mockModels.Like.getRecentlyLiked.mockReset(); 44 | }); 45 | 46 | it("should pass falsy for user, cache, limit, skip when not defined", () => { 47 | const recentlyLiked = Resolver.Query.recentlyLiked; 48 | recentlyLiked(null, {}, { models: mockModels, user: null }); 49 | expect(mockModels.Like.getRecentlyLiked).toBeCalledWith( 50 | { cache: undefined, limit: undefined, skip: undefined }, 51 | null, 52 | {} 53 | ); 54 | }); 55 | 56 | it("should call getRecentlyLiked with proper args", () => { 57 | const recentlyLiked = Resolver.Query.recentlyLiked; 58 | recentlyLiked( 59 | null, 60 | { limit: 0, skip: 1, cache: false }, 61 | { models: mockModels, user: { _id: "harambe" } } 62 | ); 63 | expect(mockModels.Like.getRecentlyLiked).toBeCalledWith( 64 | { cache: false, limit: 0, skip: 1 }, 65 | "harambe", 66 | {} 67 | ); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /src/rock/models/system/fieldTypeResolvers.js: -------------------------------------------------------------------------------- 1 | // import { pick } from "lodash"; 2 | import moment from "moment"; 3 | 4 | function castToBoolean(val) { 5 | if (val.toLowerCase() === "true") return true; 6 | return false; 7 | } 8 | 9 | // XXX there are currently 97 of class types we need to model 10 | export default { 11 | "Rock.Field.Types.TextFieldType": function(value, defaultValue) { 12 | if (!value && defaultValue) return defaultValue; 13 | return value; 14 | }, 15 | "Rock.Field.Types.DateFieldType": function(value, defaultValue) { 16 | if (!value && defaultValue) return defaultValue; 17 | return moment(value).toString(); 18 | }, 19 | "Rock.Field.Types.SelectSingleFieldType": function(value, defaultValue) { 20 | if (!value && defaultValue) return defaultValue; 21 | return value; 22 | }, 23 | "Rock.Field.Types.BooleanFieldType": function(value, defaultValue) { 24 | if (!value && defaultValue) return castToBoolean(defaultValue); 25 | return castToBoolean(value); 26 | }, 27 | "Rock.Field.Types.ImageFieldType": function(value, defaultValue) { 28 | if ( 29 | !this || 30 | !this.models || 31 | !this.models.BinaryFile || 32 | (!value && !defaultValue) 33 | ) 34 | return Promise.resolve({}); 35 | 36 | if (!value && defaultValue) 37 | return this.models.BinaryFile.getFromGuid(defaultValue); 38 | return this.models.BinaryFile.getFromGuid(value); 39 | }, 40 | "Rock.Field.Types.DecimalRangeFieldType": function(value, defaultValue) { 41 | if (!value && !defaultValue) return []; 42 | if (!value && defaultValue) value = defaultValue; 43 | 44 | const range = value.split(","); 45 | return range.map(x => Number(x)); 46 | }, 47 | "Rock.Field.Types.DefinedValueFieldType": function(value, defaultValue) { 48 | if (!this || !this.models || !this.models.Rock || (!value && !defaultValue)) 49 | return Promise.resolve({}); 50 | 51 | if (!value && defaultValue) 52 | return this.models.Rock.getDefinedValueByGuid(defaultValue); 53 | return this.models.Rock.getDefinedValueByGuid(value); 54 | } 55 | }; 56 | -------------------------------------------------------------------------------- /src/apollos/models/users/api.js: -------------------------------------------------------------------------------- 1 | import fetch from "isomorphic-fetch"; 2 | 3 | const baseURL = `${process.env.ROCK_URL}api`; 4 | const CONFIG = { 5 | headers: { 6 | "Authorization-Token": process.env.ROCK_TOKEN, 7 | "Content-Type": "application/json" 8 | } 9 | }; 10 | 11 | async function statusResponseResolver(r = {}) { 12 | if (r.status === 204) return true; 13 | if (r.status >= 200 && r.status < 300) { 14 | try { 15 | // return await to catch before returning 16 | return await r.json(); 17 | } catch (err) { 18 | // Response is not a JSON object 19 | return r; 20 | } 21 | } 22 | throw new Error(r.statusText); 23 | } 24 | 25 | export function get(url, config = {}) { 26 | return fetch(`${baseURL}${url}`, { 27 | method: "GET", 28 | ...CONFIG, 29 | ...config 30 | }).then(statusResponseResolver); 31 | } 32 | 33 | export function del(url, config = {}) { 34 | return fetch(`${baseURL}${url}`, { 35 | method: "DELETE", 36 | ...CONFIG, 37 | ...config 38 | }).then(statusResponseResolver); 39 | } 40 | 41 | export function head(url, config = {}) { 42 | return fetch(`${baseURL}${url}`, { 43 | method: "HEAD", 44 | ...CONFIG, 45 | ...config 46 | }).then(statusResponseResolver); 47 | } 48 | 49 | export function options(url, config = {}) { 50 | return fetch(`${baseURL}${url}`, { 51 | method: "OPTIONS", 52 | ...CONFIG, 53 | ...config 54 | }).then(statusResponseResolver); 55 | } 56 | 57 | export function post(url, data = {}, config = {}) { 58 | return fetch(`${baseURL}${url}`, { 59 | method: "POST", 60 | ...CONFIG, 61 | body: JSON.stringify(data), 62 | ...config 63 | }).then(statusResponseResolver); 64 | } 65 | 66 | export function put(url, data = {}, config = {}) { 67 | return fetch(`${baseURL}${url}`, { 68 | method: "PUT", 69 | ...CONFIG, 70 | body: JSON.stringify(data), 71 | ...config 72 | }).then(statusResponseResolver); 73 | } 74 | 75 | export function patch(url, data = {}, config = {}) { 76 | return fetch(`${baseURL}${url}`, { 77 | method: "PATCH", 78 | ...CONFIG, 79 | body: JSON.stringify(data), 80 | ...config 81 | }).then(statusResponseResolver); 82 | } 83 | -------------------------------------------------------------------------------- /src/google-site-search/models/search/resolver.js: -------------------------------------------------------------------------------- 1 | function getTag(tagName, { pagemap }) { 2 | if (!pagemap || !pagemap.metatags) return null; 3 | return pagemap.metatags[0][tagName]; 4 | } 5 | 6 | export default { 7 | Query: { 8 | search(_, { query, first, after, site }, { models }) { 9 | if (first > 10) first = 10; 10 | // adjust after to work with start 11 | after += 1; 12 | 13 | const fields = 14 | "fields=queries(nextPage/startIndex,previousPage/startIndex),searchInformation/totalResults,items(cacheId,title,htmlTitle,link,displayLink,snippet,htmlSnippet,pagemap(cse_image/src,metatags/og:url,metatags/article:section))"; // tslint:disable-line 15 | 16 | query += `&num=${first}&start=${after}&${fields}`; 17 | 18 | if (site) { 19 | query += `&=${site}`; 20 | } 21 | 22 | return models.SSearch.query(query).then(x => { 23 | let next, previous; 24 | if (x.queries) { 25 | next = x.queries.nextPage ? x.queries.nextPage[0].startIndex : 0; 26 | previous = x.queries.previousPage 27 | ? x.queries.previousPage[0].startIndex 28 | : 0; 29 | } else { 30 | next = 0; 31 | previous = 0; 32 | } 33 | 34 | return { 35 | total: Number(x.searchInformation.totalResults), 36 | next: Number(next), 37 | previous: Number(previous), 38 | items: x.items ? x.items : [] 39 | }; 40 | }); 41 | } 42 | }, 43 | 44 | SSSearchResult: { 45 | id: ({ cacheId }) => cacheId, 46 | title: ({ title }) => title.split("|")[0].trim(), 47 | htmlTitle: ({ htmlTitle }) => htmlTitle.split("|")[0].trim(), 48 | link: ({ link }) => link, 49 | displayLink: ({ displayLink }) => displayLink, 50 | description: ({ snippet }) => snippet, 51 | htmlDescription: ({ htmlSnippet }) => htmlSnippet, 52 | type: data => getTag("og:type", data), 53 | section: data => getTag("article:section", data), 54 | image: ({ pagemap }) => pagemap && pagemap.cse_image[0].src 55 | }, 56 | 57 | SSSearch: { 58 | total: ({ total }) => total, 59 | next: ({ next }) => next, 60 | previous: ({ previous }) => previous, 61 | items: ({ items }) => items 62 | } 63 | }; 64 | -------------------------------------------------------------------------------- /src/apollos/__tests__/mongo.spec.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | import { MongoConnector, connect } from "../mongo"; 4 | 5 | describe("connect", () => { 6 | it("'connect' should allow for a connection status to be returned", async () => { 7 | const originalConnect = mongoose.connect; 8 | mongoose.connect = jest.fn((url, opts, cb) => cb(new Error())); 9 | const status = await connect(); 10 | expect(status).toBeDefined(); 11 | expect(status).not.toBeNull(); 12 | mongoose.connect = originalConnect; 13 | }); 14 | }); 15 | 16 | describe("MongoConnector", () => { 17 | let testModel; 18 | let originalConnect; 19 | beforeEach(async () => { 20 | if (testModel) return; 21 | originalConnect = mongoose.connect; 22 | mongoose.connect = jest.fn((url, opts, cb) => { 23 | cb(null); 24 | return {}; 25 | }); 26 | await connect(); 27 | const schema = { _id: String }; 28 | testModel = new MongoConnector("test", schema); 29 | }); 30 | 31 | afterEach(() => { 32 | mongoose.connect = originalConnect; 33 | }); 34 | 35 | it(" should expose the db connection", () => { 36 | expect(testModel.db).toBeTruthy(); 37 | }); 38 | 39 | it("should create a pluralized collection based off the model name", () => { 40 | expect(testModel.model.collection.collectionName).toEqual("tests"); 41 | }); 42 | 43 | it("should create a model based on the name passed", () => { 44 | expect(testModel.model.modelName).toEqual("test"); 45 | }); 46 | 47 | it("should use the schema passed as the second argument", () => { 48 | const { schema } = testModel.model; 49 | 50 | // expect(schema instanceof Schema).toBeTruthy(); 51 | expect(schema.paths._id).toBeTruthy(); 52 | expect(schema.paths._id.instance).toEqual("String"); 53 | }); 54 | 55 | it("findOne should be a sugared passthrough to the models findOne", async () => { 56 | const oldFindOne = testModel.model.findOne; 57 | 58 | testModel.model.findOne = function mockedFindOne(...args) { 59 | expect(args[0]).toEqual("test"); 60 | return new Promise(r => r([1])); 61 | }; 62 | 63 | const tests = await testModel.findOne("test"); 64 | expect(tests[0]).toEqual(1); 65 | 66 | testModel.model.findOne = oldFindOne; 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /src/expression-engine/models/content/schema.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | ` 3 | type LiveFeed { 4 | live: Boolean! 5 | fuse: Boolean! @deprecated(reason: "This no longer determines the live status") 6 | embedCode: String 7 | embedUrl: String 8 | videoUrl: String 9 | } 10 | 11 | # should this be a global type that implements Node? 12 | # XXX abstract from content if ^^ 13 | type ContentColor { 14 | id: ID 15 | value: String! 16 | description: String 17 | } 18 | 19 | type ContentScripture { 20 | book: String 21 | passage: String 22 | } 23 | 24 | type ContentData { 25 | body: String 26 | description: String 27 | ooyalaId: String @deprecated(reason: "Use video instead") 28 | video: ContentVideo 29 | wistiaId: String 30 | speaker: String 31 | isLight: Boolean 32 | hashtag: String 33 | 34 | tags: [String] # XXX create global tag type 35 | colors: [ContentColor] 36 | images(sizes: [String], ratios: [String]): [File] 37 | 38 | # deprecated (use audio field) 39 | tracks: [File] 40 | audio: [File] 41 | scripture: [ContentScripture] 42 | isLiked: Boolean 43 | } 44 | 45 | type ContentVideo { 46 | id: String 47 | embedUrl: String 48 | videoUrl: String 49 | } 50 | 51 | type ContentMeta { 52 | site: ID 53 | channel: ID 54 | series: ID 55 | urlTitle: String 56 | summary: String 57 | 58 | date: String 59 | entryDate: String 60 | startDate: String 61 | endDate: String 62 | 63 | # XXX should this be named better? 64 | actualDate: String 65 | 66 | # deprecated 67 | siteId: ID 68 | channelId: ID 69 | } 70 | 71 | type Content implements Node { 72 | id: ID! 73 | title: String! 74 | status: String! 75 | channel: ID! 76 | channelName: String 77 | campus: Campus 78 | meta: ContentMeta 79 | content: ContentData 80 | authors: [String] 81 | parent: Content # XXX determine if this can be multiple 82 | children(channels: [String], showFutureEntries: Boolean = false): [Content] 83 | related( 84 | includeChannels: [String], 85 | limit: Int = 20, 86 | skip: Int = 0, 87 | cache: Boolean = true 88 | ): [Content] 89 | 90 | # deprecated (moved to other types) 91 | tracks: [File] 92 | seriesId: ID 93 | } 94 | ` 95 | ]; 96 | -------------------------------------------------------------------------------- /src/rock/models/binary-files/model.js: -------------------------------------------------------------------------------- 1 | // import { merge } from "lodash"; 2 | import isEmpty from "lodash/isEmpty"; 3 | import { defaultCache } from "../../../util/cache"; 4 | import { createGlobalId } from "../../../util"; 5 | 6 | import { 7 | BinaryFile as BinaryFileTable 8 | // Location as LocationTable, // XXX move to its own model 9 | } from "./tables"; 10 | 11 | import { Rock } from "../system/model"; 12 | import * as api from "../../../apollos/models/users/api"; 13 | 14 | export class BinaryFile extends Rock { 15 | __type = "BinaryFile"; 16 | 17 | constructor({ cache } = { cache: defaultCache }) { 18 | super({ cache }); 19 | this.cache = cache; 20 | } 21 | 22 | processFile(file) { 23 | // is relative path to Rock 24 | if (file.Path && file.Path[0] === "~") { 25 | file.Path = file.Path.substr(2); 26 | file.Path = this.baseUrl + file.Path; 27 | } 28 | 29 | // remove query string variables 30 | if (file.Path && file.Path.indexOf("?") > -1) { 31 | file.Path = file.Path.substr(0, file.Path.indexOf("?")); 32 | } 33 | 34 | return file; 35 | } 36 | 37 | async getFromId(id, globalId) { 38 | globalId = globalId || createGlobalId(`${id}`, this.__type); 39 | return this.cache.get(globalId, () => 40 | BinaryFileTable.findOne({ where: { Id: id } }).then(this.processFile) 41 | ); 42 | } 43 | 44 | async getFromGuid(Guid) { 45 | return this.cache.get(`${Guid}:BinaryFileGuid`, () => 46 | BinaryFileTable.findOne({ 47 | where: { Guid } 48 | }).then(this.processFile) 49 | ); 50 | } 51 | 52 | // async getFromPerson 53 | async find(query) { 54 | return this.cache.get(this.cache.encode(query), () => 55 | BinaryFileTable.find({ 56 | where: query, 57 | attributes: ["Id"] 58 | }).then(this.getFromIds.bind(this)) 59 | ); 60 | } 61 | 62 | async attachPhotoIdToUser({ personId, previousPhotoId, newPhotoId } = {}) { 63 | try { 64 | await api.patch(`/People/${personId}`, { 65 | PhotoId: newPhotoId 66 | }); 67 | if (!isEmpty(previousPhotoId)) { 68 | try { 69 | await api.delete(`/BinaryFiles/${previousPhotoId}`); 70 | } catch (e) {} // eslint-disable-line 71 | } 72 | } catch (err) { 73 | throw err; 74 | } 75 | } 76 | } 77 | 78 | export default { 79 | BinaryFile 80 | }; 81 | -------------------------------------------------------------------------------- /src/apollos/models/users/__tests__/model.spec.js: -------------------------------------------------------------------------------- 1 | import crypto from "crypto"; 2 | import { User } from "../model"; 3 | 4 | it("should expose the model", () => { 5 | const users = new User(); 6 | expect(users.model).toBeTruthy(); 7 | }); 8 | 9 | it("`getByHashedToken` should allow searching for a raw token", async () => { 10 | const token = "testToken"; 11 | const users = new User(); 12 | users.model.findOne = function mockedFindOne(mongoQuery) { 13 | const matches = mongoQuery.$or; 14 | 15 | for (const match of matches) { 16 | const tok = match["services.resume.loginTokens.hashedToken"]; 17 | if (tok !== token) continue; 18 | 19 | expect(tok).toEqual(token); 20 | return Promise.resolve({}); 21 | } 22 | 23 | // we should never get here 24 | throw new Error(); 25 | }; 26 | 27 | return await users.getByHashedToken(token); 28 | }); 29 | 30 | it("`getByHashedToken` should allow searching for an encrypted token", async () => { 31 | const token = "testToken"; 32 | const users = new User(); 33 | const encyptedToken = crypto 34 | .createHash("sha256") 35 | .update(token) 36 | .digest("base64"); 37 | 38 | users.model.findOne = function mockedFindOne(mongoQuery) { 39 | const matches = mongoQuery.$or; 40 | for (const match of matches) { 41 | const tok = match["services.resume.loginTokens.hashedToken"]; 42 | if (tok !== encyptedToken) continue; 43 | 44 | expect(tok).toEqual(encyptedToken); 45 | return Promise.resolve({}); 46 | } 47 | 48 | // we should never get here 49 | throw new Error(); 50 | }; 51 | 52 | return await users.getByHashedToken(token); 53 | }); 54 | 55 | xit("`getFromId` should allow searching by an id", async () => { 56 | const id = "id"; 57 | const users = new User(); 58 | users.model.findOne = function mockedFindOne({ _id }) { 59 | expect(_id).toEqual(id); 60 | return Promise.resolve({}); 61 | }; 62 | 63 | return await users.getFromId(id, null); 64 | }); 65 | 66 | it("`getFromId` should try and read the data from the cache using the globalId", async () => { 67 | const id = "id"; 68 | const globalId = "foo"; 69 | 70 | const cache = { 71 | get(global) { 72 | expect(globalId).toEqual(global); 73 | return Promise.resolve(); 74 | } 75 | }; 76 | 77 | const tempUsers = new User({ cache }); 78 | 79 | return await tempUsers.getFromId(id, globalId); 80 | }); 81 | -------------------------------------------------------------------------------- /src/rock/models/finances/util/__tests__/statement.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import renderer from "react-test-renderer"; 3 | import { formatMoney, Statement, generatePDF } from "../statement"; 4 | import ReactDOMServer from "react-dom/server"; 5 | 6 | describe("formatMoney", () => { 7 | it("should render 2 decimal places", () => { 8 | expect(formatMoney(1)).toEqual("$1.00"); 9 | }); 10 | 11 | it("should format with commas", () => { 12 | expect(formatMoney(1234)).toEqual("$1,234.00"); 13 | expect(formatMoney(1234566)).toEqual("$1,234,566.00"); 14 | }); 15 | 16 | it("should allow non-whole numbers", () => { 17 | expect(formatMoney(1234.6)).toEqual("$1,234.60"); 18 | expect(formatMoney(0.65)).toEqual("$0.65"); 19 | }); 20 | }); 21 | 22 | describe("Statement", () => { 23 | const person = { FirstName: "", LastName: "", nickName: "" }; 24 | const home = { 25 | Street1: "3400 Vine St", 26 | Street2: "", 27 | City: "Cincinnati", 28 | State: "OH", 29 | PostalCode: "45220" 30 | }; 31 | const transactions = [ 32 | { Name: "Admission", Date: "2016-01-01", Amount: 15 }, 33 | { Name: "Admission", Date: "2016-02-01", Amount: 15 } 34 | ]; 35 | const total = 30; 36 | 37 | it("should render with data", () => { 38 | const component = renderer.create( 39 | 45 | ); 46 | expect(component).toMatchSnapshot(); 47 | }); 48 | }); 49 | 50 | describe("generatePDF", () => { 51 | xit("should call pdf.create with markup and settings", async () => { 52 | jest.mock("react-dom/server"); 53 | ReactDOMServer.renderToStaticMarkup = jest.fn(c => c); 54 | const results = await generatePDF("
Hello
"); 55 | 56 | // XXX can't properly snapshot the results, because of a creationDate 57 | // being injected by pdf.create (which we don't have time to properly mock) 58 | // can use Buffer(results, "base64").toString() to print 59 | expect(results).toBeDefined(); 60 | }); 61 | 62 | xit("should fail with no component to render", async () => { 63 | jest.mock("react-dom/server"); 64 | ReactDOMServer.renderToStaticMarkup = jest.fn(c => c); 65 | return generatePDF().then( 66 | res => { 67 | throw new Error("generatePDF didn't fail"); 68 | }, 69 | res => { 70 | expect(res).toBeDefined(); 71 | } 72 | ); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /src/util/node/__test__/model.spec.js: -------------------------------------------------------------------------------- 1 | import casual from "casual"; 2 | import Node, { createGlobalId, parseGlobalId } from "../model"; 3 | 4 | it("`createGlobalId` should take two arguments and return a string", () => { 5 | const id = casual.word; 6 | const type = casual.word; 7 | 8 | expect(typeof createGlobalId(id, type)).toEqual("string"); 9 | }); 10 | 11 | it("`createGlobalId` should be decodeable by `parseGlobalId`", () => { 12 | const id = casual.word; 13 | const __type = casual.word; 14 | const globalId = createGlobalId(id, __type); 15 | 16 | expect(parseGlobalId(globalId)).toEqual({ __type, id }); 17 | }); 18 | 19 | it("`parseGlobalId` should take a global id and return the type and id", () => { 20 | const id = casual.word; 21 | const __type = casual.word; 22 | const globalId = createGlobalId(id, __type); 23 | 24 | expect(parseGlobalId(globalId)).toEqual({ __type, id }); 25 | }); 26 | 27 | it("Node class should parse an encoded id to get the type to resolve", async () => { 28 | const id = casual.word; 29 | const __type = "Test"; 30 | const globalId = createGlobalId(id, __type); 31 | 32 | const context = { 33 | models: { 34 | Test: { 35 | getFromId(_id) { 36 | expect(_id).toEqual(id); 37 | return {}; 38 | } 39 | } 40 | } 41 | }; 42 | 43 | const node = new Node(context); 44 | node.get(globalId); 45 | }); 46 | 47 | it("Node class should return data from the models `getFromId` method", async () => { 48 | const id = casual.word; 49 | const __type = "Test"; 50 | const globalId = createGlobalId(id, __type); 51 | const data = { test: casual.word }; 52 | 53 | const context = { 54 | models: { 55 | Test: { 56 | getFromId(_id) { 57 | return Promise.resolve(data); 58 | } 59 | } 60 | } 61 | }; 62 | 63 | const node = new Node(context); 64 | const result = await node.get(globalId); 65 | 66 | expect(result.test).toEqual(data.test); 67 | }); 68 | 69 | it("Node class should attach the __type to the resulting data", async () => { 70 | const id = casual.word; 71 | const __type = "Test"; 72 | const globalId = createGlobalId(id, __type); 73 | const data = { test: casual.word }; 74 | 75 | const context = { 76 | models: { 77 | Test: { 78 | getFromId(_id) { 79 | return Promise.resolve(data); 80 | } 81 | } 82 | } 83 | }; 84 | 85 | const node = new Node(context); 86 | const result = await node.get(globalId); 87 | 88 | expect(result.__type).toEqual(__type); 89 | }); 90 | -------------------------------------------------------------------------------- /src/rock/models/finances/util/nmi.js: -------------------------------------------------------------------------------- 1 | import { Builder, parseString } from "xml2js"; 2 | import fetch from "isomorphic-fetch"; 3 | import { timeout, TimeoutError } from "promise-timeout"; 4 | 5 | import ErrorCodes from "./language"; 6 | 7 | export default (payload, gateway) => { 8 | if (!gateway) throw new Error("must be called with NMI Gateway object"); 9 | 10 | const builder = new Builder(); 11 | const xml = builder.buildObject(payload); 12 | 13 | return timeout( 14 | fetch(gateway.APIUrl, { 15 | method: "POST", 16 | body: `${xml}`, 17 | headers: { "Content-Type": "text/xml" } 18 | }) 19 | .then(response => response.text()) 20 | .then(result => { 21 | let data = result; 22 | // clean all tags to make sure they are parseable 23 | const matches = data.match(/<([^>]+)>/gim); 24 | for (const match of matches) { 25 | if (match.indexOf(",") > -1) { 26 | const matchRegex = new RegExp(match, "gmi"); 27 | data = data.replace(matchRegex, match.replace(/,/gim, "")); 28 | } 29 | } 30 | 31 | return data; 32 | }) 33 | .then( 34 | x => 35 | new Promise((s, f) => { 36 | parseString( 37 | x, 38 | { trim: true, explicitArray: false, mergeAttrs: true }, 39 | (err, result) => { 40 | if (err) f(err); 41 | if (!err) s(result); 42 | } 43 | ); 44 | }) 45 | ) 46 | .then(({ response }) => { 47 | const data = response; 48 | 49 | if (data["result-code"] === "100") return data; 50 | let number = Number(data["result-code"]); 51 | 52 | let err; 53 | 54 | // special mapping to ensure duplicates 55 | if ( 56 | data["result-text"] && 57 | data["result-text"].indexOf("Duplicate") > -1 58 | ) { 59 | number = 430; 60 | } 61 | 62 | if (ErrorCodes[number] && ErrorCodes[number] !== "result-text") { 63 | err = ErrorCodes[number]; 64 | } else { 65 | err = data["result-text"]; 66 | } 67 | const error = new Error(err); 68 | error.code = number; 69 | throw error; 70 | }), 71 | 60000 72 | ).catch(err => { 73 | if (err instanceof TimeoutError) { 74 | throw new Error(` 75 | The request to our payment process took longer than expected. 76 | For your safety we have cancelled this action. 77 | You were not charged and should be able to try again! 78 | `); 79 | } 80 | 81 | throw err; 82 | }); 83 | }; 84 | -------------------------------------------------------------------------------- /src/expression-engine/models/ee/__tests__/model.spec.js: -------------------------------------------------------------------------------- 1 | import { EE } from "../model"; 2 | 3 | it("`cleanMarkup` should exist", () => { 4 | const ee = new EE(); 5 | expect(ee.cleanMarkup).toBeTruthy(); 6 | }); 7 | 8 | it("`cleanMarkup` should be a function", () => { 9 | const ee = new EE(); 10 | expect(typeof ee.cleanMarkup).toBe("function"); 11 | }); 12 | 13 | it("`cleanMarkup` should return false if no markup", () => { 14 | const ee = new EE(); 15 | const result = ee.cleanMarkup(null); 16 | expect(result).toBe(false); 17 | }); 18 | 19 | it("`cleanMarkup` should return input if nothing to parse", () => { 20 | const ee = new EE(); 21 | const result = ee.cleanMarkup("test"); 22 | expect(result).toBe("test"); 23 | }); 24 | 25 | it("`cleanMarkup` should remove simple asset tag", () => { 26 | const ee = new EE(); 27 | const testImage = "//test.com/test.jpg"; 28 | const testAsset = `{assets_40016:${testImage}}`; 29 | const testMarkup = ``; 30 | const result = ee.cleanMarkup(testMarkup); 31 | expect(result).toBe(``); 32 | }); 33 | 34 | it("`cleanMarkup` should remove https", () => { 35 | const ee = new EE(); 36 | const testImage = "//test.com/test.jpg"; 37 | const testAsset = `{assets_40016:https:${testImage}}`; 38 | const testMarkup = ``; 39 | const result = ee.cleanMarkup(testMarkup); 40 | expect(result).toBe(``); 41 | }); 42 | 43 | it("`cleanMarkup` should remove multiple asset tags", () => { 44 | const ee = new EE(); 45 | const testImage1 = "//test.com/test.jpg"; 46 | const testImage2 = "//test.com/test2.jpg"; 47 | const testAsset1 = `{assets_40016:https:${testImage1}}`; 48 | const testAsset2 = `{assets_40017:https:${testImage2}}`; 49 | const testMarkup = ` 50 |

51 | 52 | 53 |

54 | `; 55 | const result = ee.cleanMarkup(testMarkup); 56 | expect(result).toBe(` 57 |

58 | 59 | 60 |

61 | `); 62 | }); 63 | 64 | it("`cleanMarkup` should remove multiple asset tags on the same line", () => { 65 | const ee = new EE(); 66 | const testImage1 = "//test.com/test.jpg"; 67 | const testImage2 = "//test.com/test2.jpg"; 68 | const testAsset1 = `{assets_40016:https:${testImage1}}`; 69 | const testAsset2 = `{assets_40017:https:${testImage2}}`; 70 | const testMarkup = ` 71 |

72 | `; 73 | const result = ee.cleanMarkup(testMarkup); 74 | expect(result).toBe(` 75 |

76 | `); 77 | }); 78 | -------------------------------------------------------------------------------- /src/expression-engine/models/files/tables.js: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-shadowed-variable */ 2 | 3 | import { INTEGER, STRING } from "sequelize"; 4 | 5 | import { MySQLConnector } from "../../mysql"; 6 | 7 | const assetsFilesSchema = { 8 | file_id: { type: INTEGER, primaryKey: true }, 9 | folder_id: { type: INTEGER }, 10 | source_type: { type: STRING }, 11 | source_id: { type: INTEGER }, 12 | file_name: { type: STRING } 13 | }; 14 | 15 | const assetsSelectionSchema = { 16 | file_id: { type: INTEGER }, 17 | entry_id: { type: INTEGER }, 18 | field_id: { type: INTEGER }, 19 | col_id: { type: INTEGER }, 20 | row_id: { type: INTEGER }, 21 | var_id: { type: INTEGER } 22 | }; 23 | 24 | const assetsFoldersSchema = { 25 | folder_id: { type: INTEGER, primaryKey: true }, 26 | source_id: { type: INTEGER }, 27 | parent_id: { type: INTEGER }, 28 | full_path: { type: STRING } 29 | }; 30 | 31 | const assetsSourcesSchema = { 32 | source_id: { type: INTEGER, primaryKey: true }, 33 | settings: { type: STRING } 34 | }; 35 | 36 | let Assets; 37 | let AssetsSelections; 38 | let AssetsFolders; 39 | let AssetsSources; 40 | export { 41 | Assets, 42 | assetsFilesSchema, 43 | AssetsSelections, 44 | assetsSelectionSchema, 45 | AssetsFolders, 46 | assetsFoldersSchema, 47 | AssetsSources, 48 | assetsSourcesSchema 49 | }; 50 | 51 | export function connect() { 52 | Assets = new MySQLConnector("exp_assets_files", assetsFilesSchema); 53 | AssetsSelections = new MySQLConnector( 54 | "exp_assets_selections", 55 | assetsSelectionSchema 56 | ); 57 | AssetsFolders = new MySQLConnector("exp_assets_folders", assetsFoldersSchema); 58 | AssetsSources = new MySQLConnector("exp_assets_sources", assetsSourcesSchema); 59 | 60 | // no primary key 61 | AssetsSelections.model.removeAttribute("id"); 62 | 63 | return { 64 | Assets, 65 | AssetsSelections, 66 | AssetsFolders, 67 | AssetsSources 68 | }; 69 | } 70 | 71 | export function bind({ 72 | ChannelData, 73 | Assets, 74 | AssetsSelections, 75 | AssetsFolders, 76 | AssetsSources 77 | }) { 78 | Assets.model.hasMany(AssetsSelections.model, { foreignKey: "file_id" }); 79 | AssetsSelections.model.belongsTo(Assets.model, { foreignKey: "file_id" }); 80 | 81 | Assets.model.belongsTo(AssetsSources.model, { foreignKey: "source_id" }); 82 | AssetsSources.model.hasOne(Assets.model, { foreignKey: "source_id" }); 83 | 84 | Assets.model.belongsTo(AssetsFolders.model, { foreignKey: "folder_id" }); 85 | AssetsFolders.model.hasOne(Assets.model, { foreignKey: "folder_id" }); 86 | 87 | // get access to assets from channel data 88 | ChannelData.model.hasOne(AssetsSelections.model, { foreignKey: "entry_id" }); 89 | } 90 | 91 | export default { 92 | connect, 93 | bind 94 | }; 95 | -------------------------------------------------------------------------------- /src/rock/models/feeds/resolver.js: -------------------------------------------------------------------------------- 1 | import { flatten } from "lodash"; 2 | 3 | export default { 4 | Query: { 5 | userFeed: async ( 6 | _, 7 | { filters, limit, skip, status, cache }, 8 | { models, person, user } 9 | ) => { 10 | if (!filters) return null; 11 | 12 | const filterQueries = []; 13 | 14 | // Home feed query 15 | if (filters.includes("CONTENT")) { 16 | const topics = await models.User.getUserFollowingTopics( 17 | person && person.PrimaryAliasId 18 | ); 19 | 20 | const channels = topics 21 | .map(x => x.toLowerCase()) 22 | .map(x => { 23 | if (x === "series") return ["series_newspring"]; 24 | if (x === "music") return ["newspring_albums"]; 25 | if (x === "devotionals") return ["study_entries", "devotionals"]; 26 | if (x === "events") return ["newspring_now"]; 27 | return [x]; 28 | }) 29 | .map(flatten); 30 | 31 | const showsNews = flatten(channels).includes("news"); 32 | 33 | // get user's campus to filter news by 34 | const userCampus = 35 | person && showsNews 36 | ? await models.Person.getCampusFromId(person.Id, { cache }) 37 | : null; 38 | 39 | const content = await models.Content.findByCampusName( 40 | { 41 | channel_name: { $or: channels }, 42 | offset: skip, 43 | limit, 44 | status 45 | }, 46 | userCampus ? userCampus.Name : null, 47 | true 48 | ); 49 | 50 | filterQueries.push(content); 51 | } 52 | 53 | if (filters.includes("GIVING_DASHBOARD") && person) { 54 | filterQueries.push( 55 | models.Transaction.findByPersonAlias( 56 | person.aliases, 57 | { limit: 3, offset: 0 }, 58 | { cache: null } 59 | ) 60 | ); 61 | 62 | filterQueries.push( 63 | models.SavedPayment.findExpiringByPersonAlias( 64 | person.aliases, 65 | { limit: 3, offset: 0 }, 66 | { cache: null } 67 | ) 68 | ); 69 | } 70 | 71 | if (filters.includes("LIKES") && user) { 72 | const likedContent = await models.Like.getLikedContent( 73 | person.PrimaryAliasId, 74 | models.Node 75 | ); 76 | const reversed = Array.isArray(likedContent) 77 | ? likedContent.reverse() 78 | : likedContent; 79 | filterQueries.push(reversed); 80 | } 81 | 82 | if (!filterQueries.length) return null; 83 | 84 | return Promise.all(filterQueries) 85 | .then(flatten) 86 | .then(x => x.filter(y => Boolean(y))); 87 | } 88 | } 89 | }; 90 | -------------------------------------------------------------------------------- /src/rock/models/finances/util/__tests__/formatTransaction.js: -------------------------------------------------------------------------------- 1 | import format from "../formatTransaction"; 2 | 3 | import { 4 | singleTransaction, 5 | singleACHTransaction, 6 | scheduleTransaction, 7 | multiFundScheduleTransaction, 8 | multipleTransactions 9 | } from "../__mocks__/sample-response"; 10 | 11 | jest.mock("moment", () => date => ({ 12 | toISOString: () => `Mocked ISODate: ${date}`, 13 | subtract: (number, size) => `Mocked subtract ${number}, ${size}` 14 | })); 15 | 16 | jest.mock("node-uuid", () => ({ 17 | v4: jest.fn(() => "guid") 18 | })); 19 | 20 | it("handles standard response data", () => { 21 | expect( 22 | format( 23 | { 24 | response: singleTransaction 25 | }, 26 | { Id: 3 } 27 | ) 28 | ).toMatchSnapshot(); 29 | }); 30 | 31 | it("handles multipleTransactions", () => { 32 | expect( 33 | format( 34 | { 35 | response: multipleTransactions 36 | }, 37 | { Id: 3 } 38 | ) 39 | ).toMatchSnapshot(); 40 | }); 41 | 42 | it("handles an authenticated response", () => { 43 | expect( 44 | format( 45 | { 46 | response: singleTransaction, 47 | person: { Id: 1, PrimaryAliasId: 2 } 48 | }, 49 | { Id: 3 } 50 | ) 51 | ).toMatchSnapshot(); 52 | }); 53 | 54 | it("handles adding a saved account", () => { 55 | expect( 56 | format( 57 | { 58 | response: singleTransaction, 59 | person: { Id: 1, PrimaryAliasId: 2 }, 60 | accountName: "My Credit Card" 61 | }, 62 | { Id: 3 } 63 | ) 64 | ).toMatchSnapshot(); 65 | }); 66 | 67 | it("maps the source type value", () => { 68 | expect( 69 | format( 70 | { 71 | response: singleTransaction, 72 | origin: "http://example.com/give" 73 | }, 74 | { Id: 3 } 75 | ) 76 | ).toMatchSnapshot(); 77 | }); 78 | 79 | it("sets the schedule Id to be recovered", () => { 80 | expect( 81 | format( 82 | { 83 | response: singleTransaction, 84 | scheduleId: 10 85 | }, 86 | { Id: 3 } 87 | ) 88 | ).toMatchSnapshot(); 89 | }); 90 | 91 | it("handles an ACH transaction", () => { 92 | expect( 93 | format( 94 | { 95 | response: singleACHTransaction 96 | }, 97 | { Id: 3 } 98 | ) 99 | ).toMatchSnapshot(); 100 | }); 101 | 102 | it("handles a schedule", () => { 103 | expect( 104 | format( 105 | { 106 | response: scheduleTransaction, 107 | scheduleId: 10 108 | }, 109 | { Id: 3 } 110 | ) 111 | ).toMatchSnapshot(); 112 | }); 113 | 114 | it("handles a schedule with multipleTransactions", () => { 115 | expect( 116 | format( 117 | { 118 | response: multiFundScheduleTransaction 119 | }, 120 | { Id: 3 } 121 | ) 122 | ).toMatchSnapshot(); 123 | }); 124 | -------------------------------------------------------------------------------- /src/rock/models/campuses/resolver.js: -------------------------------------------------------------------------------- 1 | import { geography } from "mssql-geoparser"; 2 | import { createGlobalId } from "../../../util"; 3 | 4 | // const delay = (x, num = 5000) => new Promise((r, rej) => { 5 | // return setTimeout(() => r(x), num); 6 | // }); 7 | 8 | export default { 9 | Query: { 10 | campuses: (_, { name, id }, { models }) => 11 | models.Campus.find({ Id: id, Name: name }) 12 | }, 13 | 14 | Campus: { 15 | id: ({ Id }, _, $, { parentType }) => createGlobalId(Id, parentType.name), 16 | entityId: ({ Id }) => Id, 17 | name: ({ Name }) => Name, 18 | shortCode: ({ ShortCode }) => ShortCode, 19 | guid: ({ Guid }) => Guid, 20 | url: ({ Url }) => Url, 21 | services: ({ ServiceTimes }) => { 22 | if (!ServiceTimes) return []; 23 | 24 | const days = {}; 25 | 26 | ServiceTimes.split("|") 27 | .filter(x => !!x) 28 | .forEach(x => { 29 | let [day, time] = x.split("^"); 30 | day = day.trim(); 31 | time = time.trim(); 32 | if (!days[day]) days[day] = []; 33 | 34 | if (days[day].indexOf(time) === -1) days[day].push(time); 35 | }); 36 | 37 | return Object.keys(days).map(x => { 38 | let str = `${x} at `; 39 | if (days[x].length === 1) { 40 | str += `& ${days[x]}`; 41 | return str; 42 | } 43 | 44 | str += `${[...days[x]].slice(0, days[x].length - 1).join(", ")} `; 45 | str += `& ${[...days[x]].pop()}`; 46 | 47 | return str; 48 | }); 49 | }, 50 | locationId: ({ LocationId }) => LocationId, 51 | location: ({ LocationId }, _, { models }) => { 52 | if (!LocationId) return null; 53 | 54 | return models.Campus.findByLocationId(LocationId); 55 | } 56 | }, 57 | 58 | Location: { 59 | id: ({ Id }, _, $, { parentType }) => createGlobalId(Id, parentType.name), 60 | name: ({ Name }) => Name, 61 | street1: ({ Street1 }) => Street1, 62 | street2: ({ Street2 }) => Street2, 63 | city: ({ City }) => City, 64 | state: ({ State }) => State, 65 | country: ({ Country }) => Country, 66 | zip: ({ PostalCode }) => PostalCode, 67 | latitude: ({ GeoPoint, latitude }) => { 68 | if (latitude) return latitude; 69 | if (!GeoPoint) return null; 70 | try { 71 | const { points } = geography(GeoPoint); 72 | return points[0].x; 73 | } catch (e) { 74 | return null; 75 | } 76 | }, 77 | longitude: ({ GeoPoint, longitude }) => { 78 | if (longitude) return longitude; 79 | if (!GeoPoint) return null; 80 | try { 81 | const { points } = geography(GeoPoint); 82 | return points[0].y; 83 | } catch (e) { 84 | return null; 85 | } 86 | }, 87 | distance: ({ Id, Distance }) => { 88 | // tslint:disable-line 89 | if (Distance) return Distance; 90 | 91 | return null; 92 | // XXX get distance from the person 93 | // this is typically used from a geo based lookup 94 | } 95 | } 96 | }; 97 | -------------------------------------------------------------------------------- /src/util/cache/__test__/index.spec.js: -------------------------------------------------------------------------------- 1 | import casual from "casual"; 2 | import { defaultCache, resolvers } from "../defaults"; 3 | import { InMemoryCache } from "../memory-cache"; 4 | import { parseGlobalId } from "../../node/model"; 5 | 6 | it("the cache mutation should delete the id from the cache", () => { 7 | const id = casual.word, 8 | data = { test: casual.word }, 9 | cacheData = { [id]: data }, 10 | cache = new InMemoryCache(cacheData); 11 | 12 | const { Mutation } = resolvers; 13 | 14 | function get(_id) { 15 | return Promise.resolve({ [_id]: { test: casual.word } }); 16 | } 17 | const context = { cache, models: { Node: { get } } }; 18 | 19 | return Mutation.cache(null, { id, type: null }, context).then(() => { 20 | expect(cacheData[id]).toBeFalsy; 21 | }); 22 | }); 23 | 24 | it("the cache mutation should refetch and save the data in the cache", () => { 25 | const id = casual.word, 26 | data = { test: casual.word }, 27 | data2 = { test: casual.word }, 28 | cacheData = { [id]: data }, 29 | cache = new InMemoryCache(cacheData); 30 | 31 | const { Mutation } = resolvers; 32 | 33 | function get(_id) { 34 | return cache.get(_id, () => Promise.resolve(data2)); 35 | } 36 | const context = { cache, models: { Node: { get } } }; 37 | 38 | return Mutation.cache(null, { id, type: null }, context).then(result => { 39 | expect(result).toEqual(data2); 40 | 41 | return new Promise((c, r) => { 42 | // cache resetting is an async action 43 | process.nextTick(() => { 44 | expect(result).toEqual(data2); 45 | c(); 46 | }); 47 | }); 48 | }); 49 | }); 50 | 51 | it("the cache mutation should allow using a native id and type together", () => { 52 | const id = casual.word, 53 | type = casual.word, 54 | data = { test: casual.word }, 55 | data2 = { test: casual.word }, 56 | cacheData = { [id]: data }, 57 | cache = new InMemoryCache(cacheData); 58 | 59 | const { Mutation } = resolvers; 60 | 61 | function get(_id) { 62 | const parsed = parseGlobalId(_id); 63 | expect(id).toEqual(parsed.id); 64 | expect(type).toEqual(parsed.__type); 65 | return cache.get(_id, () => Promise.resolve(data2)); 66 | } 67 | const context = { cache, models: { Node: { get } } }; 68 | 69 | return Mutation.cache(null, { id, type }, context).then(result => { 70 | expect(result).toEqual(data2); 71 | 72 | return new Promise((c, r) => { 73 | // cache resetting is an async action 74 | process.nextTick(() => { 75 | expect(result).toEqual(data2); 76 | c(); 77 | }); 78 | }); 79 | }); 80 | }); 81 | 82 | // XXX how to test this 83 | it("defaultCache:get should simply run the lookup method", () => 84 | defaultCache.get(null, () => Promise.resolve())); 85 | 86 | it("defaultCache:set should return true and do nothing", () => 87 | defaultCache.set("test", {}).then(success => { 88 | if (!success) throw new Error(); 89 | expect(true).toBe(true); 90 | })); 91 | 92 | it("defaultCache:del exist as a function but do nothing", () => { 93 | expect(() => defaultCache.del("string")).not.toThrow(); 94 | }); 95 | -------------------------------------------------------------------------------- /src/rock/models/finances/models/__tests__/__snapshots__/Transaction.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`createOrder adds a job to the queue if an schedule with saved payment 1`] = `undefined`; 4 | 5 | exports[`createOrder adds a persons PrimaryAliasId to an order 1`] = ` 6 | Object { 7 | "code": 100, 8 | "success": true, 9 | "transactionId": 1, 10 | "url": Object { 11 | "sale": Object { 12 | "add-customer": "", 13 | "amount": 1, 14 | "api-key": "3", 15 | "customer-id": 10, 16 | "cvv-reject": "P|N|S|U", 17 | "ip-address": "1", 18 | "order-description": "Online contribution from Apollos", 19 | "redirect-url": "https://my.newspring.cc/give/now", 20 | }, 21 | }, 22 | } 23 | `; 24 | 25 | exports[`createOrder attempts to look up a saved account via the id 1`] = ` 26 | Object { 27 | "code": 100, 28 | "success": true, 29 | "transactionId": 1, 30 | "url": Object { 31 | "sale": Object { 32 | "amount": 1, 33 | "api-key": "3", 34 | "customer-vault-id": 100, 35 | "ip-address": "1", 36 | "order-description": "Online contribution from Apollos", 37 | "redirect-url": "https://my.newspring.cc/give/now", 38 | }, 39 | }, 40 | } 41 | `; 42 | 43 | exports[`createOrder correctly formats a subscription object 1`] = ` 44 | Object { 45 | "code": 100, 46 | "success": true, 47 | "transactionId": 1, 48 | "url": Object { 49 | "add-subscription": Object { 50 | "amount": 1, 51 | "api-key": "3", 52 | "order-description": "Online contribution from Apollos", 53 | "redirect-url": "https://my.newspring.cc/give/now", 54 | "start-date": "01012020", 55 | }, 56 | }, 57 | } 58 | `; 59 | 60 | exports[`createOrder correctly formats the data object 1`] = ` 61 | Object { 62 | "code": 100, 63 | "success": true, 64 | "transactionId": 1, 65 | "url": Object { 66 | "sale": Object { 67 | "add-customer": "", 68 | "amount": 1, 69 | "api-key": "3", 70 | "cvv-reject": "P|N|S|U", 71 | "ip-address": "1", 72 | "order-description": "Online contribution from Apollos", 73 | "redirect-url": "https://my.newspring.cc/give/now", 74 | "test": true, 75 | }, 76 | }, 77 | } 78 | `; 79 | 80 | exports[`createOrder handles a validation attempt 1`] = ` 81 | Object { 82 | "code": 100, 83 | "success": true, 84 | "transactionId": 1, 85 | "url": Object { 86 | "validate": Object { 87 | "amount": 0, 88 | "api-key": "3", 89 | "customer-id": 10, 90 | "cvv-reject": "P|N|S|U", 91 | "ip-address": "1", 92 | "order-description": "Online contribution from Apollos", 93 | "redirect-url": "https://my.newspring.cc/give/now", 94 | }, 95 | }, 96 | } 97 | `; 98 | 99 | exports[`createOrder handles an add customer attempt 1`] = ` 100 | Object { 101 | "code": 100, 102 | "success": true, 103 | "transactionId": 1, 104 | "url": Object { 105 | "add-customer": Object { 106 | "api-key": "3", 107 | "redirect-url": "https://my.newspring.cc/give/now", 108 | }, 109 | }, 110 | } 111 | `; 112 | -------------------------------------------------------------------------------- /src/util/heighliner.js: -------------------------------------------------------------------------------- 1 | import { merge } from "lodash"; 2 | 3 | import { 4 | schema as nodeSchema, 5 | resolver as nodeResolver, 6 | mocks as nodeMocks 7 | } from "./node"; 8 | 9 | import { 10 | resolvers as cacheResolver, 11 | mutations as cacheMutation 12 | } from "./cache/defaults"; 13 | 14 | import { schema as dateSchema, resolver as dateResolver } from "./scalars/Date"; 15 | 16 | export function getIp(request) { 17 | return ( 18 | request.headers["x-forwarded-for"] || 19 | request.connection.remoteAddress || 20 | request.socket.remoteAddress || 21 | request.connection.socket.remoteAddress 22 | ); 23 | } 24 | 25 | export function createQueries(queries) { 26 | return [ 27 | ` 28 | type Query { 29 | ${queries.join("\n")} 30 | node(id: ID!): Node 31 | } 32 | ` 33 | ]; 34 | } 35 | 36 | export function createMutations(mutations = []) { 37 | return [ 38 | ` 39 | type Mutation { 40 | ${mutations.join("\n")} 41 | ${cacheMutation.join("\n")} 42 | } 43 | ` 44 | ]; 45 | } 46 | 47 | export function createApplication(models) { 48 | const joined = { 49 | schema: [], 50 | models: {}, 51 | resolvers: {}, 52 | queries: [], 53 | mutations: [] 54 | }; 55 | 56 | for (const model of models) { 57 | if (model.schema) joined.schema = [...joined.schema, ...model.schema]; 58 | if (model.models) joined.models = merge(joined.models, model.models); 59 | if (model.resolvers) 60 | joined.resolvers = merge(joined.resolvers, model.resolvers); 61 | if (model.queries) joined.queries = [...joined.queries, ...model.queries]; 62 | if (model.mutations) 63 | joined.mutations = [...joined.mutations, ...model.mutations]; 64 | } 65 | 66 | return joined; 67 | } 68 | 69 | export function loadApplications(applications) { 70 | const joined = { 71 | schema: [...nodeSchema, ...dateSchema], 72 | models: {}, 73 | resolvers: merge({}, nodeResolver, cacheResolver, dateResolver), 74 | mocks: merge({}, nodeMocks) 75 | }; 76 | 77 | Object.keys(applications).forEach(name => { 78 | const app = applications[name]; 79 | joined.schema = [...joined.schema, ...app.schema]; 80 | joined.models = merge(joined.models, app.models); 81 | joined.resolvers = merge(joined.resolvers, app.resolvers); 82 | }); 83 | 84 | // dynmically create the root query mock 85 | const queries = merge({}, joined.mocks.Query); 86 | joined.mocks.Query = () => queries; 87 | 88 | // XXX dynamically create the root mutation mock 89 | const mutations = merge({}, joined.mocks.Mutation); 90 | joined.mocks.Mutation = () => mutations; 91 | 92 | return joined; 93 | } 94 | 95 | export function createSchema({ queries, schema, mutations }) { 96 | // build base level schema 97 | const root = [ 98 | ` 99 | schema { 100 | query: Query 101 | mutation: Mutation 102 | } 103 | ` 104 | ]; 105 | 106 | const query = createQueries(queries); 107 | const mutation = createMutations(mutations); 108 | 109 | // generate the final schema 110 | return [...root, ...query, ...mutation, ...schema]; 111 | } 112 | -------------------------------------------------------------------------------- /src/rock/models/finances/schema.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | ` 3 | type TransactionDetail implements Node { 4 | id: ID! 5 | amount: Float! 6 | account: FinancialAccount 7 | } 8 | 9 | type ScheduledTransaction implements Node { 10 | id: ID! 11 | entityId: Int! 12 | reminderDate: String 13 | start: String 14 | next: String 15 | code: String 16 | gateway: Int # we use this on the client I think? 17 | end: String 18 | numberOfPayments: Int 19 | date: String 20 | details: [TransactionDetail] 21 | transactions: [Transaction] 22 | schedule: DefinedValue 23 | payment: PaymentDetail 24 | isActive: Boolean 25 | } 26 | 27 | type Transaction implements Node { 28 | id: ID! 29 | entityId: Int! 30 | summary: String 31 | status: String 32 | statusMessage: String 33 | date: String 34 | details: [TransactionDetail] 35 | payment: PaymentDetail 36 | person: Person 37 | schedule: ScheduledTransaction 38 | } 39 | 40 | type OrderMutationResponse implements MutationResponse { 41 | error: String 42 | success: Boolean! 43 | code: Int 44 | url: String 45 | transactionId: ID 46 | } 47 | 48 | type CompleteOrderMutationResponse implements MutationResponse { 49 | error: String 50 | success: Boolean! 51 | code: Int 52 | transaction: Transaction 53 | schedule: ScheduledTransaction 54 | person: Person 55 | savedPayment: SavedPayment 56 | } 57 | 58 | type ValidateMutationResponse implements MutationResponse { 59 | error: String 60 | success: Boolean! 61 | code: Int 62 | } 63 | 64 | type FinancialAccount implements Node { 65 | id: ID! 66 | entityId: Int! 67 | transactions( 68 | limit: Int = 20, 69 | skip: Int = 0, 70 | cache: Boolean = true, 71 | start: String, 72 | end: String, 73 | people: [Int], 74 | ): [Transaction] 75 | total( 76 | start: String, 77 | end: String, 78 | people: [Int], 79 | ): Int 80 | name: String 81 | order: Int 82 | description: String 83 | summary: String 84 | image: String 85 | end: String 86 | start: String 87 | images: [File] 88 | } 89 | 90 | type PaymentDetail implements Node { 91 | id: ID! 92 | accountNumber: String 93 | paymentType: String! 94 | } 95 | 96 | type SavedPayment implements Node { 97 | id: ID! 98 | entityId: Int! 99 | name: String 100 | guid: String 101 | code: String! 102 | date: String 103 | payment: PaymentDetail 104 | expirationMonth: String 105 | expirationYear: String 106 | } 107 | 108 | type SavePaymentMutationResponse implements MutationResponse { 109 | error: String 110 | success: Boolean! 111 | code: Int 112 | savedPayment: SavedPayment 113 | } 114 | 115 | type ScheduledTransactionMutationResponse implements MutationResponse { 116 | error: String 117 | success: Boolean! 118 | code: Int 119 | schedule: ScheduledTransaction 120 | } 121 | 122 | type StatementMutationResponse implements MutationResponse { 123 | error: String 124 | success: Boolean! 125 | code: Int 126 | file: String 127 | } 128 | ` 129 | ]; 130 | -------------------------------------------------------------------------------- /src/util/cache/__test__/memory-cache.spec.js: -------------------------------------------------------------------------------- 1 | import casual from "casual"; 2 | import { InMemoryCache } from "../memory-cache"; 3 | 4 | it("`InMemoryCache` should have a way to get items from the cache", () => { 5 | const id = casual.word; 6 | const data = { test: casual.word }; 7 | 8 | const cacheData = { [id]: data }; 9 | const cache = new InMemoryCache(cacheData); 10 | 11 | return cache 12 | .get(id, () => Promise.resolve()) 13 | .then(result => { 14 | expect(result).toEqual(data); 15 | }); 16 | }); 17 | 18 | it("`InMemoryCache` should use a lookup method if no cache entry exists", () => { 19 | const id = casual.word; 20 | const data = { test: casual.word }; 21 | 22 | const cacheData = {}; 23 | const cache = new InMemoryCache(cacheData); 24 | 25 | const spyLookup = () => Promise.resolve(data); 26 | return cache.get(id, spyLookup).then(result => { 27 | expect(result).toEqual(data); 28 | }); 29 | }); 30 | 31 | it("`InMemoryCache` should have a way to set items in the cache with a ttl", () => { 32 | const id = casual.word; 33 | const data = { test: casual.word }; 34 | 35 | const cacheData = {}; 36 | const cache = new InMemoryCache(cacheData); 37 | 38 | const spyLookup = () => Promise.resolve(data); 39 | return cache 40 | .get(id, spyLookup, { ttl: 0.1 }) 41 | .then(result => { 42 | expect(result).toEqual(data); 43 | }) 44 | .then( 45 | () => 46 | new Promise((c, r) => { 47 | setTimeout(() => { 48 | expect(cacheData[id]).toBeFalsy(); 49 | c(); 50 | }, 0.1 * 60 + 25); 51 | }) 52 | ); 53 | }); 54 | 55 | it("should have a way to set items in the cache", () => { 56 | const id = casual.word; 57 | const data = { test: casual.word }; 58 | 59 | const cacheData = {}; 60 | const cache = new InMemoryCache(cacheData); 61 | 62 | return cache.set(id, data).then(() => { 63 | expect(cacheData[id]).toEqual(data); 64 | }); 65 | }); 66 | 67 | it("should eventually return true if successfully set", () => { 68 | const id = casual.word; 69 | const data = { test: casual.word }; 70 | 71 | const cacheData = {}; 72 | const cache = new InMemoryCache(cacheData); 73 | 74 | return cache.set(id, data).then(success => { 75 | expect(cacheData[id]).toEqual(data); 76 | expect(success).toBeTruthy(); 77 | }); 78 | }); 79 | 80 | it("should have a way to set items in the cache with a ttl", () => { 81 | const id = casual.word; 82 | const data = { test: casual.word }; 83 | 84 | const cacheData = {}; 85 | const cache = new InMemoryCache(cacheData); 86 | 87 | return cache 88 | .set(id, data, 0.1) 89 | .then(result => { 90 | expect(cacheData[id]).toEqual(data); 91 | }) 92 | .then( 93 | () => 94 | new Promise((c, r) => { 95 | setTimeout(() => { 96 | expect(cacheData[id]).toBeFalsy(); 97 | c(); 98 | }, 0.1 * 60 + 25); 99 | }) 100 | ); 101 | }); 102 | 103 | it("`InMemoryCache` should allow removing existing cache entries", () => { 104 | const id = casual.word; 105 | const data = { test: casual.word }; 106 | 107 | const cacheData = { [id]: data }; 108 | const cache = new InMemoryCache(cacheData); 109 | 110 | cache.del(id); 111 | expect(cacheData[id]).toBeFalsy(); 112 | }); 113 | -------------------------------------------------------------------------------- /src/rock/models/people/tables.js: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-shadowed-variable */ 2 | 3 | import { INTEGER, STRING, BOOLEAN } from "sequelize"; 4 | 5 | import { MSSQLConnector } from "../../mssql"; 6 | 7 | const personSchema = { 8 | Id: { type: INTEGER, primaryKey: true }, 9 | BirthDate: { type: INTEGER }, 10 | BirthDay: { type: INTEGER }, 11 | BirthMonth: { type: INTEGER }, 12 | BirthYear: { type: INTEGER }, 13 | ConnectionStatusValueId: { type: INTEGER }, 14 | Email: { type: STRING }, 15 | EmailPreference: { type: STRING }, 16 | FirstName: { type: STRING }, 17 | Gender: { type: INTEGER }, 18 | GivingGroupId: { type: INTEGER }, 19 | GivingId: { type: INTEGER }, 20 | Guid: { type: STRING }, 21 | IsDeceased: { type: BOOLEAN }, 22 | LastName: { type: STRING }, 23 | MaritalStatusValueId: { type: INTEGER }, 24 | MiddleName: { type: STRING }, 25 | NickName: { type: STRING }, 26 | PhotoId: { type: INTEGER } 27 | }; 28 | 29 | const aliasSchema = { 30 | Id: { type: INTEGER, primaryKey: true }, 31 | PersonId: { type: INTEGER } 32 | }; 33 | 34 | // XXX move to its own model if/when needed 35 | const phoneNumberSchema = { 36 | Id: { type: INTEGER, primaryKey: true }, 37 | CountryCode: { type: STRING }, 38 | Description: { type: STRING }, 39 | Extension: { type: STRING }, 40 | IsMessagingEnabled: { type: STRING }, 41 | IsSystem: { type: BOOLEAN }, 42 | IsUnlisted: { type: BOOLEAN }, 43 | Number: { type: STRING }, 44 | NumberFormatted: { type: STRING }, 45 | PersonId: { type: INTEGER } 46 | }; 47 | 48 | const personalDeviceSchema = { 49 | Id: { type: INTEGER, primaryKey: true }, 50 | PersonAliasId: { type: INTEGER }, 51 | DeviceRegistrationId: { type: STRING }, 52 | PersonalDeviceTypeValueId: { type: INTEGER }, 53 | NotificationsEnabled: { type: BOOLEAN } 54 | }; 55 | 56 | let Person; 57 | let PersonAlias; 58 | let PhoneNumber; 59 | let PersonalDevice; 60 | export { 61 | Person, 62 | personSchema, 63 | PersonAlias, 64 | aliasSchema, 65 | PhoneNumber, 66 | phoneNumberSchema, 67 | PersonalDevice 68 | }; 69 | 70 | export function connect() { 71 | Person = new MSSQLConnector("Person", personSchema, {}, "People"); 72 | PersonAlias = new MSSQLConnector("PersonAlias", aliasSchema); 73 | PhoneNumber = new MSSQLConnector("PhoneNumber", phoneNumberSchema); 74 | PersonalDevice = new MSSQLConnector("PersonalDevice", personalDeviceSchema); 75 | 76 | return { 77 | Person, 78 | PersonAlias, 79 | PhoneNumber, 80 | PersonalDevice 81 | }; 82 | } 83 | 84 | export function bind({ 85 | Person, 86 | PersonAlias, 87 | PhoneNumber, 88 | PersonalDevice, 89 | Group 90 | }) { 91 | PersonAlias.model.belongsTo(Person.model, { 92 | foreignKey: "PersonId", 93 | targetKey: "Id" 94 | }); 95 | Person.model.hasOne(PersonAlias.model, { foreignKey: "PersonId" }); 96 | 97 | PhoneNumber.model.belongsTo(Person.model, { 98 | foreignKey: "PersonId", 99 | targetKey: "Id" 100 | }); 101 | PersonalDevice.model.belongsTo(PersonAlias.model, { 102 | foreignKey: "PersonAliasId", 103 | targetKey: "Id" 104 | }); 105 | 106 | Person.model.belongsToMany(Group.model, { 107 | as: "Groups", 108 | through: "GroupMember", 109 | foreignKey: "PersonId", 110 | otherKey: "GroupId" 111 | }); 112 | } 113 | 114 | export default { 115 | connect, 116 | bind 117 | }; 118 | -------------------------------------------------------------------------------- /src/expression-engine/models/content/__tests__/images.spec.js: -------------------------------------------------------------------------------- 1 | import { addResizings } from "../images"; 2 | 3 | const createSampleImage = () => ({ 4 | url: "url.jpg", 5 | fileLabel: null 6 | }); 7 | 8 | it("`addResizings` should return 5 images when handed 1", () => { 9 | const images = [createSampleImage()]; 10 | 11 | const result = addResizings(images); 12 | expect(result.length).toEqual(5); 13 | }); 14 | 15 | it("`addResizings` should return 15 images when handed 3", () => { 16 | const images = [ 17 | createSampleImage(), 18 | createSampleImage(), 19 | createSampleImage() 20 | ]; 21 | 22 | const result = addResizings(images); 23 | expect(result.length).toEqual(15); 24 | }); 25 | 26 | it("`addResizings` should generate an xlarge image", () => { 27 | const images = [createSampleImage()]; 28 | 29 | const results = addResizings(images); 30 | const { url, size } = results[0]; 31 | expect(url.indexOf("xlarge") > -1).toBeTruthy(); 32 | expect(size).toEqual("xlarge"); 33 | }); 34 | 35 | it("`addResizings` should generate a large image", () => { 36 | const images = [createSampleImage()]; 37 | 38 | const results = addResizings(images); 39 | const { url, size } = results[1]; 40 | expect(url.indexOf("large") > -1).toBeTruthy(); 41 | expect(size).toEqual("large"); 42 | }); 43 | 44 | it("`addResizings` should generate a medium image", () => { 45 | const images = [createSampleImage()]; 46 | 47 | const results = addResizings(images); 48 | const { url, size } = results[2]; 49 | expect(url.indexOf("medium") > -1).toBeTruthy(); 50 | expect(size).toEqual("medium"); 51 | }); 52 | 53 | it("`addResizings` should generate a small image", () => { 54 | const images = [createSampleImage()]; 55 | 56 | const results = addResizings(images); 57 | const { url, size } = results[3]; 58 | expect(url.indexOf("small") > -1).toBeTruthy(); 59 | expect(size).toEqual("small"); 60 | }); 61 | 62 | it("`addResizings` should generate an xsmall image", () => { 63 | const images = [createSampleImage()]; 64 | 65 | const results = addResizings(images); 66 | const { url, size } = results[4]; 67 | expect(url.indexOf("xsmall") > -1).toBeTruthy(); 68 | expect(size).toEqual("xsmall"); 69 | }); 70 | 71 | it("`addResizings` should return 1 image if 1 image and 1 size", () => { 72 | const images = [createSampleImage()]; 73 | const options = { 74 | sizes: ["medium"], 75 | ratios: [] 76 | }; 77 | 78 | const results = addResizings(images, options); 79 | expect(results.length).toEqual(1); 80 | expect(results[0].url.indexOf("medium") > -1).toBeTruthy(); 81 | expect(results[0].size).toEqual("medium"); 82 | }); 83 | 84 | it("`addResizings` should return 4 images if 2 images and 2 sizes", () => { 85 | const images = [createSampleImage(), createSampleImage()]; 86 | const options = { 87 | sizes: ["medium", "large"], 88 | ratios: [] 89 | }; 90 | 91 | const results = addResizings(images, options); 92 | expect(results.length).toEqual(4); 93 | }); 94 | 95 | it("`addResizings` should return only the ratio specified", () => { 96 | const images = [createSampleImage(), createSampleImage()]; 97 | const options = { 98 | sizes: null, 99 | ratios: ["2:1"] 100 | }; 101 | images[0].fileLabel = "2:1"; 102 | images[1].fileLabel = "1:2"; 103 | 104 | const results = addResizings(images, options); 105 | expect(results.length).toEqual(5); 106 | results.map(image => { 107 | expect(image.fileLabel).toEqual("2:1"); 108 | }); 109 | }); 110 | -------------------------------------------------------------------------------- /src/rock/models/finances/models/ScheduledTransaction.js: -------------------------------------------------------------------------------- 1 | import { createGlobalId } from "../../../../util"; 2 | import nmi from "../util/nmi"; 3 | 4 | import { 5 | Transaction as TransactionTable, 6 | ScheduledTransaction as ScheduledTransactionTable, 7 | ScheduledTransactionDetail 8 | } from "../tables"; 9 | 10 | import { Rock } from "../../system"; 11 | 12 | export default class ScheduledTransaction extends Rock { 13 | __type = "ScheduledTransaction"; 14 | 15 | async getFromId(id, globalId) { 16 | globalId = globalId || createGlobalId(`${id}`, this.__type); 17 | return this.cache.get(globalId, () => 18 | ScheduledTransactionTable.findOne({ where: { Id: id } }) 19 | ); 20 | } 21 | 22 | async getTransactionsById(id) { 23 | if (!id) return Promise.resolve(null); 24 | const globalId = createGlobalId( 25 | `${id}`, 26 | "ScheduledTransactionTransactions" 27 | ); 28 | return this.cache.get(globalId, () => 29 | TransactionTable.find({ 30 | where: { ScheduledTransactionId: id }, 31 | order: [["TransactionDateTime", "DESC"]] 32 | }) 33 | ); 34 | } 35 | 36 | async getDetailsByScheduleId(id) { 37 | if (!id) return Promise.resolve(null); 38 | const globalId = createGlobalId(`${id}`, "ScheduledTransactionDetails"); 39 | // XXX why isn't this caching? 40 | return this.cache.get(globalId, () => 41 | ScheduledTransactionDetail.find({ 42 | where: { ScheduledTransactionId: id } 43 | }) 44 | ); 45 | } 46 | 47 | async findByPersonAlias(aliases, { limit, offset, isActive }, { cache }) { 48 | const query = { aliases, limit, offset, isActive }; 49 | return await this.cache 50 | .get( 51 | this.cache.encode(query), 52 | () => 53 | ScheduledTransactionTable.find({ 54 | where: { 55 | AuthorizedPersonAliasId: { $in: aliases }, 56 | IsActive: isActive 57 | }, 58 | order: [["CreatedDateTime", "DESC"]], 59 | attributes: ["Id"], 60 | limit, 61 | offset 62 | }), 63 | { cache } 64 | ) 65 | .then(this.getFromIds.bind(this)); 66 | } 67 | 68 | async cancelNMISchedule(id, gatewayDetails) { 69 | const existing = await this.getFromId(id); 70 | if (!existing) return Promise.resolve({ error: "Schedule not found" }); 71 | 72 | const payload = { 73 | "delete-subscription": { 74 | "api-key": gatewayDetails.SecurityKey, 75 | "subscription-id": existing.GatewayScheduleId 76 | } 77 | }; 78 | 79 | return nmi(payload, gatewayDetails) 80 | .catch(error => { 81 | // If this schedule isn't in NMI, go ahead and clean up Rock 82 | if ( 83 | !/Transaction not found/.test(error.message) && 84 | !/No recurring subscriptions found/.test(error.message) 85 | ) 86 | throw error; 87 | }) 88 | .then(() => { 89 | if (existing.GatewayScheduleId) { 90 | return ScheduledTransactionTable.patch(existing.Id, { 91 | IsActive: false 92 | }); 93 | } 94 | 95 | return ScheduledTransactionTable.delete(existing.Id); 96 | }) 97 | .then(() => { 98 | const nodeId = createGlobalId(`${existing.Id}`, this.__type); 99 | this.cache.del(nodeId); 100 | return { scheduleId: existing.Id }; 101 | }) 102 | .catch(error => ({ code: error.code, error: error.message })); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/expression-engine/models/navigation/model.js: -------------------------------------------------------------------------------- 1 | import { orderBy } from "lodash"; 2 | import { defaultCache } from "../../../util/cache"; 3 | 4 | // import { 5 | // ChannelTitles, 6 | // channelTitleSchema, 7 | // } from "../content/tables"; 8 | 9 | import { Navee, NaveeNav } from "../navigation/tables"; 10 | 11 | import { Sites } from "../ee/sites"; 12 | 13 | import { EE } from "../ee"; 14 | 15 | export class Navigation extends EE { 16 | constructor({ cache } = { cache: defaultCache }) { 17 | super({ cache }); 18 | this.cache = cache; 19 | } 20 | 21 | // XXX add caching 22 | // XXX support getting children from the node interface 23 | async getFromId(id) { 24 | return this.cache 25 | .get(id, () => 26 | Navee.findOne({ 27 | where: { navee_id: id }, 28 | include: [{ model: Sites.model }] 29 | }) 30 | ) 31 | .then(x => { 32 | x.site_pages = Sites.parsePage(x.exp_site.site_pages)[x.site_id]; 33 | return x; 34 | }) 35 | .then(x => { 36 | if (x.type === "pages" && x.entry_id) { 37 | x.link = x.site_pages.uris[x.entry_id]; 38 | } 39 | 40 | return { 41 | link: x.link, 42 | id: x.navee_id, 43 | text: x.text, 44 | url: x.site_pages.url, 45 | parent: x.parent, 46 | sort: x.sort, 47 | image: x.custom 48 | }; 49 | }); 50 | } 51 | 52 | async find({ nav }) { 53 | const navigation = {}; 54 | const orphans = []; 55 | return await NaveeNav.find({ 56 | where: { nav_title: nav }, 57 | include: [{ model: Navee.model }, { model: Sites.model }] 58 | }) 59 | .then(data => 60 | data.map(x => { 61 | x.exp_navee.site_pages = Sites.parsePage(x.exp_site.site_pages)[ 62 | x.site_id 63 | ]; 64 | return x.exp_navee; 65 | }) 66 | ) 67 | .then(data => 68 | data.map(x => { 69 | if (x.type === "pages" && x.entry_id) { 70 | x.link = x.site_pages.uris[x.entry_id]; 71 | } 72 | 73 | return { 74 | link: x.link, 75 | id: x.navee_id, 76 | text: x.text, 77 | url: x.site_pages.url, 78 | parent: x.parent, 79 | sort: x.sort, 80 | image: x.custom 81 | }; 82 | }) 83 | ) 84 | .then(data => { 85 | // get all parents 86 | data 87 | .filter(x => x.parent === 0) 88 | .forEach(x => { 89 | navigation[x.id] = x; 90 | }); 91 | // get all children 92 | data 93 | .filter(x => x.parent !== 0) 94 | .forEach(x => { 95 | if (navigation[x.parent]) { 96 | navigation[x.parent].children || 97 | (navigation[x.parent].children = []); // tslint:disable-line 98 | navigation[x.parent].children.push(x); 99 | return; 100 | } 101 | // XXX make this recursive 102 | orphans.push(x); 103 | }); 104 | return [navigation]; 105 | }) 106 | .then(() => { 107 | const results = []; 108 | for (const parent in navigation) { 109 | const item = navigation[parent]; 110 | item.children = orderBy(item.children, "sort"); 111 | results.push(item); 112 | } 113 | return orderBy(results, "sort"); 114 | }); 115 | } 116 | } 117 | 118 | export default { 119 | Navigation 120 | }; 121 | -------------------------------------------------------------------------------- /src/apollos/models/users/resolver.js: -------------------------------------------------------------------------------- 1 | import get from "lodash/get"; 2 | import pick from "lodash/pick"; 3 | import { FOLLOWABLE_TOPICS } from "../../../constants"; 4 | 5 | export default { 6 | Query: { 7 | currentUser(_, args, { user, person }) { 8 | if (!user || !person) return null; 9 | return { user, person }; 10 | }, 11 | topics() { 12 | return FOLLOWABLE_TOPICS; 13 | } 14 | }, 15 | 16 | UserTokens: { 17 | tokens: ({ loginTokens }) => loginTokens 18 | }, 19 | 20 | UserRock: { 21 | id: ({ PersonId }) => PersonId, 22 | alias: ({ PrimaryAliasId }) => PrimaryAliasId 23 | }, 24 | 25 | UserService: { 26 | rock: ({ rock }) => rock, 27 | resume: ({ resume }) => resume 28 | }, 29 | 30 | User: { 31 | id: ({ user, person } = {}) => { 32 | const mongoId = get(user, "_id"); // Deprecated 33 | const rockId = get(person, "PrimaryAliasId"); 34 | return rockId || mongoId; 35 | }, 36 | createdAt: ({ user } = {}) => { 37 | const { 38 | createdAt, // Deprecated Mongo User 39 | CreatedDateTime // Rock User 40 | } = user; 41 | return CreatedDateTime || createdAt; 42 | }, 43 | services: (props = {}) => get(props, "user.services"), // Deprecated 44 | emails: (props = {}) => get(props, "user.emails"), // Deprecated 45 | email: ({ user, person }) => { 46 | const email = get(user, "emails.0.address"); 47 | if (email) return email; // Deprecated Mongo User 48 | 49 | // Rock Profile 50 | return person.Email; 51 | }, 52 | followedTopics({ person }, $, { models }) { 53 | return models.User.getUserFollowingTopics(person.PrimaryAliasId); 54 | } 55 | }, 56 | 57 | Mutation: { 58 | loginUser(_, props, { models }) { 59 | return models.User.loginUser(props); 60 | }, 61 | registerUser(_, props, { models }) { 62 | return models.User.registerUser(props); 63 | }, 64 | logoutUser(_, props, { models, authToken, user }) { 65 | return models.User.logoutUser({ 66 | token: authToken, 67 | loginId: user && user.Id 68 | }); 69 | }, 70 | forgotUserPassword(_, props, { models }) { 71 | const { email, sourceURL } = props; 72 | return models.User.forgotPassword(email, sourceURL); 73 | }, 74 | resetUserPassword(_, props, { models }) { 75 | const { token, newPassword } = props; 76 | return models.User.resetPassword(token, newPassword); 77 | }, 78 | changeUserPassword(_, props, { models, user }) { 79 | const { oldPassword, newPassword } = props; 80 | return models.User.changePassword(user, oldPassword, newPassword); 81 | }, 82 | toggleTopic(_, props, { models, person }) { 83 | const { topic } = props; 84 | return models.User.toggleTopic({ 85 | topic, 86 | userId: person.PrimaryAliasId 87 | }); 88 | }, 89 | updateProfile(_, { input } = {}, { models, person }) { 90 | return models.User.updateProfile( 91 | person.Id, 92 | pick(input, [ 93 | "NickName", 94 | "FirstName", 95 | "LastName", 96 | "Email", 97 | "BirthMonth", 98 | "BirthDay", 99 | "BirthYear", 100 | "Campus" 101 | ]) 102 | ); 103 | }, 104 | updateHomeAddress(_, { input } = {}, { models, person }) { 105 | return models.User.updateHomeAddress( 106 | person.Id, 107 | pick(input, ["Street1", "Street2", "City", "State", "PostalCode"]) 108 | ); 109 | } 110 | } 111 | }; 112 | -------------------------------------------------------------------------------- /src/apollos/mongo.js: -------------------------------------------------------------------------------- 1 | import mongoose, { Schema } from "mongoose"; 2 | // import DataLoader from "dataloader"; 3 | mongoose.Promise = global.Promise; 4 | 5 | let db = mongoose.connect( 6 | process.env.MONGO_URL, 7 | { 8 | server: { reconnectTries: Number.MAX_VALUE } 9 | } 10 | ); 11 | let dd; 12 | 13 | // NOTE: Wherever this is called it's too late in the game for 14 | // use to take advantage of indexing 15 | export function connect(monitor) { 16 | if (db) return Promise.resolve(true); 17 | dd = monitor && monitor.datadog; 18 | return new Promise(cb => { 19 | db = mongoose.connect( 20 | process.env.MONGO_URL, 21 | { 22 | server: { reconnectTries: Number.MAX_VALUE } 23 | }, 24 | err => { 25 | if (err) { 26 | db = false; 27 | cb(false); 28 | return; 29 | } 30 | 31 | cb(true); 32 | } 33 | ); 34 | }); 35 | } 36 | 37 | mongoose.connection.on( 38 | "error", 39 | console.error.bind(console, "MONGO connection error:") 40 | ); 41 | 42 | export class MongoConnector { 43 | constructor(collection, schema, indexes = []) { 44 | this.db = db; 45 | this.schema = new Schema(schema); 46 | indexes.forEach(({ keys, options } = {}) => 47 | this.schema.index(keys, options) 48 | ); 49 | this.model = mongoose.model(collection, this.schema); 50 | this.count = 0; 51 | 52 | // XXX integrate data loader 53 | } 54 | 55 | findOne(...args) { 56 | return this.time(this.model.findOne.apply(this.model, args)); 57 | } 58 | 59 | time(promise) { 60 | const prefix = "MongoConnector"; 61 | const count = this.getCount(); 62 | const start = new Date(); 63 | const label = `${prefix}-${count}`; 64 | if (dd) dd.increment(`${prefix}.transaction.count`); 65 | console.time(label); 66 | return promise 67 | .then(x => { 68 | const end = new Date(); 69 | if (dd) dd.histogram(`${prefix}.transaction.time`, end - start, [""]); 70 | console.timeEnd(label); 71 | return x; 72 | }) 73 | .catch(x => { 74 | const end = new Date(); 75 | if (dd) dd.histogram(`${prefix}.transaction.time`, end - start, [""]); 76 | if (dd) dd.increment(`${prefix}.transaction.error`); 77 | console.timeEnd(label); 78 | return x; 79 | }); 80 | } 81 | 82 | find(...args) { 83 | const label = `MongoConnector${this.getCount()}`; 84 | console.time(label); 85 | return this.model.find.apply(this.model, args).then(x => { 86 | console.timeEnd(label); 87 | return x; 88 | }); 89 | } 90 | 91 | remove(...args) { 92 | const label = `MongoConnector${this.getCount()}`; 93 | console.time(label); 94 | return this.model.remove.apply(this.model, args).then(x => { 95 | console.timeEnd(label); 96 | return x; 97 | }); 98 | } 99 | 100 | create(...args) { 101 | const label = `MongoConnector${this.getCount()}`; 102 | console.time(label); 103 | return this.model.create.apply(this.model, args).then(x => { 104 | console.timeEnd(label); 105 | return x; 106 | }); 107 | } 108 | 109 | distinct(field, query) { 110 | const label = `MongoConnector${this.getCount()}`; 111 | console.time(label); 112 | return this.model.distinct(field, query).then(x => { 113 | console.timeEnd(label); 114 | return x; 115 | }); 116 | } 117 | 118 | aggregate(...args) { 119 | return this.model.aggregate(...args); 120 | } 121 | 122 | getCount() { 123 | this.count++; 124 | return this.count; 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/expression-engine/models/content/tables.js: -------------------------------------------------------------------------------- 1 | import { INTEGER, STRING, CHAR } from "sequelize"; 2 | 3 | import { MySQLConnector } from "../../mysql"; 4 | 5 | const channelSchema = { 6 | channel_id: { type: INTEGER, primaryKey: true }, 7 | channel_name: { type: STRING }, 8 | field_group: { type: INTEGER } 9 | }; 10 | 11 | const channelFieldSchema = { 12 | field_id: { type: INTEGER, primaryKey: true }, 13 | group_id: { type: INTEGER }, 14 | field_name: { type: STRING }, 15 | field_label: { type: STRING } 16 | }; 17 | 18 | const channelTitleSchema = { 19 | entry_id: { type: INTEGER, primaryKey: true }, 20 | title: { type: STRING }, 21 | status: { type: STRING }, 22 | channel_id: { type: INTEGER }, 23 | url_title: { type: STRING }, 24 | year: { type: CHAR }, 25 | day: { type: CHAR }, 26 | month: { type: CHAR }, 27 | entry_date: { type: INTEGER }, 28 | expiration_date: { type: INTEGER } 29 | }; 30 | 31 | const channelDataSchema = { 32 | entry_id: { type: INTEGER, primaryKey: true }, 33 | channel_id: { type: INTEGER }, 34 | site_id: { type: INTEGER } 35 | }; 36 | 37 | const lowReorderSetSchema = { 38 | set_id: { type: INTEGER, primaryKey: true }, 39 | set_name: { type: STRING } 40 | }; 41 | 42 | const lowReorderOrderSchema = { 43 | set_id: { type: INTEGER, primaryKey: true }, 44 | sort_order: { type: STRING } 45 | }; 46 | 47 | let Channels; 48 | let ChannelFields; 49 | let ChannelTitles; 50 | let ChannelData; 51 | let LowReorder; 52 | let LowReorderOrder; 53 | export { 54 | Channels, 55 | channelSchema, 56 | ChannelFields, 57 | channelFieldSchema, 58 | ChannelTitles, 59 | channelTitleSchema, 60 | ChannelData, 61 | channelDataSchema, 62 | LowReorder, 63 | lowReorderSetSchema, 64 | LowReorderOrder, 65 | lowReorderOrderSchema 66 | }; 67 | 68 | export function connect() { 69 | Channels = new MySQLConnector("exp_channels", channelSchema); 70 | ChannelFields = new MySQLConnector("exp_channel_fields", channelFieldSchema); 71 | ChannelTitles = new MySQLConnector("exp_channel_titles", channelTitleSchema); 72 | ChannelData = new MySQLConnector("exp_channel_data", channelDataSchema); 73 | LowReorder = new MySQLConnector("exp_low_reorder_sets", lowReorderSetSchema); 74 | LowReorderOrder = new MySQLConnector( 75 | "exp_low_reorder_orders", 76 | lowReorderOrderSchema 77 | ); 78 | 79 | return { 80 | Channels, 81 | ChannelFields, 82 | ChannelTitles, 83 | ChannelData, 84 | LowReorder, 85 | LowReorderOrder 86 | }; 87 | } 88 | 89 | export function bind({ 90 | Channels, 91 | ChannelTitles, 92 | ChannelData, 93 | ChannelFields, 94 | LowReorder, 95 | LowReorderOrder 96 | }) { 97 | Channels.model.hasMany(ChannelTitles.model, { foreignKey: "channel_id" }); 98 | Channels.model.hasMany(ChannelData.model, { foreignKey: "channel_id" }); 99 | 100 | Channels.model.belongsTo(ChannelFields.model, { 101 | foreignKey: "field_group", 102 | targetKey: "group_id" 103 | }); 104 | ChannelFields.model.hasOne(Channels.model, { foreignKey: "field_group" }); 105 | 106 | ChannelTitles.model.belongsTo(Channels.model, { foreignKey: "channel_id" }); 107 | ChannelTitles.model.hasMany(ChannelData.model, { foreignKey: "entry_id" }); 108 | 109 | ChannelData.model.belongsTo(Channels.model, { foreignKey: "channel_id" }); 110 | ChannelData.model.belongsTo(ChannelTitles.model, { foreignKey: "entry_id" }); 111 | 112 | LowReorderOrder.model.belongsTo(LowReorder.model, { 113 | foreignKey: "set_id", 114 | targetKey: "set_id" 115 | }); 116 | } 117 | 118 | export default { 119 | connect, 120 | bind 121 | }; 122 | -------------------------------------------------------------------------------- /scripts/commit.js: -------------------------------------------------------------------------------- 1 | // a mix between https://github.com/commitizen/cz-conventional-changelog and 2 | // https://github.com/commitizen/cz-jira-smart-commit 3 | 4 | "format cjs"; 5 | 6 | var wrap = require('word-wrap'); 7 | 8 | // This can be any kind of SystemJS compatible module. 9 | // We use Commonjs here, but ES6 or AMD would do just 10 | // fine. 11 | module.exports = { 12 | 13 | // When a user runs `git cz`, prompter will 14 | // be executed. We pass you cz, which currently 15 | // is just an instance of inquirer.js. Using 16 | // this you can ask questions and get answers. 17 | // 18 | // The commit callback should be executed when 19 | // you're ready to send back a commit template 20 | // to git. 21 | // 22 | // By default, we'll de-indent your commit 23 | // template and will keep empty lines. 24 | prompter: function(cz, commit) { 25 | console.log('\nAll lines will be wrapped after 100 characters.\n'); 26 | 27 | // Let's ask some questions of the user 28 | // so that we can populate our commit 29 | // template. 30 | // 31 | // See inquirer.js docs for specifics. 32 | // You can also opt to use another input 33 | // collection library if you prefer. 34 | cz.prompt([ 35 | { 36 | type: 'list', 37 | name: 'type', 38 | message: 'Select the type of change that you\'re committing:', 39 | choices: [ 40 | { 41 | name: 'feat: A new feature', 42 | value: 'feat' 43 | }, { 44 | name: 'fix: A bug fix', 45 | value: 'fix' 46 | }, { 47 | name: 'test: Adding missing tests', 48 | value: 'test' 49 | }, { 50 | name: 'style: Changes that do not affect the meaning of the code\n (white-space, formatting, missing semi-colons, etc)', 51 | value: 'style' 52 | }, { 53 | name: 'refactor: A code change that neither fixes a bug or adds a feature', 54 | value: 'refactor' 55 | }, { 56 | name: 'docs: Documentation only changes', 57 | value: 'docs' 58 | }, { 59 | name: 'perf: A code change that improves performance', 60 | value: 'perf' 61 | }, { 62 | name: 'chore: Changes to the build process or auxiliary tools\n and libraries such as documentation generation', 63 | value: 'chore' 64 | }] 65 | }, { 66 | type: 'input', 67 | name: 'issues', 68 | message: 'Jira Issue ID(s) (required):\n', 69 | }, { 70 | type: 'input', 71 | name: 'subject', 72 | message: 'Write a short description of the change (required):\n', 73 | }, { 74 | type: 'input', 75 | name: 'body', 76 | message: 'Provide a longer description of the change [Sent to JIRA] (optional):\n' 77 | }, { 78 | type: 'confirm', 79 | name: 'ci', 80 | message: 'Run this build on CI?\n' 81 | } 82 | ]).then(function(answers) { 83 | try { 84 | var wrapOptions = { 85 | trim: true, 86 | newline: '\n', 87 | indent:'', 88 | width: 100 89 | }; 90 | 91 | var issues = answers.issues.trim(); 92 | var body = answers.body ? '#comment ' + answers.body.trim(): ''; 93 | var ci = answers.ci ? '' : ' [ci skip]'; 94 | 95 | // Hard limit this line 96 | var head = answers.type + ': ' + answers.subject + ' ' + issues + ci; 97 | if (body) head = head + ' ' + body; 98 | 99 | // Wrap these lines at 100 characters 100 | var body = wrap(body, wrapOptions); 101 | 102 | if (!body) body = ''; 103 | 104 | commit(head + '\n\n' + body); 105 | } catch (e) { 106 | console.log("COMMIT ERROR: ", e); 107 | } 108 | 109 | }); 110 | } 111 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "heighliner", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "jest", 8 | "coverage": "jest --coverage", 9 | "commit": "git-cz", 10 | "lint-fix": "eslint ./src --fix", 11 | "lint": "eslint ./src", 12 | "start": "NODE_ENV=development webpack --watch & ./node_modules/.bin/nodemon ./lib/server.js", 13 | "build": "NODE_ENV=development webpack", 14 | "build:production": "NODE_ENV=production webpack", 15 | "danger": "danger run --verbose" 16 | }, 17 | "author": "NewSpring", 18 | "jest": { 19 | "moduleFileExtensions": [ 20 | "js", 21 | "json" 22 | ], 23 | "roots": [ 24 | "./src" 25 | ], 26 | "collectCoverageFrom": [ 27 | "src/**/*.js" 28 | ], 29 | "testRegex": "./__tests__/.*\\.(js)$", 30 | "testEnvironment": "node", 31 | "collectCoverage": false 32 | }, 33 | "license": "ISC", 34 | "dependencies": { 35 | "@google/maps": "^0.5.5", 36 | "apollo-engine": "^0.5.6", 37 | "apollo-server-express": "^1.1.2", 38 | "bcrypt": "^1.0.3", 39 | "body-parser": "^1.14.2", 40 | "bull": "^2.0.0", 41 | "connect-datadog": "0.0.6", 42 | "cors": "^2.7.1", 43 | "credit-card-type": "^5.0.1", 44 | "datadog-metrics": "^0.3.0", 45 | "dataloader": "^1.1.0", 46 | "express": "^4.13.3", 47 | "geo-from-ip": "^1.0.6", 48 | "google-geocoding": "^0.1.7", 49 | "graphql": "^0.11.3", 50 | "graphql-date": "^1.0.3", 51 | "graphql-tools": "^2.3.0", 52 | "html-pdf": "^2.1.0", 53 | "ical": "^0.5.0", 54 | "isomorphic-fetch": "^2.2.1", 55 | "liquid-node": "^3.0.0", 56 | "lodash": "^4.15.0", 57 | "meteor-random": "^0.0.3", 58 | "moment": "^2.13.0", 59 | "mongoose": "^4.5.8", 60 | "morgan": "^1.7.0", 61 | "mssql-geoparser": "0.0.1", 62 | "mysql": "^2.10.2", 63 | "node-mssql": "0.0.1", 64 | "node-uuid": "^1.4.7", 65 | "optics-agent": "^1.1.6", 66 | "phantomjs-prebuilt": "^2.1.14", 67 | "php-unserialize": "0.0.1", 68 | "promise-timeout": "^1.0.0", 69 | "ramda": "^0.23.0", 70 | "raven": "^0.12.1", 71 | "react": "^15.4.1", 72 | "react-dom": "^15.4.1", 73 | "redis": "^2.4.2", 74 | "redis-commands": "github:newspring/redis-commands", 75 | "sequelize": "^3.24.0", 76 | "striptags": "^3.1.0", 77 | "tedious": "^1.14.0", 78 | "to-pascal-case": "^1.0.0", 79 | "to-snake-case": "^1.0.0", 80 | "truncate": "^2.0.0", 81 | "xml2js": "^0.4.17" 82 | }, 83 | "devDependencies": { 84 | "babel-cli": "^6.9.0", 85 | "babel-core": "^6.21.0", 86 | "babel-eslint": "^6.1.2", 87 | "babel-loader": "^6.2.7", 88 | "babel-plugin-transform-react-jsx": "^6.8.0", 89 | "babel-plugin-transform-runtime": "^6.9.0", 90 | "babel-preset-es2015": "^6.18.0", 91 | "babel-preset-stage-0": "^6.5.0", 92 | "casual": "1.5.3", 93 | "commitizen": "^2.8.5", 94 | "coveralls": "^2.11.9", 95 | "danger": "^0.15.0", 96 | "eslint": "^3.9.1", 97 | "eslint-config-airbnb-base": "^10.0.1", 98 | "eslint-config-prettier": "^3.3.0", 99 | "eslint-plugin-babel": "^4.0.0", 100 | "eslint-plugin-import": "^2.2.0", 101 | "eslint-plugin-prettier": "^3.0.1", 102 | "ghooks": "^1.2.3", 103 | "graphql-tester": "0.0.4", 104 | "jest": "^19.0.0", 105 | "nodemon": "1.18.7", 106 | "npm-install-webpack-plugin": "^4.0.5", 107 | "prettier": "^1.15.3", 108 | "react-test-renderer": "^15.4.1", 109 | "redis-mock": "^0.16.0", 110 | "webpack": "^3.6.0", 111 | "webpack-config-utils": "^2.3.0", 112 | "webpack-dashboard": "^1.0.0-7", 113 | "webpack-dev-middleware": "^1.12.0", 114 | "webpack-dotenv-plugin": "^2.0.2", 115 | "webpack-hot-middleware": "^2.19.0", 116 | "webpack-node-externals": "^1.6.0", 117 | "word-wrap": "^1.1.0" 118 | }, 119 | "config": { 120 | "commitizen": { 121 | "path": "./scripts/commit" 122 | }, 123 | "ghooks": { 124 | "pre-commit": "echo npm run lint" 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/rock/models/likes/model.js: -------------------------------------------------------------------------------- 1 | import uuid from "node-uuid"; 2 | import { defaultCache } from "../../../util/cache"; 3 | import { MongoConnector } from "../../../apollos/mongo"; 4 | import { createGlobalId } from "../../../util/node/model"; 5 | 6 | const schema = { 7 | _id: String, 8 | userId: String, // AKA: PrimaryAliasId (pending migration from mongoId to rockId) 9 | entryId: String, // AKA: id returned by content 10 | type: String, 11 | createdAt: { type: Date, default: Date.now } 12 | }; 13 | 14 | const Model = new MongoConnector("like", schema, [ 15 | { 16 | keys: { userId: 1, entryId: 1 } 17 | }, 18 | { 19 | keys: { createdAt: -1 } 20 | } 21 | ]); 22 | 23 | /* 24 | skip?: Int, // how many to trim off the front 25 | limit?: Int, // max array size at the end 26 | arr!: Array, // input array 27 | emptyRet: any // what to return if array is empty at any point (null, [], {}, etc) 28 | */ 29 | export const safeTrimArray = (skip, limit, arr, emptyRet) => { 30 | if (!arr || !arr.length) return emptyRet; 31 | if (skip && skip >= arr.length) return emptyRet; // skips more than we have 32 | 33 | /* 34 | * first slice: trims the front of the array by "skip" 35 | * second: trims the back. 36 | * it checks for limit, then checks if it would be outside of array bounds 37 | * if it's outside of array bounds, just returns whole array 38 | */ 39 | const trimmed = arr 40 | .slice(skip || 0) 41 | .slice(0, limit ? (limit > arr.length ? arr.length : limit) : null); 42 | 43 | if (!trimmed || !trimmed.length) return emptyRet; 44 | return trimmed; 45 | }; 46 | 47 | export class Like { 48 | __type = "Like"; 49 | 50 | constructor({ cache } = { cache: defaultCache }) { 51 | this.model = Model; 52 | this.cache = cache; 53 | } 54 | 55 | async getFromUserId(userId) { 56 | const guid = createGlobalId(userId); 57 | return await this.cache.get(guid, () => this.model.find({ userId })); 58 | } 59 | 60 | async getLikedContent(userId, node) { 61 | const likes = await this.getFromUserId(userId); 62 | return await likes.map(async like => await node.get(like.entryId)); 63 | } 64 | 65 | async getRecentlyLiked({ limit, skip, cache }, userId, nodeModel) { 66 | const query = userId 67 | ? { userId: { $ne: userId } } // exlude user if there is one 68 | : {}; 69 | 70 | query.createdAt = { $ne: null }; 71 | 72 | const guid = createGlobalId(`${limit}:${skip}:${userId}`, this.__type); 73 | const entryIds = await this.cache.get(guid, async () => { 74 | const likes = await this.model.aggregate([ 75 | { $match: query }, 76 | { $group: { _id: "$entryId", date: { $max: "$createdAt" } } }, 77 | { $sort: { date: -1 } } 78 | ]); 79 | 80 | const ids = likes.map(({ _id }) => _id); 81 | return safeTrimArray(skip, limit, ids, null); 82 | }); 83 | 84 | if (!entryIds || !entryIds.length) return null; 85 | 86 | const promises = entryIds.map(x => nodeModel.get(x)); 87 | return Promise.all(promises).then(likes => likes.filter(x => x)); 88 | } 89 | 90 | async toggleLike(nodeId, userId, nodeModel) { 91 | const existingLike = await this.model.findOne({ 92 | entryId: nodeId, 93 | userId 94 | }); 95 | 96 | if (existingLike) { 97 | await this.model.remove({ 98 | _id: existingLike._id 99 | }); 100 | } else { 101 | await this.model.create({ 102 | _id: uuid.v4(), 103 | userId, 104 | entryId: nodeId, 105 | createdAt: new Date() 106 | }); 107 | } 108 | 109 | const guid = createGlobalId(userId); 110 | await this.cache.del(guid); 111 | 112 | return { 113 | like: nodeModel.get(nodeId), 114 | success: true, 115 | error: "", 116 | code: "" 117 | }; 118 | } 119 | 120 | async hasUserLike({ userId, entryId, entryType } = {}) { 121 | if (!userId || !entryId || !entryType) return false; 122 | return !!(await this.model.findOne({ 123 | entryId: createGlobalId(entryId, entryType), // Why are IDs encrypted? 124 | userId 125 | })); 126 | } 127 | } 128 | 129 | export default { 130 | Like 131 | }; 132 | -------------------------------------------------------------------------------- /src/expression-engine/mysql.js: -------------------------------------------------------------------------------- 1 | import Sequelize, { 2 | Options, 3 | Connection, 4 | Model, 5 | DefineOptions 6 | } from "sequelize"; 7 | 8 | import { merge, isArray } from "lodash"; 9 | // import DataLoader from "dataloader"; 10 | 11 | import { createTables } from "./models"; 12 | 13 | const noop = (...args) => {}; // tslint:disable-line 14 | const loud = console.log.bind(console, "MYSQL:"); // tslint:disable-line 15 | let db; 16 | let dd; 17 | let isReady; 18 | 19 | // MySQL connections 20 | const EESettings = { 21 | user: process.env.MYSQL_USER || "root", 22 | password: process.env.MYSQL_PASSWORD || "password", 23 | database: process.env.MYSQL_DB || "ee_local", 24 | opts: { 25 | host: process.env.MYSQL_HOST, 26 | ssl: process.env.MYSQL_SSL || false 27 | } 28 | }; 29 | 30 | export function connect(monitor) { 31 | if (isReady) return Promise.resolve(true); 32 | dd = monitor && monitor.datadog; 33 | return new Promise(cb => { 34 | const opts = merge({}, EESettings.opts, { 35 | dialect: "mysql", 36 | logging: process.env.LOG === "true" ? loud : noop, // tslint:disable-line 37 | benchmark: process.env.NODE_ENV !== "production", 38 | define: { 39 | timestamps: false, 40 | freezeTableName: true 41 | } 42 | }); 43 | 44 | db = new Sequelize( 45 | EESettings.database, 46 | EESettings.user, 47 | EESettings.password, 48 | opts 49 | ); 50 | 51 | db.authenticate() 52 | .then(() => cb(true)) 53 | .then(() => createTables()) 54 | .then(() => { 55 | isReady = true; 56 | }) 57 | .catch(e => { 58 | console.error(e); // tslint:disable-line 59 | db = false; 60 | cb(false); 61 | }); 62 | }); 63 | } 64 | 65 | export class MySQLConnector { 66 | prefix = "exp_"; 67 | count = 0; 68 | 69 | constructor(tableName, schema = {}, options = {}) { 70 | this.db = db; 71 | options = merge(options, { tableName, underscored: true }); 72 | this.model = db.define(tableName, schema, options); 73 | 74 | // XXX integrate data loader 75 | } 76 | 77 | find(...args) { 78 | return this.time( 79 | this.model.findAll 80 | .apply(this.model, args) 81 | .then(this.getValues) 82 | .then(data => data.map(this.mergeData)) 83 | ); 84 | } 85 | 86 | findOne(...args) { 87 | return this.time( 88 | this.model.findOne 89 | .apply(this.model, args) 90 | .then(x => x.dataValues) 91 | .then(this.mergeData) 92 | ); 93 | } 94 | 95 | mergeData = data => { 96 | const keys = []; 97 | for (const key in data) { 98 | if (key.indexOf(this.prefix) > -1) keys.push(key); 99 | } 100 | 101 | for (const key of keys) { 102 | const table = data[key]; 103 | if (!data[key]) continue; 104 | 105 | if (isArray(table)) { 106 | data[key] = this.getValues(table).map(this.mergeData); 107 | } else if (data[key] && data[key].dataValues) { 108 | data[key] = this.mergeData(data[key].dataValues); 109 | } 110 | } 111 | 112 | return data; 113 | }; 114 | 115 | getValues(data) { 116 | return data.map(x => x.dataValues); 117 | } 118 | 119 | queryCount() { 120 | this.count++; 121 | return this.count; 122 | } 123 | 124 | time(promise) { 125 | const prefix = "MYSQLConnector"; 126 | const count = this.queryCount(); 127 | const start = new Date(); 128 | const label = `${prefix}-${count}`; 129 | if (dd) dd.increment(`${prefix}.transaction.count`); 130 | console.time(label); // tslint:disable-line 131 | return promise 132 | .then(x => { 133 | const end = new Date(); 134 | if (dd) dd.histogram(`${prefix}.transaction.time`, end - start, [""]); 135 | console.timeEnd(label); // tslint:disable-line 136 | return x; 137 | }) 138 | .catch(x => { 139 | const end = new Date(); 140 | if (dd) dd.histogram(`${prefix}.transaction.time`, end - start, [""]); 141 | if (dd) dd.increment(`${prefix}.transaction.error`); 142 | console.timeEnd(label); // tslint:disable-line 143 | return x; 144 | }); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/rock/models/finances/models/__tests__/FinancialBatch.js: -------------------------------------------------------------------------------- 1 | import moment from "moment"; 2 | 3 | import FinancialBatch from "../FinancialBatch"; 4 | import { FinancialBatch as FinancialBatchTable } from "../../tables"; 5 | 6 | import { createGlobalId } from "../../../../../util/node"; 7 | 8 | jest.mock("../../tables", () => ({ 9 | FinancialBatch: { 10 | find: jest.fn(), 11 | findOne: jest.fn(), 12 | delete: jest.fn(), 13 | post: jest.fn() 14 | } 15 | })); 16 | 17 | jest.mock("../../util/nmi"); 18 | 19 | jest.mock("moment"); 20 | 21 | jest.mock("node-uuid", () => ({ 22 | v4: jest.fn(() => "guid") 23 | })); 24 | 25 | const mockedCache = { 26 | get: jest.fn((id, lookup) => Promise.resolve().then(lookup)), 27 | set: jest.fn(() => Promise.resolve().then(() => true)), 28 | del() {}, 29 | encode: jest.fn((obj, prefix) => `${prefix}${JSON.stringify(obj)}`) 30 | }; 31 | 32 | it("sets the __type", () => { 33 | const Local = new FinancialBatch({ cache: mockedCache }); 34 | expect(Local.__type).toBe("FinancialBatch"); 35 | }); 36 | 37 | describe("getFromId", () => { 38 | it("tries to load the passed id", async () => { 39 | const id = 1; 40 | const localCache = { ...mockedCache }; 41 | localCache.get = jest.fn((guid, cb) => cb()); 42 | 43 | const Local = new FinancialBatch({ cache: localCache }); 44 | const nodeId = createGlobalId(1, Local.__type); 45 | FinancialBatchTable.findOne.mockReturnValueOnce([]); 46 | const result = await Local.getFromId(1); 47 | 48 | expect(localCache.get.mock.calls[0][0]).toEqual(nodeId); 49 | expect(FinancialBatchTable.findOne).toBeCalledWith({ where: { Id: id } }); 50 | expect(result).toEqual([]); 51 | }); 52 | }); 53 | 54 | describe("findOrCreate", () => { 55 | it("looks up batches based on currencyType and date", async () => { 56 | const localCache = { ...mockedCache }; 57 | localCache.get = jest.fn((guid, cb) => cb()); 58 | 59 | const Local = new FinancialBatch({ cache: localCache }); 60 | FinancialBatchTable.find.mockReturnValueOnce([{ Id: 1 }]); 61 | const result = await Local.findOrCreate({ 62 | currencyType: "Visa", 63 | date: "date" 64 | }); 65 | 66 | expect(FinancialBatchTable.find).toBeCalledWith({ 67 | where: { 68 | Status: 1, 69 | BatchStartDateTime: { $lte: "date" }, 70 | BatchEndDateTime: { $gt: "date" }, 71 | Name: "Online Giving Visa" 72 | } 73 | }); 74 | expect(result).toEqual({ Id: 1 }); 75 | }); 76 | 77 | it("looks up batches based on currencyType and date", async () => { 78 | const localCache = { ...mockedCache }; 79 | localCache.get = jest.fn((guid, cb) => cb()); 80 | 81 | const Local = new FinancialBatch({ cache: localCache }); 82 | FinancialBatchTable.find.mockReturnValueOnce([]); 83 | 84 | const toISOString = jest.fn(() => "date"); 85 | // const subtract = jest.fn(() => ({ toISOString })); 86 | const startOf = jest.fn(() => ({ toISOString })); 87 | const endOf = jest.fn(() => ({ toISOString })); 88 | 89 | moment.mockReturnValueOnce({ startOf }).mockReturnValueOnce({ endOf }); 90 | 91 | FinancialBatchTable.post.mockReturnValueOnce(Promise.resolve({ Id: 1 })); 92 | FinancialBatchTable.findOne.mockReturnValueOnce({ Id: 1 }); 93 | const result = await Local.findOrCreate({ 94 | currencyType: "Visa", 95 | date: "date" 96 | }); 97 | 98 | expect(moment).toBeCalledWith("date"); 99 | expect(startOf).toBeCalledWith("day"); 100 | // expect(subtract).toBeCalledWith(1, "minute"); 101 | expect(toISOString).toBeCalled(); 102 | 103 | expect(moment).toBeCalledWith("date"); 104 | expect(endOf).toBeCalledWith("day"); 105 | // expect(subtract).toBeCalledWith(1, "minute"); 106 | expect(toISOString).toBeCalled(); 107 | 108 | expect(FinancialBatchTable.post).toBeCalledWith({ 109 | Guid: "guid", 110 | Name: "Online Giving Visa", 111 | Status: 1, 112 | ControlAmount: 0, 113 | BatchStartDateTime: "date", 114 | BatchEndDateTime: "date" 115 | }); 116 | expect(FinancialBatchTable.findOne).toBeCalledWith({ 117 | where: { Id: 1 } 118 | }); 119 | expect(result).toEqual({ Id: 1 }); 120 | }); 121 | }); 122 | --------------------------------------------------------------------------------