diff --git a/demo.txt b/demo.txt new file mode 100644 index 000000000..bbd495fb8 --- /dev/null +++ b/demo.txt @@ -0,0 +1,463 @@ +import { + camelizeKeys, + decamelizeKeys + // decamelize +} from 'humps'; +import { + Model + // raw +} from 'objection'; +import { + knex, + returnId + // orderedFor +} from '@gqlapp/database-server-ts'; +import { User } from '@gqlapp/user-server-ts/sql'; +// import { has } from 'lodash'; + +Model.knex(knex); + +export interface GroupMemberInput { + email: string; + type: string; + status: string; + groupId: number; +} + +export interface GroupInput { + title: string; + avatar: string; + description: string; + groupType: string; + members: MemberInput[]; +} + +export interface MemberInput { + id: number; + email: string; + type: string; + status: string; +} + +export interface Identifier { + id: number; +} + +export interface EmailIdentifier { + email: string; +} + +// const eager = '[author.[profile], group]'; +const eager = '[author, group, tags]'; + +export default class GroupMember extends Model { + // private id: any; + + static get tableName() { + return 'group_member'; + } + + static get idColumn() { + return 'id'; + } + + static get relationMappings() { + return { + member: { + relation: Model.BelongsToOneRelation, + groupClass: User, + join: { + from: 'group_member.email', + to: 'user.email' + } + }, + group: { + relation: Model.BelongsToOneRelation, + groupClass: Group, + join: { + from: 'group_member.group_id', + to: 'group.id' + } + }, + }; + } + + public async allGroupMembers() { + return camelizeKeys( + await GroupMember.query() + .eager(eager) + .orderBy('id', 'desc') + ); + } + + public async groupMembers(id: number) { + return camelizeKeys( + await GroupMember.query() + .where({ group_id: id }) + .eager(eager) + .orderBy('id', 'desc') + ); + } + + public async groupMember(id: number) { + const res = camelizeKeys( + await GroupMember.query() + .eager(eager) + .findById(id) + ); + return res; + } + + public async addGroupMember(input: GroupMemberInput) { + const res = await GroupMember.query().insertGraph(decamelizeKeys(input)); + return res.id; + } + + public async editGroupMember(input: Identifier & GroupMemberInput) { + const res = await GroupMember.query().upsertGraph(decamelizeKeys(input)); + return res.id; + } + + public async deleteGroupMember(id: number) { + return knex('group_member') + .where({ id }) + .del(); + } +} + +export class Group extends Model { + static get tableName() { + return 'group'; + } + + static get idColumn() { + return 'id'; + } + + static get relationMappings() { + return { + members: { + relation: Model.HasManyRelation, + groupClass: GroupMember, + join: { + from: 'group.id', + to: 'group_member.group_id' + } + } + }; + } + + public async groups() { + return camelizeKeys(await Group.query() + .eager(gEager).orderBy('id', 'desc')); + } + + public async group(id: number) { + const res = camelizeKeys(await Group.query() + .eager(gEager).findById(id)); + return res; + } + + public async userGroups(email: EmailIdentifier) { + const res = camelizeKeys(await Group.query() + .eager(gEager).where({ email }).orderBy('id', 'desc'))); + return res; + } + + public async addGroup(input: GroupInput) { + const res = await Group.query().insertGraph(decamelizeKeys(input)); + return res.id; + } + + public async updateGroup(input: GroupInput & Identifier) { + const res = await Group.query().upsertGraph(decamelizeKeys(input)); + return res.id; + } + + public async deleteGroup(id: number) { + return knex('group') + .where({ id }) + .del(); + } +} + + + + + + +type Group { + id: Int! + title: String! + avatar: String! + description: String! + members: [GroupMember] + groupType: String! + createdAt: String! + updatedAt: String! + } + + type GroupMember { + id: Int! + email: String! + type: String! + status: String! + group: Group! + member: User + createdAt: String! + updatedAt: String! + } + + extend type Query { + groups: [Group] + userGroups(email: String): [Group] + group(id: Int!): Group + allGroupMembers: [GroupMember] + groupMember(id: Int!): GroupMember + groupMembers(id: Int!): [GroupMember] + } + + extend type Mutation { + addGroupMember(input: AddGroupMemberInput!): GroupMember + editGroupMember(input: EditGroupMemberInput!): GroupMember + deleteGroupMember(id: Int!): GroupMember + addGroup(input: AddGroupInput!): Group + updateGroup(input: UpdateGroupInput!): Group + deleteGroup(id: Int!): Group + } + + input AddGroupInput { + title: String! + avatar: String + description: String! + groupType: String! + members: [MemberInput] + } + + input MemberInput { + id: Int + email: String! + type: String! + status: String! + } + + input UpdateGroupInput { + id: Int! + title: String + avatar: String + description: String + groupType: String + } + + input AddGroupMemberInput { + email: String! + type: String! + status: String! + groupId: Int! + } + + input EditGroupMemberInput { + id: Int! + email: String + type: String + status: String + } + + extend type Subscription { + groupUpdated: UpdateGroupPayload + groupMembersUpdated: UpdateGroupMembersPayload + } + + type UpdateGroupMembersPayload { + mutation: String! + node: GroupMember! + } + + type UpdateGroupPayload { + mutation: String! + node: Group! + } + + + + + + + + import { PubSub, withFilter } from 'graphql-subscriptions'; + import withAuth from 'graphql-auth'; + import { GroupInput, GroupMemberInput, Identifier } from './sql'; + + const GROUP_SUBSCRIPTION = 'group_subscription'; + const GMEMBER_SUBSCRIPTION = 'groupMembers_subscription'; + + interface AddGroup { + input: GroupInput; + } + + interface EditGroup { + input: GroupInput & Identifier; + } + + interface EditGroupMember { + input: GroupMemberInput & Identifier; + } + + interface AddGroupMember { + input: GroupMemberInput; + } + + export default (pubsub: PubSub) => ({ + Query: { + async allGroupMembers(obj: any, args: any, context: any) { + return context.GroupMember.allGroupMembers(); + }, + async groupMembers(obj: any, { id }: Identifier, { GroupMember }: any) { + return GroupMember.groupMembers(id); + }, + async groupMember(obj: any, { id }: Identifier, context: any) { + return context.GroupMember.groupMember(id); + }, + async groups(obj: any, args: any, context: any) { + return context.Group.groups(); + }, + async userGroups(obj: any,{ email }: EmailIdentifier, context: any) { + return context.Group.userGroups(email); + }, + async group(obj: any, { id }: Identifier, context: any) { + return context.Group.group(id); + } + }, + Mutation: { + addGroup: withAuth(async (obj: any, { input }: AddGroup, { Group }: any) => { + try { + const id = await Group.addGroup(input); + const item = await Group.group(id); + pubsub.publish(GROUP_SUBSCRIPTION, { + groupUpdated: { + mutation: 'CREATED', + node: item + } + }); + return item; + } catch (e) { + return e; + } + }), + updateGroup: withAuth(async (obj: any, { input }: EditGroup, { Group }: any) => { + try { + await Group.updateGroup(input); + const item = await Group.group(input.id); + pubsub.publish(GROUP_SUBSCRIPTION, { + groupUpdated: { + mutation: 'UPDATED', + node: item + } + }); + return item; + } catch (e) { + return e; + } + }), + deleteGroup: withAuth(async (obj: any, { id }: Identifier, { Group }: any) => { + try { + const data = await Group.group(id); + await Group.deleteGroup(id); + pubsub.publish(GROUP_SUBSCRIPTION, { + groupUpdated: { + mutation: 'DELETED', + node: data + } + }); + return data; + } catch (e) { + return e; + } + }), + addGroupMember: withAuth(async (obj: any, { input }: AddGroupMember, { Group, GroupMember }: any) => { + try { + const id = await GroupMember.addGroupMember(input); + const data = await GroupMember.groupMember(id); + pubsub.publish(GMEMBER_SUBSCRIPTION, { + groupMembersUpdated: { + mutation: 'CREATED', + node: data + } + }); + const item = await Group.group(data.groupId); + pubsub.publish(GROUP_SUBSCRIPTION, { + groupUpdated: { + mutation: 'UPDATED', + node: item + } + }); + return data; + } catch (e) { + return e; + } + }), + editGroupMember: withAuth(async (obj: any, { input }: EditGroupMember, { GroupMember, Group }: any) => { + try { + const inputId = await GroupMember.editGroupMember(input); + const data = await GroupMember.groupMember(inputId); + pubsub.publish(GMEMBER_SUBSCRIPTION, { + groupMembersUpdated: { + mutation: 'UPDATED', + node: data + } + }); + const item = await Group.group(data.groupId); + pubsub.publish(GROUP_SUBSCRIPTION, { + groupUpdated: { + mutation: 'UPDATED', + node: item + } + }); + return data; + } catch (e) { + return e; + } + }), + deleteGroupMember: withAuth(async (obj: any, { id }: Identifier, { GroupMember, Group }: any) => { + try { + const data = await GroupMember.groupMember(id); + await GroupMember.deleteGroupMember(id); + pubsub.publish(GMEMBER_SUBSCRIPTION, { + groupMembersUpdated: { + mutation: 'DELETED', + node: data + } + }); + const item = await Group.group(data.groupId); + pubsub.publish(GROUP_SUBSCRIPTION, { + groupUpdated: { + mutation: 'UPDATED', + node: item + } + }); + return data; + } catch (e) { + return e; + } + }) + }, + Subscription: { + groupUpdated: { + subscribe: withFilter( + () => pubsub.asyncIterator(GROUP_SUBSCRIPTION), + (payload, variables) => { + return payload.groupUpdated.id === variables.id; + } + ) + }, + groupMembersUpdated: { + subscribe: withFilter( + () => pubsub.asyncIterator(GMEMBER_SUBSCRIPTION), + (payload, variables) => { + return payload.groupMembersUpdated.id === variables.id; + } + ) + } + } + }); + \ No newline at end of file diff --git a/modules/authentication/server-ts/access/jwt/resolvers.js b/modules/authentication/server-ts/access/jwt/resolvers.js index 76946f9b0..68f90793b 100644 --- a/modules/authentication/server-ts/access/jwt/resolvers.js +++ b/modules/authentication/server-ts/access/jwt/resolvers.js @@ -7,15 +7,7 @@ import createTokens from './createTokens'; export default () => ({ Mutation: { - async refreshTokens( - obj, - { refreshToken: inputRefreshToken }, - { - getIdentity, - getHash, - req: { t } - } - ) { + async refreshTokens(obj, { refreshToken: inputRefreshToken }, { getIdentity, getHash, req: { t } }) { const decodedToken = jwt.decode(inputRefreshToken); const isValidToken = decodedToken && decodedToken.id; diff --git a/modules/blog/client-react/__tests__/Blog.test.ts b/modules/blog/client-react/__tests__/Blog.test.ts new file mode 100644 index 000000000..a3d31f03f --- /dev/null +++ b/modules/blog/client-react/__tests__/Blog.test.ts @@ -0,0 +1,19 @@ +import { expect } from 'chai'; + +import { updateContent, Renderer } from '@gqlapp/testing-client-react'; + +describe('Blog UI works', () => { + const renderer = new Renderer({}); + const app = renderer.mount(); + renderer.history.push('/Blog'); + const content = updateContent(app.container); + + it('Blog page renders on mount', () => { + // tslint:disable:no-unused-expression + expect(content).to.not.be.empty; + }); + + it('Blog page has title', async () => { + expect(content.textContent).to.include('Hello, This is the Blog module'); + }); +}); diff --git a/modules/blog/client-react/components/AdminBlogsComponent.jsx b/modules/blog/client-react/components/AdminBlogsComponent.jsx new file mode 100644 index 000000000..28de507b9 --- /dev/null +++ b/modules/blog/client-react/components/AdminBlogsComponent.jsx @@ -0,0 +1,156 @@ +/* eslint-disable react/display-name */ +import React, { Fragment } from 'react'; +import PropTypes from 'prop-types'; +import { Link } from 'react-router-dom'; +import { Table, Loading } from '@gqlapp/look-client-react'; +import { Popconfirm, Button, message, Tooltip, Spin, Divider } from 'antd'; +import InfiniteScroll from 'react-infinite-scroll-component'; + +class AdminBlogsComponent extends React.Component { + fetchMoreData = async () => { + let hasMore = this.props.blogs.pageInfo.hasNextPage; + const endCursor = this.props.blogs.pageInfo.endCursor; + if (!hasMore) return; + await this.props.loadData(endCursor + 1, 'add'); + }; + + render() { + const { blogs, deleteBlog, blogLoading } = this.props; + + const cancel = () => { + message.error('Task cancelled'); + }; + + const columns = [ + { + title: {'Id'}, + dataIndex: 'id', + key: 'id', + sorter: (a, b) => a.id - b.id, + sortDirections: ['descend', 'ascend'] + }, + { + title: {'Title'}, + dataIndex: 'title', + key: 'title', + sorter: (a, b) => a.title.length - b.title.length, + sortDirections: ['descend', 'ascend'] + }, + { + title: {'Author'}, + dataIndex: 'author', + key: 'author', + sorter: (a, b) => a.author.username.length - b.author.username.length, + sortDirections: ['descend', 'ascend'], + render: text => + // + // + // { + // text.id === currentUser.id ? "You" : + text.username + // } + // + // + }, + { + title: {'Model'}, + dataIndex: 'model', + key: 'model', + sorter: (a, b) => a.model.name.length - b.model.name.length, + sortDirections: ['descend', 'ascend'], + render: text => {text.name} + }, + { + title: 'Actions', + dataIndex: 'id', + key: 'actions', + render: text => ( + <> + + {' '} + +