import express from 'express';
import { graphqlExpress, graphiqlExpress } from 'graphql-server-express';
import bodyParser from 'body-parser';
import cors from 'cors';
import { schema } from './schema';
import { execute, subscribe } from 'graphql';
import { createServer } from 'http';
import { SubscriptionServer } from 'subscriptions-transport-ws';
import db from 'sqlite';
import connectors from './connectors';
import jwt from'express-jwt';
const { GQLSERVERLOCAL, GQLSERVERPROD, WEBSERVER, PATH, DATABASE, AUTH } = require('./config');
let p = db.open(DATABASE.NAME).then(db =>{
console.error("SQLite: DB ok ");
}).catch(error =>
{
console.error("SQLite: DB error: " + error);
});
const server = express();
const corsOptions = {
origin(origin, callback){
callback(null, true);
},
credentials: true
};
server.use(cors(corsOptions));
server.use(PATH.GQL, bodyParser.json(), jwt({
secret: AUTH.secret,
credentialsRequired: false,
getToken: function fromHeaderOrQuerystring (req) {
if (req.headers.authorization && req.headers.authorization.split(' ')[0] === 'Bearer') {
return req.headers.authorization.split(' ')[1];
} else if (req.query && req.query.token) {
return req.query.token;
}
return null;
}
}), graphqlExpress(req => ({
schema: schema,
context: {
user: req.user ? connectors.findUser(req.user.id) : Promise.resolve(null),
}, // передаётся в context в resolvers.js
})));
// Далее то, что нужно для подписок и обмена сообщениями через websockets
const ws = createServer(server);
server.use(PATH.GIQL, graphiqlExpress({
endpointURL: PATH.GQL,
subscriptionsEndpoint: `ws://${GQLSERVERPROD.HOST}:${GQLSERVERPROD.PORT}/subscriptions`
}));
ws.listen(GQLSERVERPROD.PORT, () => {
console.log(`GraphQL Server v1 is now running on ${GQLSERVERPROD.HOST}:${GQLSERVERPROD.PORT}`);
new SubscriptionServer({
execute,
subscribe,
schema
}, {
server: ws,
path: PATH.SUBS,
});
});
[...]
type DeletePlantResult
{
rows_changed: Int
error: String
}
input IdsInput {
ids: [String]
}
deletePlant(data: IdsInput): DeletePlantResult
[...]
import connectors from './connectors';
export const resolvers = {
Mutation: {
[...]
deletePlant: (root, params, context) => {
// context содержит то, что в него передано в server.js
[...]
const errors = [];
const {data} = params
const ids = data.ids.map(function(id){
return "'" + id + "'"
}).join(",")
return connectors.deletePlant(ids)
.then(rows_changed => ({
rows_changed,
errors
}))
.catch((err) => {
if (err.code && err.message) {
errors.push({
key: err.code,
value: err.message
});
return { errors };
}
});
},//deletePlant
}
[...]
}
deletePlant(ids) {
return new Promise((resolve, reject) => {
return db.run(`
DELETE FROM 'plants' WHERE id IN(${ids})
`)
.then((plant) => {
resolve(plant.changes);
})
.catch((err) => {
return reject(err);
});
})
},//deletePlant
import React from 'react';
import { BootstrapTable, TableHeaderColumn } from 'react-bootstrap-table';
import { reduxForm } from 'redux-form';
import { Button, ProgressBar } from 'react-bootstrap';
[...]
class PlantsPage extends React.Component
{
addEmptyItem() {
this.props.addPlant({ variables: {data: {firm_id:this.props.params.firm_id, user_crt_id:'system_user',user_mod_id:'system_user'}} });
}
[...]
};
render() {
if (this.props.data.loading)
return <ProgressBar active bsStyle="info" now={100} />;
return (
<div className="container">
<h1>Объекты</h1>
<BootstrapTable data={ this.props.data.getPlants.plants } [...] >
<TableHeaderColumn dataField='title' [...] >Название</TableHeaderColumn>
[...]
</BootstrapTable>
<Button [...] onClick={ () => { this.addEmptyItem(); } }>
Добавить
</Button>
</div>
)
}
}
[...]
export default PlantsPage;
import React from 'react';
import { withRouter } from 'react-router';
import { connect } from 'react-redux';
import gql from 'graphql-tag';
import { graphql, compose } from 'react-apollo';
import PlantsPage from "../components/PlantsPage";
class PlantsContainer extends React.Component {
render()
{
return <PlantsPage {...this.props} />;
}
}//class
const gqlGetPlants = gql`
query getForPlants($firm_id:String!,$object_id:String!,$holding_id:String!) {
getPlants(firm_id: $firm_id) {
plants {
id,
title,
shops_count,
[...]
},
errors
{
key,
value,
}
},
}
`;
const gqlGetPlantsProps =
{
options: (ownProps) => ({
pollInterval: 0,
variables: {
firm_id: ownProps.params.firm_id,
object_id: ownProps.params.firm_id,
holding_id: 'all',
},
}),
}
const gqlAddPlant = gql`
mutation ($data: PlantInput) {
addPlant(data: $data)
{
id,
errors {
key
value
}
}
}
`;
const gqlAddPlantProps = {
name: 'addPlant',
options: {
refetchQueries: [
'getForPlants',
],
},
};
[...]
const PlantsWithData =
compose(
graphql(gqlUpdatePlant, gqlUpdatePlantProps),
graphql(gqlGetPlants, gqlGetPlantsProps),
[...]
)(withRouter(PlantsContainer));
const PlantsWithDataAndDispatch = connect(
null, // место для mapStateToProps
null, // место для mapDispatchToProps
)(PlantsWithData);
export default PlantsWithDataAndDispatch
import React from 'react';
import { Field, reduxForm } from 'redux-form';
import { Button, ControlLabel, FormGroup, Alert } from 'react-bootstrap';
const renderErrors = (errors) => (
<Alert bsStyle="warning">
{errors.map((error, index) => <span key={index}>{error.value}</span>)}
</Alert>
);
class ProfilePage extends React.Component
{
render() {
const errors = this.props.errors <= 0 ? null : renderErrors(this.props.errors)
const { handleSubmit } = this.props;
return (
<div className="container">
<form onSubmit={handleSubmit} >
<FormGroup controlId="firstname">
<ControlLabel>Имя:</ControlLabel>
<Field className="form-control" id="firstname" name="firstname" type="text" component="input"
placeholder="Ваше имя (например: 'Иван')"/>
</FormGroup>
[...]
{errors}
<Button type="submit" bsStyle="primary">
Сохранить
</Button>
</form>
</div>
)
}
}
ProfilePage = reduxForm({
form: 'ProfileForm',
enableReinitialize: true,
})(ProfilePage);
// enableReinitialize: true, - если нужно менять значения в полях
export default ProfilePage;
import React from 'react';
import { withRouter } from 'react-router';
import { connect } from 'react-redux';
import gql from 'graphql-tag';
import { graphql, compose } from 'react-apollo';
import ProfilePage from '../components/ProfilePage';
import { toastr } from 'react-redux-toastr'
const jwtDecode = require('jwt-decode');
class ProfileContainer extends React.Component {
constructor(props) {
super(props);
this.state = { errors: [] };
}
handleSubmit(data) {
this.props.updateProfile({ variables: {
data: {
firstname: data.firstname,
[...]
},
id: jwtDecode(localStorage.getItem('token')).id
}})
.then((response) => {
this.setState({
errors: []
});
if (response.data.updateProfile.errors.length <= 0) {
toastr.success('*', 'Данные профиля сохранены', {showCloseButton: false})
} else {
this.setState({
errors: response.data.updateProfile.errors
});
}
})
.catch((err) => {
console.error(err);
toastr.error('*', 'Ошибка при сохранении профиля', {showCloseButton: false})
});
}//handleSubmit
render()
{
return <ProfilePage {...this.props} onSubmit={this.handleSubmit.bind(this)} errors={this.state.errors} />;
}
}//class
const gqlGetProfile = gql`
query getProfile ($user_id: String!) {
getProfile(user_id: $user_id) {
profile {
id,
firstname,
[...]
},
errors
{
key,
value
},
}
}
`;
const gqlGetProfileProps =
{
options: (ownProps) => ({
variables: {
user_id: jwtDecode(localStorage.getItem('token')).id,
},
}),
props: ({ ownProps, data }) => {
if (data.loading) {
return {
initialValues: { test: [] },
errors: []
};
};
return {
initialValues: data.getProfile.profile,
errors: data.getProfile.errors,
};
}
}
const gqlUpdateProfile = gql`
mutation ($data: ProfileInput, $id : String) {
updateProfile(data: $data, id : $id)
{
token,
errors
{
key,
value
},
}
}
`;
const gqlUpdateProfileProps = {
name: 'updateProfile',
options: {
refetchQueries: [
'getProfile',
],
},
};
const ProfileWithData =
compose(
graphql(gqlUpdateProfile, gqlUpdateProfileProps),
graphql(gqlGetProfile, gqlGetProfileProps),
)(withRouter(ProfileContainer));
const ProfileWithDataAndDispatch = connect(
null,
null
)(ProfileWithData);
export default ProfileWithDataAndDispatch
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import CheckpointPage from '../components/CheckpointPage';
import { saveCheckpoint } from '../actions/CheckpointsActions';
import { Actions } from 'react-native-router-flux';
import { ToastAndroid } from 'react-native';
class CheckpointContainer extends Component {
handleSubmit = (data, dispatch) => {
return new Promise((resolve) => {
setTimeout(() => {
this.props.saveCheckpoint(this.props.checkpoint_id,this.props.checkup_id,data.check_note,data.params).then((result)=>{
ToastAndroid.showWithGravity('Данные сохранены', ToastAndroid.SHORT, ToastAndroid.BOTTOM);
return true;
}).catch((error)=>{
ToastAndroid.showWithGravity('Не удалось сохранить данные', ToastAndroid.SHORT, ToastAndroid.BOTTOM);
})
resolve();
}, 100)
})
}
render()
{
return <CheckpointPage {...this.props} onSubmit={this.handleSubmit.bind(this)} />
}
}//class
const mapStateToProps = (state) => {
return {
stateValues: state.checkpointData,
};
};
const mapDispatchToProps = (dispatch) => {
return bindActionCreators ({
saveCheckpoint: (checkpoint_id, checkup_id, note,formData) => saveCheckpoint(checkpoint_id, checkup_id, note, formData),
}, dispatch);
}
export default connect(mapStateToProps, mapDispatchToProps)(CheckpointContainer);
import React, { Component } from 'react'
import styles from '../styles';
import { reduxForm } from 'redux-form/immutable'
import {
ActionsContainer,
Button,
FieldsContainer,
Fieldset,
Form,
} from 'react-native-clean-form'
import {
Input,
} from 'react-native-clean-form/redux-form-immutable'
import {
View,
Text,
StyleSheet,
} from 'react-native'
class CheckpointPage extends Component {
render() {
const { handleSubmit, submitting, onSubmit } = this.props
return (
<View style={{marginTop: 80, flex:1, flexDirection: 'column', }}>
<Form>
<FieldsContainer>
<Fieldset label="ПАРАМЕТРЫ:" last>
{
this.props.stateValues.params.map((param, i) => {
[...]
})
}
</Fieldset>
<Fieldset label="ЗАМЕЧАНИЯ:" last>
<Input name="check_note" label="Замечания:" placeholder="заметки, комментарии..." multiline={true} numberOfLines={2} inlineLabel={false} />
</Fieldset>
</FieldsContainer>
<ActionsContainer>
<Button icon="md-checkmark" iconPlacement="right" onPress={handleSubmit(onSubmit)} submitting={submitting}>Сохранить</Button>
</ActionsContainer>
</Form>
</View>
)
}
}
export default reduxForm({
form: 'Form',
enableReinitialize: true,
keepDirtyOnReinitialize: true,
})(CheckpointPage)
import * as types from '../actions/ActionTypes';
var uuid = require('react-native-uuid');
[...]
export function saveCheckpointSuccess(data) {
return {
type: 'SAVE_CHECKPOINT_SUCCESS',
data
};
}
function txUpdateChecks(result, check_id, note)
{
console.log('CheckpointsActions/txUpdateChecks:', check_id, note);
return new Promise(function(resolve, reject) {
db.transaction((tx)=>{
tx.executeSql(`UPDATE checks SET note = '${note}', dt_mod = '${nowSQL()}' WHERE checks.id = '${check_id}'`).then(([tx,results]) =>{
result = results.rowsAffected;
resolve(result);
}).catch((error) => {
reject(error);
});//catch executeSql
});//db.transaction
})//return
}//txUpdateChecks()
[...]
export function saveCheckpoint(checkpoint_id, checkup_id, note, values)
{
return (dispatch) => {
let result;
return txSelectChecks(checkup_id,checkpoint_id).then((check_id) => {
if (!check_id)
{
txInsertChecks(result,checkpoint_id,checkup_id,note).then((check_id) => txInsertCheckValues(result,check_id,values))
.then((result) => {
dispatch(saveCheckpointSuccess(result));
}).catch(error => {
dispatch(dbFailed({method:'saveCheckpoint !check_id',error}));
});
}//if
else
{
txUpdateChecks(result,check_id,note).then((result) => txUpdateCheckValues(result,check_id,values))
.then((result) => {
dispatch(saveCheckpointSuccess(result));
}).catch(error => {
dispatch(dbFailed({method:'txUpdateChecks/tx',error}));
});
}//else
}).catch(error => {
dispatch(dbFailed({method:'txSelectChecks/tx',error}));
});
}//return
}//saveCheckpoint
Переходы между экранами выполняются вызовами вида:
Actions.routesPage({shop_id:'6f6853d0-a642-11e7-83e7-792a5b00d12c_shop'});
[...]
<uses-permission android:name="android.permission.NFC" />
[...]
<intent-filter>
<action android:name="android.nfc.action.TECH_DISCOVERED"/>
</intent-filter>
<meta-data android:name="android.nfc.action.TECH_DISCOVERED"
android:resource="@xml/nfc_tech_filter" />
[...]
А в nfc_tech_filter.xml, соответственно, перечислить все возможные типы NFC меток:
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<tech-list>
<tech>android.nfc.tech.IsoDep</tech>
</tech-list>
<tech-list>
<tech>android.nfc.tech.NfcA</tech>
</tech-list>
<tech-list>
<tech>android.nfc.tech.NfcB</tech>
</tech-list>
<tech-list>
<tech>android.nfc.tech.NfcF</tech>
</tech-list>
<tech-list>
<tech>android.nfc.tech.NfcV</tech>
</tech-list>
<tech-list>
<tech>android.nfc.tech.Ndef</tech>
</tech-list>
<tech-list>
<tech>android.nfc.tech.NdefFormatable</tech>
</tech-list>
<tech-list>
<tech>android.nfc.tech.MifareClassic</tech>
</tech-list>
<tech-list>
<tech>android.nfc.tech.MifareUltralight</tech>
</tech-list>
<tech-list>
<tech>android.nfc.tech.NfcBarcode</tech>
</tech-list>
</resources>
const nfcListener = DeviceEventEmitter.addListener('NFCCardID', data =>
{
console.log('NFC id', data.id)
[...]
})
[...]
import { SubscriptionClient, addGraphQLSubscriptions } from 'subscriptions-transport-ws';
wsClient = new SubscriptionClient(`ws://10.0.2.2:3002/subscriptions`, {
reconnect: true,
timeout: 10000,
} )
const networkInterfaceWithSubscriptions = addGraphQLSubscriptions(
networkInterface,
wsClient,
);
function dataIdFromObject (result) {
if (result.__typename) {
if (result.id !== undefined) {
return `${result.__typename}:${result.id}`;
}
}
return null;
}
const client = new ApolloClient({
networkInterface: networkInterfaceWithSubscriptions,
customResolvers: {
Query: {
channel: (_, args) => {
return toIdValue(dataIdFromObject({ __typename: 'Channel', id: 1 }))
},
},
},
dataIdFromObject,
});
[...]
[...]
<Button title='Отправить номер метки' icon={{name:'tap-and-play'}} buttonStyle={styles.button} onPress={ () => { this.props.sendTag({variables: { tag: {shop_id : 1, tag_id : data.tagId} } }).then(res => {
ToastAndroid.showWithGravity(`Номер метки отправлен`, ToastAndroid.SHORT, ToastAndroid.BOTTOM);
} ).catch(error =>{
ToastAndroid.showWithGravity(`Ошибка при отправке (сеть?)`, ToastAndroid.SHORT, ToastAndroid.BOTTOM);
}) ; } } />
[...]
[...]
const gqlSendTag = gql`
mutation sendTag($tag: TagInput!) {
sendTag(tag: $tag) {
tag_id
shop_id
title
}
}
`;
const gqlSendTagProps = {
name: 'sendTag',
options: {
},
};
[...]
import express from 'express';
import { createServer } from 'http';
const server = express();
[...]
const ws = createServer(server);
server.use(PATH.GIQL, graphiqlExpress({
endpointURL: PATH.GQL,
subscriptionsEndpoint: `ws://${GQLSERVERLOCAL.HOST}:${GQLSERVERLOCAL.PORT}/subscriptions`
}));
ws.listen(GQLSERVERLOCAL.PORT, () => {
new SubscriptionServer({
execute,
subscribe,
schema
}, {
server: ws,
path: PATH.SUBS,
});
});
[...]
type Mutation {
[...]
sendTag(tag: TagInput!): Tag
}
[...]
input TagInput{
shop_id: String
tag_id: String
title: String
}
type Tag {
shop_id: String
tag_id: String
title: String
}
type Subscription {
tagSent(shop_id: String): Tag
}
[...]
import { PubSub } from 'graphql-subscriptions';
import { withFilter } from 'graphql-subscriptions';
const pubsub = new PubSub();
[...]
export const resolvers = {
Mutation: {
sendTag: (root, { tag }) => {
const newMessage = { title: tag.title, shop_id: tag.shop_id, tag_id: tag.tag_id };
pubsub.publish('tagSent', { tagSent: newMessage, shop_id: '1'});
return newMessage;
},
[...]
},
Subscription: {
tagSent: {
subscribe: withFilter(() => pubsub.asyncIterator('tagSent'), (payload, variables) => {
return payload.shop_id === variables.shop_id;
}),
},
[...]
},
}
[...]
import { SubscriptionClient, addGraphQLSubscriptions } from 'subscriptions-transport-ws';
wsClient = new SubscriptionClient(`ws://localhost:3002/subscriptions`, {
reconnect: true,
})
const networkInterfaceWithSubscriptions = addGraphQLSubscriptions(
networkInterface,
wsClient,
);
function dataIdFromObject (result) {
if (result.__typename) {
if (result.id !== undefined) {
return `${result.__typename}:${result.id}`;
}
}
return null;
}
const client = new ApolloClient({
networkInterface: networkInterfaceWithSubscriptions,
customResolvers: {
Query: {
channel: (_, args) => {
return toIdValue(dataIdFromObject({ __typename: 'Channel', id: 1 }))
},
},
},
dataIdFromObject,
});
[...]
import { Field } from 'redux-form';
[...]
<Field className="form-control" id="tag_id" name="tag_id" type="text" component="input" placeholder="В приложении на телефоне выберите 'привязка метки'"/>
[...]
[...]
import { change as changeFieldValue } from 'redux-form';
const tagSubscription = gql`
subscription tagSent($shop_id: String) {
tagSent(shop_id: $shop_id) {
tag_id
shop_id
title
},
}
`
class AddCheckpointContainer extends React.Component {
componentWillMount() {
this.props.data.subscribeToMore({
document: tagSubscription,
variables: {
shop_id: 1,
},
onError: (err) => console.error('subscribeToMore ERROR:',err),
updateQuery: (prev, {subscriptionData}) => {
console.log('updateQuery:',subscriptionData)
this.props.changeFieldValue("addCheckpointForm", "tag_id", subscriptionData.data.tagSent.tag_id);
}
});
}//componentWillMount()
[...]
}
const util = require('util')
util.inspect.defaultOptions.showHidden = false
util.inspect.defaultOptions.depth = null
util.inspect.defaultOptions.maxArrayLength = 1000
util.inspect.defaultOptions.colors = true
[...]
console.log("data: ", util.inspect(data))
variable.then(function(result) { console.log('Эврика!',result) })