Working with a message having repeated field - javascript

My Proto file looks like this -
message Todos {
repeated Todo todos = 1;
}
message Todo {
int32 id = 1;
string name = 2;
bool done = 3;
}
It comes works fine when I send {todos: [...]} from the server, but gets an empty object {} when directly sending an array.
Server
getAll(_, callback) {
console.log(todos);
return callback(null, { todos });
}
Client
client.getAll({}, function (err, res) {
if (err) {
return console.log(err);
}
console.log('todos: ');
return console.log(res);
});
Versions -
#grpc/proto-loader - ^0.1.0
grpc - ^1.13.0

In my case I was trying to return an array and it seems you always have to return an object....
hero.proto
syntax = "proto3";
package hero;
service HeroService {
rpc GetHeroById(HeroById) returns (Hero) {}
rpc ListHeroesById(HeroById) returns (HeroList) {}
}
message HeroById {
int32 id = 1;
}
message Hero {
int32 id = 1;
string name = 2;
}
message HeroList {
repeated Hero heroes = 1;
}
hero.controller.ts
#GrpcMethod('HeroService')
listHeroesById(data: HeroById, metadata: any): object {
const items = [
{ id: 1, name: 'John' },
{ id: 2, name: 'Doe' },
{ id: 3, name: 'Billy' },
];
// make sure you return an object, even if you want an array!
return { heroes: items.filter(({ id }) => id === data.id) };
}
Check out my example TypeScript project here:
https://github.com/kmturley/angular-nest-grpc

If I understand correctly, you are having problems if you send just the array todos, instead of an object containing that array. Sending just the array is simply an invalid use of the API. Protobuf services always send protobuf messages, so you have to pass an actual message object, and not a single field of that object.

If you use grpc.load then you can send back an array:
callback(null, todos);
If you use protoLoader.loadSync and grpc.loadPackageDefinition, then you need to send back:
callback(null, { todos: todos });

Related

Assign dynamically nested array of classes

