I'm using Angular2 RC5 with an ASP.NET Core server that makes the API calls to get my data.
I'm actually wondering if there is a way to reduce the number of http calls you make with Angular2, because I fear there will be a lot if I keep using components the way I do. Here is a concrete example.
I want to get a text value from the database, which is defined by an ID and a Language. I then made the following component :
dico.component.ts
#Component({
selector: 'dico',
template: `{{text}}`,
providers: [EntitiesService]
})
class Dico implements AfterViewInit {
#Input() private id: string;
#Input() private lang: string;
private text: string = null;
// DI for my service
constructor(private entitiesService: EntitiesService) {
}
ngAfterViewInit() {
this.getDico();
}
// Call the service that makes the http call to my ASP Controller
getDico() {
this.entitiesService.getDico(this.id, this.lang)
.subscribe(
DicoText => this.text = DicoText
);
}
}
#Component({
template: `<dico [id] [lang]></dico>`,
directives: [Dico]
})
export class DicoComponent {
}
Here is the code from my service :
entities.service.ts
getDico(aDicoID: string, aLangue: string) {
// Parameters to use in my controller
let params = new URLSearchParams();
params.set("aDicoID", aDicoID);
params.set("aLangue", aLangue);
// Setting up the Http request
let lHttpRequestBody = params.toString();
let lControllerAction: string = "/libelle";
let lControllerFullURL: string = this.controllerURL + lControllerAction;
let headers = new Headers({ 'Content-Type': 'application/x-www-form-urlencoded' });
let options = new RequestOptions({ headers: headers });
return this.http.post(lControllerFullURL, lHttpRequestBody, options)
.map((res: any) => {
// Parsing the data from the response
let data = res.json();
// Managing the error cases
switch (data.status) {
case "success":
let l_cRet: string = data.results;
if (l_cRet != null && !l_cRet.includes("UNDEFINED")) {
return data.results;
} else {
throw new Error("Erreur récupération Dico : " + l_cRet);
}
case "error":
throw new Error("Erreur récupération Dico : " + data.message);
}
}
).catch(this.handleError);
}
Then I can use my newly made component in my app :
randomFile.html
<dico id="201124" lang="it"></dico>
<dico id="201125" lang="en"></dico>
<dico id="201126" lang="fr"></dico>
But this application will eventually use hundreds of these "dico" and I was wondering how I could manage some pre-fetch or something like that before the app fully loads. Does it even matter ? Will that affect performance in the long term ?
Any advice would be greatly appreciated.
EDIT : These dico allow me to fetch from the database a text translated into the langage I want. Here, in the example above, I have 3 "dico" that will output some text in italian, french, and english.
My application will use a lot of them, as every text in every menu will be a "dico", and the problem is that there will be a lot of them, and right now for every "dico" I make, my service is called and makes one http call to get the data. What I want is to somehow define all my dicos, call the service which will give me the text of all my dicos in an array to avoid making several calls (but I don't really know how to do that).
A basic untested approach (I don't know observables too well myself)
class DicoService {
private subjects = {}
private ids = [];
getDico(String id):Observable<Dico> {
var s = this.subjects[id];
if(!s) {
this.ids.push(id);
s = new Subject();
this.subjects[id]=s;
}
return s.asObservable().share().first();
}
sendRequest() {
http.get(....) /* pass this.ids */
map(response => response.json())
.subscribe(data => {
for(item in data) { // don't know how to iterate exactly because I don't know how the response will look like
this.subject[item.id].next(item.langText);
}
// you might cache them if other components added by the router also request them
// this.subjects = {};
// this.ids = []
});
}
}
<dico [text]="dicoService.getDico('someId') | async"></dico>
ngAfterViewInit() {
this.dicoService.sendRequest();
}
Related
Imagine a case where an editor adds a “Latest Products” component to a page using dynamic zone: they add a title, a summary, and then the latest products will automatically be fetched to be available in the response. How can I add this data to the response of the component?
I know we can override the response for content types using a custom controller, but I can't find anything for how to modify the response for a component.
Maybe there's an alternative approach I haven't thought of, but coming from a Drupal preprocess-everything background this is all I can think of.
Any help appreciated!
I'm sure this isn't the best way, but I created a service for components that can be used in the content type controller to modify the response. Any improvements appreciated!
/api/custom-page/controllers/custom-page.js
'use strict';
/**
* custom-page controller
*/
const { createCoreController } = require('#strapi/strapi').factories;
module.exports = createCoreController('api::custom-page.custom-page', ({ strapi }) => ({
async find(ctx) {
const componentService = strapi.service('api::components.components');
let { data, meta } = await super.find(ctx);
data = await Promise.all(data.map(async (entry, index) => {
if(entry.attributes.sections){
await Promise.all(entry.attributes.sections.map(async (section, index) => {
const component = await componentService.getComponent(section);
entry.attributes.sections[index] = component;
}));
}
return entry;
}));
return { data, meta };
},
}));
/components/services/components.js
'use strict';
/**
* components service.
*/
module.exports = () => ({
getComponent: async (input) => {
// Latest products
if(input.__component === 'sections.latest-products'){
input.products = 'customdatahere';
}
return input;
}
});
The issue appears to happen when I post the link on platforms like Discord and Slack, where then to produce a URL preview they send a request to the link. The link which in this case follows this structure (normal format) www.domain.com/ctg/[...ids].
Within [...ids] I either pass one of two ids for the same object, the object has the following structure:
type Catalogue {
id: ID!
edit_id: String!
user_id: String!
title: String
...
}
The first id I could pass into [...ids] would be Catalogue.id
The second id I could pass into [...ids] would be Catalogue.edit_id
Whenever either of those inputs for [...ids] is passed as part of a request the following getStaticProps is ran:
export const getStaticProps: GetStaticProps = async ({ params }) => {
const { ids } = params;
let catalogue: CatalogueType | null = await fetchFullCatalogue(ids[0]);
return {
props: {
catalogue_prop: catalogue,
params,
},
};
};
with fetchFullCatalogue being:
export const fetchFullCatalogue = async (
id: string
): Promise<CatalogueType | null> => {
let catalogue: CatalogueType;
const fetchToUrl =
process.env.NODE_ENV === "development"
? "http://localhost:4000/graphql"
: process.env.BACKEND_URL + "/graphql";
// create a axios fetch request to the http://localhost:4000/graphql
const query = `
<...SOME FRAGMENTS LATER...>
fragment AllCatalogueFields on Catalogue {
id
edit_id
user_id
status
title
description
views
header_image_url
header_color
author
profile_picture_url
event_date
location
created
updated
labels {
...AllLabelFields
}
listings {
...AllListingFields
}
}
query Catalogues($id: ID, $edit_id: String) {
catalogues(id: $id, edit_id: $edit_id) {
...AllCatalogueFields
}
}`;
const config: AxiosRequestConfig = {
method: "post",
url: fetchToUrl,
headers: {
"Content-Type": "application/json",
},
data: JSON.stringify({
query,
variables: { id: id, edit_id: id },
}),
};
let response = await axios(config);
if (response.data.errors) return null;
catalogue = response.data.data.catalogues[0];
console.log("catalogue", catalogue);
return catalogue;
};
The request it is making is to the following API endpoint
Query: {
catalogues: async (
_: null,
args: { id: string; edit_id: string }
): Promise<Catalogue[]> => {
let catalogues: Catalogue[];
// when both id and edit_are passed
if (args.id && args.edit_id) {
catalogues = await getFullCatalogues(args.id, "id", true);
// the following convoluted request is the result of
// me responding to the fact that only the edit_id was working
if (catalogues.length === 0) {
catalogues = await getFullCatalogues(args.edit_id, "edit_id", true);
if (catalogues.length === 0) {
throw new UserInputError("No catalogues found");
}
} else {
catalogues = await getFullCatalogues(
catalogues[0].edit_id,
"edit_id",
true
);
}
console.log("catalogues", catalogues);
} else if (args.id) {
catalogues = await getFullCatalogues(args.id);
} else if (args.edit_id) {
catalogues = await getFullCatalogues(args.edit_id, "edit_id");
} else {
const res = await db.query(fullCatalogueQuery());
catalogues = res.rows;
}
return catalogues;
},
...
},
This results in the following output within the deployed logs:
The logs show the data when the Catalogue is first created which simultaneously navigates me to the URL of "normal format" with Catalogue.id which is interpreted as /_next/data/qOrdpdpcJ0p6rEbV8eEfm/ctg/dab212a0-826f-42fb-ba21-6ebb3c1350de.json. This contains the default data when Catalogue is first generated with Catalogue.title being "Untitled List"
Before sending both requests I changed the Catalogue.title to "asd".
Notice how the request with the Catalogue.edit_id which was sent as the "normal format" was interpreted as /ctg/ee0dc1d7-5458-4232-b208-1cbf529cbf4f?edit=true. This resulted in the correct data being returned with Catalogue.title being "asd".
Yet the following request with the Catalogue.id although being of the same "normal format" never provoked any logs.
(I have tried sending the request without the params ?edit=true and the same happens)
Another important detail is that the (faulty) request with the Catalogue.id produces the (faulty) URL preview much faster than the request with Catalogue.edit_id.
My best theory as to why this is happening is that the data of the URL with Catalogue.id is somehow stored/cached. This would happen as the Catalogue is first created. In turn it would result in the old stored Catalogue.id being returned instead of making the fetch again. Whereas the Catalogue.edit_id makes the fetch again.
Refrences:
Live site: https://www.kuoly.com/
Client: https://github.com/CakeCrusher/kuoly-client
Backend: https://github.com/CakeCrusher/kuoly-backend
Anything helps, I felt like ive tried everything under the sun, thanks in advance!
I learned that For my purposes I had to use getServerSideProps instead of getStaticProps
Internally in my server my entities are handled using the database's native fields where possible, so the entity's type is keyed with "dgraph.type". My graphql api does not need to know that the database is dgraph, but I don't want to have to change the field name on every resolver. Is it possible to create a Scalar or some other process so that I can send
{
"dgraph.type": "User",
uid: "0x01",
username: "JimNaysium",
}
and have the client receive
{
type: "User",
uid: "0x01",
username: "JimNaysium",
}
If you've found your way to this benighted question, the answer is: There is no built in way to do this. Apollo server's plugins all trigger too early or too late to help and the schema cannot rename properties. I solved this using the following code:
type KeyedList<T = any> = {[key: string]: any};
const getConvertTypesResolver = (
resolver: (parent: any, args: any, context: any, info: any) => Promise<any>
): any => {
return async (parent: any, args: any, context: any, info: any): Promise<any> => {
const result = await resolver(parent, args, context, info);
// Check for serializable data now so the processor does not choke on
// circular references later.
try {
JSON.stringify(result);
} catch (e) {
throw new Error("Resolver results must be serializable.");
}
const nestedKeysArrays: string[][] = [Object.keys(result)];
const path: KeyedList[] = [result];
// Iterate over every nested object in the result body.
while (nestedKeysArrays.length) {
let done = true;
// Iterate over every key of every object.
while (nestedKeysArrays[0].length) {
// Progressively destroy the keys arrays to prevent rework.
const key = nestedKeysArrays[0].shift();
if (!key) {
continue;
}
const current = path[0][key];
// If the current key is an object add it to the beginning of the
// lists and begin processing it now.
if (current && typeof current === "object") {
nestedKeysArrays.unshift(Object.keys(current));
path.unshift(current);
done = false;
break;
}
// Change the "dgraph.type" key to "type". This is where the real
// work is done. Everything else is just navigation.
if (key === "dgraph.type") {
path[0].type = path[0]["dgraph.type"];
delete path[0]["dgraph.type"];
}
}
if (done) {
// Remove the array of keys from the list of keys to be processed.
nestedKeysArrays.shift();
// Return to the previous object.
path.shift();
}
}
return result;
};
};
The function middlewares a resolver. I'm calling it before constructing ApolloServer on every resolver in the project, and using it's result in lieu of the original resolver.
The issue can be more succinctly solved if you don't avoid recursion, but then you would be using recursion. Good luck in your future endeavors.
I've been trying to research on how to add another root property of a GraphQL response but found nothing after 1 hour.
Normally, a GraphQL query looks like this:
{
myQuery() {
name
}
}
It responds with:
{
"data": {
"myQuery": []
}
}
I'm curious if I can add another root property in this response say "meta"
{
"data": {
"myQuery": []
},
"meta": {
"page": 1,
"count": 10,
"totalItems": 90
}
}
Is this possible, if not what's the best approach in tackling this with respect to GraphQL?
Thanks!
The apollo-server middleware can be configured with a number of configuration options, including a formatResponse function that allows you to modify the outgoing GraphQL response
const formatResponse = (response) => {
return {
meta
...response
}
}
app.use('/graphql', bodyParser.json(), graphqlExpress({
schema,
formatResponse,
}));
You could pass the req object down to your context, mutate it within your resolver(s) and then use the result inside formatResponse. Something like...
app.use('/graphql', bodyParser.json(), (req, res, next) => graphqlExpress({
schema,
formatResponse: (gqlResponse) => ({
...gqlResponse
meta: req.metadata
}),
})(req, res, next));
Typically, though, you would want to include the metadata as part of your actual schema and have it included with the data. That will also allow you to potentially request multiple queries and get the metadata for all of them.
There's any number of ways to do that, depending on how your data is structured, but here's an example:
type Query {
getFoos: QueryResponse
getBars: QueryResponse
}
type QueryResponse {
results: [Result]
meta: MetaData
}
union Result = Bar | Foo
You can add anything in the response as well... Please follow below code.
app.use('/graphql', bodyParser.json(), graphqlExpress(req => {
return {
schema: tpSchemaNew,
context: {
dbModel
},
formatError: err => {
if (err.originalError && err.originalError.error_message) {
err.message = err.originalError.error_message;
}
return err;
},
formatResponse : res => {
res['meta'] = 'Hey';
return res;
}
}
}))
Apollo Server-specific:
Just adding to the previous answers that formatResponse() has another useful argument, requestContext.
If you are interested in extracting values from that (for example, the context passed to the resolver), you can do the following. BEWARE HOWEVER, the context will likely contain sensitive data that is supposed to be private. You may be leaking authentication data and secrets if not careful.
const server = new ApolloServer({
schema,
formatResponse: (response, requestContext) => {
//return response
const userId = requestContext.context.user.id
response = Object.assign(response, {
extensions: {
meta: {
userId: userId
}
}
}
return response
},
})
The above will return something like this in the gql query response (note the extensions object):
{
data: {
user: {
firstName: 'Hello',
lastName: 'World'
}
},
extensions: { // <= in Typescript, there is no `meta` in GraphQLResponse, but you can use extensions
meta: {
userId: 1234 //<= data from the context
}
}
}
The full list of properties available in requestContext:
at node_modules/apollo-server-types/src/index.ts>GraphQLRequestContext
export interface GraphQLRequestContext<TContext = Record<string, any>> {
readonly request: GraphQLRequest;
readonly response?: GraphQLResponse;
readonly context: TContext;
readonly cache: KeyValueCache;
// This will be replaced with the `operationID`.
readonly queryHash?: string;
readonly document?: DocumentNode;
readonly source?: string;
// `operationName` is set based on the operation AST, so it is defined even if
// no `request.operationName` was passed in. It will be set to `null` for an
// anonymous operation, or if `requestName.operationName` was passed in but
// doesn't resolve to an operation in the document.
readonly operationName?: string | null;
readonly operation?: OperationDefinitionNode;
/**
* Unformatted errors which have occurred during the request. Note that these
* are present earlier in the request pipeline and differ from **formatted**
* errors which are the result of running the user-configurable `formatError`
* transformation function over specific errors.
*/
readonly errors?: ReadonlyArray<GraphQLError>;
readonly metrics?: GraphQLRequestMetrics;
debug?: boolean;
}
Not sure is there any way to set default request headers in rxjs like we do with axios js as-
axios.defaults.headers.common['Authorization'] = 'c7b9392955ce63b38cf0901b7e523efbf7613001526117c79376122b7be2a9519d49c5ff5de1e217db93beae2f2033e9';
Here is my epic code where i want to set request headers -
export default function epicFetchProducts(action$, store) {
return action$.ofType(FETCH_PRODUCTS_REQUEST)
.mergeMap(action =>
ajax.get(`http://localhost/products?${action.q}`)
.map(response => doFetchProductsFulfilled(response))
);
}
Please help.
It's not possible to set default headers for all ajax requests using RxJS's ajax utilities.
You can however provide headers in each call, or create your own simple wrapper that provides them by default.
utils/ajax.js
const defaultHeaders = {
Authorization: 'c7b9392955ce63b38cf090...etc'
};
export const get = (url, headers) =>
ajax.get(url, Object.assign({}, defaultHeaders, headers));
my-example.js
import * as ajax from './utils/ajax';
// Usage is the same, but now with defaults
ajax.get(`http://localhost/products?${action.q}`;)
I'm using redux-observable but this applies to rxjs; maybe the next answer its too over-engineered, but I needed to get dinamically the headers depending of certain factors, without affecting the unit testing (something decoupled from my epics too), and without changing the sintax of ajax.get/ajax.post etc, this is what I found:
ES6 has proxies support, and after reading this and improving the solution here, I'm using a High Order Function to create a Proxy in the original rxjs/ajax object, and return the proxified object; below is my code:
Note: I'm using typescript, but you can port it to plain ES6.
AjaxUtils.ts
export interface AjaxGetHeadersFn {
(): Object;
}
// the function names we will proxy
const getHeadersPos = (ajaxMethod: string): number => {
switch (ajaxMethod) {
case 'get':
case 'getJSON':
case 'delete':
return 1;
case 'patch':
case 'post':
case 'put':
return 2;
default:
return -1;
}
};
export const ajaxProxy = (getHeadersFn: AjaxGetHeadersFn) =>
<TObject extends object>(obj: TObject): TObject => {
return new Proxy(obj, {
get(target: TObject, propKey: PropertyKey) {
const origProp = target[propKey];
const headersPos = getHeadersPos(propKey as string);
if (headersPos === -1 || typeof origProp !== 'function') {
return origProp;
}
return function (...args: Array<object>) {
args[headersPos] = { ...args[headersPos], ...getHeadersFn() };
// #ts-ignore
return origProp.apply(this, args);
};
}
});
};
You use it this way:
ConfigureAjax.ts
import { ajax as Ajax } from 'rxjs/ajax'; // you rename it
// this is the function to get the headers dynamically
// anything, a function, a service etc.
const getHeadersFn: AjaxGetHeadersFn = () => ({ 'Bearer': 'BLABLABLA' });
const ajax = ajaxProxy(getHeadersFn)(Ajax); // proxified object
export default ajax;
Anywhere in you application you import ajax from ConfigureAjax.ts and use it as normal.
If you are using redux-observable you configure epics this way (injecting ajax object as a dependency more info here):
ConfigureStore.ts
import ajax from './ConfigureAjax.ts'
const rootEpic = combineEpics(
fetchUserEpic
)({ ajax });
UserEpics.ts
// the same sintax ajax.getJSON, decoupled and
// under the covers with dynamically injected headers
const fetchUserEpic = (action$, state$, { ajax }) => action$.pipe(
ofType('FETCH_USER'),
mergeMap(({ payload }) => ajax.getJSON(`/api/users/${payload}`).pipe(
map(response => ({
type: 'FETCH_USER_FULFILLED',
payload: response
}))
)
);
Hope it helps people looking for the same :D