How do I merge slices of state for my presentational component properly (ngrx/store)? - ngrx

I'm still trying to learn ngrx - the store is set-up and everything seems to work fine. The database I'm using is SQL so basically I'm "clongin" the tables and load them into the store but have problems joining the state in the end when selecting an employee.
The employee entity looks something like this:
export class Employee = {
id: number;
firstName: string;
lastName: string;
degreeId?: number;
degree: Degree;
}
export class Degree = {
id: number;
description: string;
}
Now in my component, I'd like to get the specific employee to display like this:
{
id: 1,
firstName: George,
lastName: Costanza,
degreeId: 2,
degree: {
id: 2,
description: 'College'
}
}
What I tried is to create a selector that merges these two entities:
export const getEmployeeWithAllData = createSelector(
getSelectedEmployee,
getRelationalData,
(employee, data) => {
const employeesDegree = degree[employee.degreeId]
employee.degree = employeesDegree
return employee;
}
);
This does seem to work if I don't use ngrx-store-freeze - so since I don't know if I am creating the selector correctly or if ngrx-store-freeze has a bug I'm asking this question.
Am I really mutating state when I do this?
If yes, how can I select a specific employee from my store with all the relational data that he has?
What I'm doing doesn't really feel right. In my actual application the employee has about 8 fields of relational data which I need to join...
Edit: I forgot to include the error ngrx-store-freeze throws:
ERROR TypeError: Cannot assign to read only property 'degree' of object '[object Object]'
at eval (employee.selector.ts:85)
at eval (store.es5.js:602)
at memoized (store.es5.js:539)
at defaultStateFn (store.es5.js:573)
at eval (store.es5.js:605)
at MapSubscriber.memoized [as project] (store.es5.js:539)
at MapSubscriber._next (map.js:79)
at MapSubscriber.Subscriber.next (Subscriber.js:95)
at MapSubscriber._next (map.js:85)
at MapSubscriber.Subscriber.next (Subscriber.js:95)

