#How do I make sure a repository method returns the right data ?

24 messages · Page 1 of 1 (latest)

oak cobalt
#

Lets say for example I have a class created for my domain entity User and repository interface for it , then I implemented the repository to match the interface.

class User {
  constructor(
    public id: string,
    public name: string,
    public email: string
  ) {}

  static create(data: userDto) {
    const { id, name, email } = data;
    return new User(id, name, email);
  }
}
interface IUserRepository {
  save(user: User): Promise<void>;
  findById(id: string): Promise<User>;
  findAll(): Promise<User[]>;
}
interface UserDto {
  id: string;
  name: string;
  email: string;
}```

```ts
import UserEntity from '../domain/entities/user'

class UserRepository implements IUserRepository {
  async findById(id: string): Promise<User> {
    const result: UserDto = await objectionModel.query().findById(id);
    
    if (!result) {
      throw new Error('User not found');
    }

    // do I put a zod schema here?

    const user: User = User.create(result);
    return user;
  }
}

How do I make sure the result returned by my orm model query matches the data needed for UserEntity.create ? Is this a good place to use runtime validation like a zod schema, or am I missing something else entirely here. For context, I'm learning typescript and proper splitting of code for proper backend architecture and still confused by some aspects.

gusty cobalt
#

the fact that result has the UserDto type should be enough to prove that it matches what UserEntity.create needs. unless findById returns a Promise<any> or something lame like that?

oak cobalt
#

maybe my example is wrong on the line you're mentioning, but what i mean is how does typescript know i didnt make a mistake in the query. making the result of type Userdto doesnt necessarily mean thats what the data is gonna look like right? (especially when i chain relation methods to objection models, i dont think typescript recognizes that) eg. objectionModel.query().findById(1).withRelations(["address", "orders"]

modern ivy
#

That depends on what is objectionModel and what does its querying and finding by id do.

oak cobalt
#

in my case it would return an instance of an orm model class (or undefined if it wasnt found)

#

i was thinking about this if i was also using library that lets me write raw sql like
result = await someSqlLibrary.query('SELECT * FROM Users WHERE id = ?', [id])

modern ivy
#

Are you using an ORM or is objectionModel something you wrote yourself?

random bone
#

sounds like they wrote their own repository interface/orm thingy. in which case you're basically trying to re-implement what popular ORMs like prisma/type-orm/etc already do to give you type safe queries, which is to take an existing schema you write, optionally validate that against your actual db (and propagate schema differences to your db if it isnt in sync), and then generate a type safe query interface that returns types also generated from that same schema

modern ivy
#

Yeah type safety doesn't magically come from thin air, you either:

  • Ensure database has the exact schema as your code.
  • Or validate things returned from database queries to be the shape you expect them to be.
#

Most ORMs do the former so there's no unnecessary validation to be done at runtime.

random bone
#

especially since the latter would be a significant performance overhead for applications with moderate to high db load

oak cobalt
#

just to clarify , you mean the database query ?

so far the orm i have doesn't infer types when i chain methods like .withRelated("attributes") so i have to do something like const result: typeOfDbModelInstance & { attributes: {name: string, value: string }[]} or
sometimes my orm doesn't integrate well with features like Postgres' Postgis, and I have to do queries like:

    static async getNearestStores(lat: number, long: number, radiusInMeters: number = 10000) {
        try {
            const point = DbStore.knex().raw("ST_SetSRID(ST_MakePoint(?, ?), 4326)::geography", [long, lat]);

            const stores = await DbStore.query()
                .select("*")
                .select(DbStore.knex().raw("ST_Y(location::geometry) AS lat"))
                .select(DbStore.knex().raw("ST_X(location::geometry) AS lng"))
                .select(
                    DbStore.knex().raw("cast(ST_Distance(location::geography, ?) as int) AS dist_meters", [point])
                )
                .whereRaw("ST_DWithin(location::geography, ?, ?)", [point, radiusInMeters])
                .orderByRaw("location <-> ?", [point]);

            return stores.map(({ lat, lng, dist_meters, location, ...rest }) => ({
                ...rest,
                location: { lat, lng, dist_meters },
            })) as StoreObject[];
        } catch (err) {
            console.log(err);
            return [];
        }
    }```
i guess this is where your points come in and this would be a good candidate for validation?
#

now that i think about it , i'll just write a test for reads and make sure the query works as intended, and i'll just validate inputs like form inputs. as for that function, it was a WIP and should probably be split up anyway so i can probably type it easier

gusty cobalt
#

if you hover over stores in your IDE with that code, what type do you see?

oak cobalt
#

hastebin is not working , let me try to find another website

gusty cobalt
#

how's that work at runtime? you're destructuring properties like lat, lng, etc that don't appear to exist on DbStore (unless maybe those are part of BaseModel?)

#

ah, maybe that example is before refactor or something... are they meant to come from the store_location property?

#

anywho, it seems like the ORM library you're using (objection?) has unsafe return types for those builder methods… if orderByRaw is claiming to return a Promise<DbStore[]> but not actually guaranteeing that it returns something with that shape at runtime then i guess that's the source of your concerns. IMO that's kind of scary and it seems like it's just asking for trouble—if the ORM doesn't know the shape of objects that it will return at runtime then it should be giving back something safe like unknown instead of lying to you

oak cobalt
gusty cobalt
oak cobalt
#

as much as i really like it so far i will definitely check out other libraries to see how its properly done, ive been meaning to test out slonik