I need to be able to receive data from an external API and map it dynamically to classes. When the data is plain object, a simple Object.assign do the job, but when there's nested objects you need to call Object.assign to all nested objects.
The approach which I used was to create a recursive function, but I stumble in this case where there's a nested array of objects.
Classes
class Organization {
id = 'org1';
admin = new User();
users: User[] = [];
}
class User {
id = 'user1';
name = 'name';
account = new Account();
getFullName() {
return `${this.name} surname`;
}
}
class Account {
id = 'account1';
money = 10;
calculate() {
return 10 * 2;
}
}
Function to initialize a class
function create(instance: object, data: any) {
for (const [key, value] of Object.entries(instance)) {
if (Array.isArray(value)) {
for (const element of data[key]) {
// get the type of the element in array dynamically
const newElement = new User();
create(newElement, element)
value.push(newElement);
}
} else if (typeof value === 'object') {
create(value, data[key]);
}
Object.assign(value, data);
}
}
const orgWithError = Object.assign(new Organization(), { admin: { id: 'admin-external' }});
console.log(orgWithError.admin.getFullName()); // orgWithError.admin.getFullName is not a function
const org = new Organization();
const data = { id: 'org2', admin: { id: 'admin2' }, users: [ { id: 'user-inside' }]}
create(org, data);
// this case works because I manually initialize the user in the create function
// but I need this function to be generic to any class
console.log(org.users[0].getFullName()); // "name surname"
Initially I was trying to first scan the classes and map it and then do the assign, but the problem with the array of object would happen anyway I think.
As far as I understand from your code, what you basically want to do is, given an object, determine, what class it is supposed to represent: Organization, Account or User.
So you need a way to distinguish between different kinds of objects in some way. One option may be to add a type field to the API response, but this will only work if you have access to the API code, which you apparently don't. Another option would be to check if an object has some fields that are unique to the class it represents, like admin for Organization or account for User. But it seems like your API response doesn't always contain all the fields that the class does, so this might also not work.
So why do you need this distinction in the first place? It seems like the only kind of array that your API may send is array of users, so you could just stick to what you have now, anyway there are no other arrays that may show up.
Also a solution that I find more logical is not to depend on Object.assign to just assign all properties somehow by itself, but to do it manually, maybe create a factory function, like I did in the code below. That approach gives you more control, also you can perform some validation in these factory methods, in case you will need it
class Organization {
id = 'org1';
admin = new User();
users: User[] = [];
static fromApiResponse(data: any) {
const org = new Organization()
if(data.id) org.id = data.id
if(data.admin) org.admin = User.fromApiResponse(data.admin)
if(data.users) {
this.users = org.users.map(user => User.fromApiResponse(user))
}
return org
}
}
class User {
id = 'user1';
name = 'name';
account = new Account();
getFullName() {
return `${this.name} surname`;
}
static fromApiResponse(data: any) {
const user = new User()
if(data.id) user.id = data.id
if(data.name) user.name = data.name
if(data.account)
user.account = Account.fromApiResponse(data.account)
return user
}
}
class Account {
id = 'account1';
money = 10;
calculate() {
return 10 * 2;
}
static fromApiResponse(data: any) {
const acc = new Account()
if(data.id) acc.id = data.id
if(data.money) acc.money = data.money
return acc
}
}
const data = { id: 'org2', admin: { id: 'admin2' }, users: [ { id: 'user-inside' }]}
const organization = Organization.fromApiResponse(data)
I can't conceive of a way to do this generically without any configuration. But I can come up with a way to do this using a configuration object that looks like this:
{
org: { _ctor: Organization, admin: 'usr', users: '[usr]' },
usr: { _ctor: User, account: 'acct' },
acct: { _ctor: Account }
}
and a pointer to the root node, 'org'.
The keys of this object are simple handles for your type/subtypes. Each one is mapped to an object that has a _ctor property pointing to a constructor function, and a collection of other properties that are the names of members of your object and matching properties of your input. Those then are references to other handles. For an array, the handle is [surrounded by square brackets].
Here's an implementation of this idea:
const create = (root, config) => (data, {_ctor, ...keys} = config [root]) =>
Object.assign (new _ctor (), Object .fromEntries (Object .entries (data) .map (
([k, v]) =>
k in keys
? [k, /^\[.*\]$/ .test (keys [k])
? v .map (o => create (keys [k] .slice (1, -1), config) (o))
: create (keys [k], config) (v)
]
: [k, v]
)))
class Organization {
constructor () { this.id = 'org1'; this.admin = new User(); this.users = [] }
}
class User {
constructor () { this.id = 'user1'; this.name = 'name'; this.account = new Account() }
getFullName () { return `${this.name} surname`}
}
class Account {
constructor () { this.id = 'account1'; this.money = 10 }
calculate () { return 10 * 2 }
}
const createOrganization = create ('org', {
org: { _ctor: Organization, admin: 'usr', users: '[usr]' },
usr: { _ctor: User, account: 'acct' },
acct: { _ctor: Account }
})
const orgWithoutError = createOrganization ({ admin: { id: 'admin-external' }});
console .log (orgWithoutError .admin .getFullName ()) // has the right properties
const data = { id: 'org2', admin: { id: 'admin2' }, users: [ { id: 'user-inside' }]}
const org = createOrganization (data)
console .log (org .users [0] .getFullName ()) // has the right properties
console .log ([
org .constructor .name,
org .admin .constructor.name, // has the correct hierarchy
org .users [0]. account. constructor .name
] .join (', '))
console .log (org) // entire object is correct
.as-console-wrapper {min-height: 100% !important; top: 0}
The main function, create, receives the name of the root node and such a configuration object. It returns a function which takes a plain JS object and hydrates it into your Object structure. Note that it doesn't require you to pre-construct the objects as does your attempt. All the calling of constructors is done internally to the function.
I'm not much of a Typescript user, and I don't have a clue about how to type such a function, or whether TS is even capable of doing so. (I think there's a reasonable chance that it is not.)
There are many ways that this might be expanded, if needed. We might want to allow for property names that vary between your input structure and the object member name, or we might want to allow other collection types besides arrays. If so, we probably would need a somewhat more sophisticated configuration structure, perhaps something like this:
{
org: { _ctor: Organization, admin: {type: 'usr'}, users: {type: Array, itemType: 'usr'} },
usr: { _ctor: User, account: {type: 'acct', renameTo: 'clientAcct'} },
acct: { _ctor: Account }
}
But that's for another day.
It's not clear whether this approach even comes close to meeting your needs, but it was an interesting problem to consider.

Apollo GraphQL updateQuery to typePolicy

I am beating my head against a wall. I have updated to Apollo 3, and cannot figure out how to migrate an updateQuery to a typePolicy. I am doing basic continuation based pagination, and this is how I used to merged the results of fetchMore:
await fetchMore({
query: MessagesByThreadIDQuery,
variables: {
threadId: threadId,
limit: Configuration.MessagePageSize,
continuation: token
},
updateQuery: (prev, curr) => {
// Extract our updated message page.
const last = prev.messagesByThreadId.messages ?? []
const next = curr.fetchMoreResult?.messagesByThreadId.messages ?? []
return {
messagesByThreadId: {
__typename: 'MessagesContinuation',
messages: [...last, ...next],
continuation: curr.fetchMoreResult?.messagesByThreadId.continuation
}
}
}
I have made an attempt to write the merge typePolicy myself, but it just continually loads and throws errors about duplicate identifiers in the Apollo cache. Here is what my typePolicy looks like for my query.
typePolicies: {
Query: {
fields: {
messagesByThreadId: {
keyArgs: false,
merge: (existing, incoming, args): IMessagesContinuation => {
const typedExisting: IMessagesContinuation | undefined = existing
const typedIncoming: IMessagesContinuation | undefined = incoming
const existingMessages = (typedExisting?.messages ?? [])
const incomingMessages = (typedIncoming?.messages ?? [])
const result = existing ? {
__typename: 'MessageContinuation',
messages: [...existingMessages, ...incomingMessages],
continuation: typedIncoming?.continuation
} : incoming
return result
}
}
}
}
}
So I was able to solve my use-case. It seems way harder than it really needs to be. I essentially have to attempt to locate existing items matching the incoming and overwrite them, as well as add any new items that don't yet exist in the cache.
I also have to only apply this logic if a continuation token was provided, because if it's null or undefined, I should just use the incoming value because that indicates that we are doing an initial load.
My document is shaped like this:
{
"items": [{ id: string, ...others }],
"continuation": "some_token_value"
}
I created a generic type policy that I can use for all my documents that have a similar shape. It allows me to specify the name of the items property, what the key args are that I want to cache on, and the name of the graphql type.
export function ContinuationPolicy(keyArgs: Array<string>, itemPropertyKey: string, typeName: string) {
return {
keyArgs,
merge(existing: any, incoming: any, args: any) {
if (!!existing && !!args.args?.continuation) {
const existingItems = (existing ? existing[itemPropertyKey] : [])
const incomingItems = (incoming ? incoming[itemPropertyKey] : [])
let items: Array<any> = [...existingItems]
for (let i = 0; i < incomingItems.length; i++) {
const current = incomingItems[i] as any
const found = items.findIndex(m => m.__ref === current.__ref)
if (found > -1) {
items[found] === current
} else {
items = [...items, current]
}
}
// This new data is a continuation of the last data.
return {
__typename: typeName,
[itemPropertyKey]: items,
continuation: incoming.continuation
}
} else {
// When we have no existing data in the cache, we'll just use the incoming data.
return incoming
}
}
}
}

Data getting added suspiciously after Firebase call

I am struggling with a uncertain issue and not able to figure out what can be the root cause.
I am calling Firebase for data using the below code:
public getAllObject(filters: Filter[]): Observable<T[]> {
return this.afs
.collection('somecollection')
.snapshotChanges()
.pipe(
take(1),
map((changes) => {
return changes.map((a) => {
console.log(a.payload.doc.data()) ==============> displays everything is alright
const data: any = a.payload.doc.data() as T;
const code = a.payload.doc.id;
return { code, ...data } as T;
});
})
);
}
and I am consuming the above service mentioned below:
this.service.getAllObject(this.service.getFilters()).subscribe(
(entities) => {
console.log(entities);==============> display's array and things are wrong.
},
(error) => {
console.error(error);
}
)
Problem description
When I call the above API, I get an array of below objects. Problem is with the stores attribute. As we move forward in the array the stores attribute contains values from pre elements. This phenomenon is HAPPENING ON CLIENT SIDE only.
I was wondering if I am using attribute name 'stores' which is any kind of reserved keyword which is causing this to happen. Or i am using rxjs in wrong way.
Current results
{
code: 123,
stores: { abc }
},
{
code: 345,
stores: { def, abc }
},
{
code: 678,
stores: { xyz, def, abc }
},
Expected results
getAllObject console.log displays the following which is correct
{
code: 123,
stores: { abc }
},
{
code: 345,
stores: { def }
},
{
code: 678,
stores: { xyz }
},
Current analysis
console.log(a.payload.doc.data()); ====> Showing correct
const data: any = a.payload.doc.data();
const code: string = a.payload.doc.id;
console.log({ code, ...data });
return { code, ...data } as T; =====> Showing INCORRECT and adding stores from earlier element to current one.

Concatenate Query Params as string - Angular- Typescript

I'm trying to concatenate all the query params string that I have into a one final query including all params but I keep getting this result :
_filter=0=((startDate=ge=2019-09-30T22:00:00.000Z);(endDate=le=2019-10-
03T22:00:00.000Z));1=
(category=eq=Warning,category=eq=Error);&_limit=50&_sort=asc
with the annoying 0=, 1=, 2=, ....
The expected result is to be like that :
_filter=((startDate=ge=2019-10-06T12:39:05.000Z;endDate=le=2019-10-
07T23:59:59.999Z);(category=eq=ALERT,category=eq=META))"
Here is my code:
generateQueryString(obj: any) {
let query = [];
if (obj.startDate && obj.endDate) {
query = query.concat(this.buildDateQueryString(obj.startDate, obj.endDate)
);
}
if (obj.category) {
query = query.concat(this.buildCategoryQueryString(obj.category));
}
return query;
}
Let us assume you want pass some data like
const userData = {
firstName: 'Arpit',
lastName: 'Patel'
}
You can pass this object into Query Params like this.
this.router.navigate(['/dashboard/details'], { queryParams: { user: JSON.stringify(userData) } });
Then extract this Object in next component which is called as below.
this.route.queryParams.subscribe(params => {
const user= JSON.parse(params.user);
console.log(user);
});
This will result in JSON object. You can access it directly.

cloud firestore only add object if its name is unique

Object1 looks like this { category1: '', category2: '', description: '' }
Category1 has an array field for [category2] and a Name
My firebase contains 3 documents: 1 for object1 1 for Category1 and the last one is not relevant.
In my Ionic application I have made an input field where the user can paste an array of JSON objects. these JSON objects get converted to Object1 and added to the database.
My problem is when category1 is new it should be added to the firebase document for category1 but can't be a duplicate. Or when category2 is new it should update category1.
I think the solution is to make category1 unique Id its property name, so I can check with firebase security rules if it already exists when trying to add it.
Can someone explain to me how to set name as the unique Id of this document or does anyone know a better solution?
in my service:
private category1Collection: AngularFirestoreCollection<Category1>;
private category1s: Observable<Category1[]>;
constructor(db: AngularFirestore) {
this.category1Collection = db.collection<category1>('category1s');
var docRef = db.collection("category1s").doc("category1s");
this.category1s= this.category1Collection.snapshotChanges().pipe(
map(actions => {
return actions.map(a => {
const data = a.payload.doc.data();
const id = a.payload.doc.id;
return { id, ...data };
})
})
)
}
addItem(category1: Category1) {
return this.category1Collection.add(category1);
}
I fixed it:
changed code in the service to:
addItem(category1: Category1) {
return this.category1Collection.doc(category1.Naam).set(category1);
}
and this code in the page.ts:
this.category1Service.getItem(HF.Naam).subscribe(data => {
if (data != null) {
console.log(data)
HF.category2s = data.category2s;
if (!this.containsCategory2(element.category2, HF.category2s)) {
HF.category2s.push(element.category2);
this.category1Service.addItem(HF);
}
} else {
this.category1Service.addItem(HF);
}
});
containsCategory2(category2: string, list: string[]) {
var i;
for (i = 0; i < list.length; i++) {
if (list[i] === category2) {
return true;
}
}
return false;
}

Categories