yes, you are trying to mutate the state. You can prevent this simply like bellow
export const getEmployeeWithAllData = createSelector(
getSelectedEmployee,
getRelationalData,
(employee, data) => {
const emp = JSON.parse(JSON.stringify(employee)
emp.degree = degree[employee.degreeId]
return emp;
}
)

Related

zod (or toZod): how to model "type" field in discriminated union

I have this type:
export interface ConnectorForModel {
_type: "connector.for.model",
connectorDefinitionId: string
}
I want to model is as a zod schema. Actually I am using toZod, like this:
export const ConnectorForModelZod: toZod<ConnectorForModel> = z.object({
_type: z.literal("connector.for.model"),
connectorDefinitionId: z.string()
});
And I get this type error:
Type 'ZodLiteral<"connector.for.model">' is not assignable to type 'never'.
Whats the right way to express this?
I think the quickest way to get this working is with ZodType:
import { z } from "zod";
export interface ConnectorForModel {
_type: "connector.for.model";
connectorDefinitionId: string;
}
export const ConnectorForModelZod: z.ZodType<ConnectorForModel> = z.object({
_type: z.literal("connector.for.model"),
connectorDefinitionId: z.string()
});
Aside: I tend to define my types from my zod schemas rather than the other way around. If you don't have control over the type that you're working with then the given approach is the way to go, but you could potentially avoid writing the same code twice using z.TypeOf on the zod schema
type ConnectorForModel = z.TypeOf<typeof ConnectorForModelZod>;
This type would be equivalent to your interface.

How to paginate results when filtering on left join table column

Let's define for example these entities:
#Entity()
export class Author {
#OneToMany(() => Book, book => book.author)
books = new Collection<Book>(this);
}
#Entity()
export class Book {
#Property()
name: string;
#ManyToOne()
author: Author;
}
const authors = await authorRepository.find({ books: {name: 'Test'} }, {
limit: 10
});
As you can see i want to select all authors that have books with name 'Test', but this will generate the following query:
select `e0`.* from `author` as `e0`
left join `book` as `e1` on `e0`.`id` = `e1`.`author_id`
where `e1`.`name` = 'Test' limit 10
The problem is when i have more than 2 authors and each of them has more than 10 books with name 'Test', this query will return only the first author because of the limit clause.
I am not sure if this is a bug in the ORM or it's a expected behavior.
One way to fix that is to select all rows without the limit clause and do the pagination in memory like they are doing in hibernate, but i am not sure how much memory will be used with very large tables and this might block the event loop in NodeJS while processing them.
You could fallback to query builder here to apply group by clause:
const qb = await authorRepository.createQueryBuilder('a');
qb.select('a.*').where({ books: { name: 'Test' } }).limit(10).groupBy('a.id');
const authors = await qb.getResult();
Will think about how to support this use case directly via EM/repository API.

Sequelize Create a new instance and add association

I've looked at the documentation and read through some issues related to this on the sequelize github repo but so far haven't found any solutions to this, IMHO, simple operation.
I have two models: Users and Cards. They are associated with a one-to-one relationship.
Orders.create({
date: req.body.date,
...
card: req.body.card //FK to a card ID
})
req.body.card is the id of the card. When I get the created object back though card is null.
I also tried creating the order first and then adding an association after but haven't had any luck with that either. I found some stuff about a set function that would set the association but I only saw examples of it with hasMany relationships.
I'm not trying to do anything fancy - just want to set the card id in the card field on an Order.
There are a few issues with your model definition:
Orders.hasOne(models.Cards, { foreignKey: 'id', as: 'card', });
In your definition, the foreign key will be placed on the cards table.
Naming the foreignKey as id will collide with the models own id field.
Try the following:
const Card = sequelize.define('card', {
name: { type: Sequelize.STRING }
});
const Order = sequelize.define('order', {
name: { type: Sequelize.STRING }
});
Order.hasOne(Card, { foreignKey: 'order_id' }); // if foreignKey is not included, will default to orderId
const order = await Order.create({
name: 'first order'
})
const card = await Card.create({
name: 'first card',
order_id: order.id
})

Denormalize with normalizr

I am trying to denormalize some data from the redux state, but I can't get it to work. I have tried putting together the denormalize function in all ways I can think. It is a guessing game and nothing have worked so far.
First I normalize the product data and put it into the state.
The entities are being put in state.data.products, including the products.
state.data: {
products: {
products: {
9: {
id: 9
attributes: [ 10, 45, 23 ],
things: [23, 55, 77]
},
11 //etc
}
attributes: {
10: {
id: 10
},
45: //etc
},
things: { //etc
}
}
So there is one property "products" that includes all entities, and there is another "products" for the "products" entity.
The normalizing works without problem:
normalize(data, [productSchema]);
The "productSchema" is defined like this:
productSchema = new schema.Entity('products', {
attributes: [attributeSchema],
things: [thingsSchema],
//+ other stuff
});
Now I want to denormalize a product, for example 9.
I use "connect" in a view component to call this function to retrieve the data from state and denormalize it:
export const denormalizeProduct = (state, productId) => {
const entities = state.data.products;
return denormalize(
productId,
[productSchema],
entities
);
}
If I call denormalizeProductById(state, 9) , I only get 9 back.
(similar to this question , but I couldn't find a solution from it, and it mixes together two examples)
If I put the "productSchema" without the array:
return denormalize(
productId,
productSchema,
entities
);
I get the error: TypeError: entities[schemaKey] is undefined.
Everything I have tried results in either "9" or the above error.
Edit:
found the error - added it below
I found the error - one of my schemas was wrong: I had created new schema.Entity('shippingZones') but in the reducer I added it to the state with a different name:
shipping_zones: action.payload.shippingZones
So it was looking for "shippingZones" while there was only "shipping_zones" I guess. So I changed it to new schema.Entity('shipping_zones') and shipping_zones: action.payload.shipping_zones
It's a pity that the message TypeError: entities[schemaKey] is undefined couldn't have specified "shippingZones" , then I could have saved a few days time.
So this code worked in the end:
const entities = state.data.products;
return denormalize(
productId,
productSchema,
entities
);

How to combine data model types with document ids?

I'm working with Firestore and Typescript.
For the data models I have types definitions. For example User could be this:
interface User {
name: string;
age: number;
}
The users are stored in the database in the users collection under a unique name/id.
In Firebase when you query a collection, the ids of the documents are available on the document reference, and do not come with the data. In a common use-case for front-end, you want to retrieve an array of records with their ids, because you probably want to interact with them and need to identify each.
So I made a query similar to the code below, where the id is merged into the resulting array:
async function getUsers(): Promise<any[]> {
const query = db.collection("users")
const snapshot = await query.get();
const results = snapshot.docs.map(doc => {
return { ...doc.data(), id: doc.id };
});
}
Now the problem is, that I have a User type, but it can't be used here because it does not contain an id field.
A naïve solution could be to create a new type:
interface UserWithId extends User {
id: string
}
And write the function like:
async function getUsers(): Promise<UserWithId[]> {}
But this doesn't feel right to me, because you would have to potentially do this for many types.
A better solution I think would be to create a generic type:
type DatabaseRecord<T> = {
id: string,
data: T
}
Thus keeping data and ids separate in the returning results:
const results = snapshot.docs.map(doc => {
return { data: doc.data(), id: doc.id };
});
... and use the function signature:
async function getUsers(): Promise<DatabaseRecord<User>[]> {}
I would favour the second over the first solution, because creating new types for each case feels silly. But I am still not sure if that is the best approach.
This seems like such a common scenario but I didn't manage to find any documentation on this. I have seen developers simply write the id in the model data, essentially duplicating the document name in its data, but that to me seems like a big mistake.
I can imagine that if you don't use Typescript (of Flow) that you just don't care about the resulting structure and simply merge the id with the data, but this is one of the reasons I really love using type annotation in JS. It forces you think more about your data and you end up writing cleaner code.

Resources