js-sdk icon indicating copy to clipboard operation
js-sdk copied to clipboard

Recommended way of typing records?

Open mnorlin opened this issue 1 year ago • 7 comments

What is the best practice of getting typed fields for my records? I have tried a few different approaches, but I haven't found any straightforward way.

In listResult1, I'm limited by ListResult is not exported from the sdk In listResult2, the getList function is not generic.

import PocketBase, {ListResult, Record} from 'pocketbase'
const client = new PocketBase('http://127.0.0.1:8090');

interface MyItem extends Record {
    name: string,
    age: number
}

async function example() {
    // ListResult type is not exported
    const listResult1: ListResult<MyItem> = await client.records.getList("my-item", 1, 20)

    // getList() is not generic
    const listResult2 = await client.records.getList<MyItem>("my-item", 1, 20)

    // the age field resolves to "any" instead of "number"
    listResult1.items[0].age
    listResult2.items[0].age
}

mnorlin avatar Sep 21 '22 20:09 mnorlin

i was running into the same problem just now and i would also prefer if the getList (and all the other functions to manage records) would be generic.

As a workaround you can just cast in the meantime:

import PocketBase, { Record } from 'pocketbase'

interface BananaRecord extends Record {
  foo: string
  bar: string
}

const client = new PocketBase('http://127.0.0.1:8090')

async function getBananas(): Promise<BananaRecord[]> {
  const result = await client.records.getList('game')
  return result.items as BananaRecord[]
}

silberjan avatar Sep 21 '22 20:09 silberjan

@mnorlin Using type assertion as suggested by @silberjan is the easiest way to have typed records at the moment.

The generics suggestion is a nice one, although it will be a little hacky since the generic type cannot be used directly as constructor, but it is doable and I'll consider it for the next major release (v0.8.0) that will be shipped with the changes from https://github.com/pocketbase/pocketbase/issues/376.

ganigeorgiev avatar Sep 21 '22 20:09 ganigeorgiev

What about making the Records class generic, expose it and letting users instantiate it themselves with the record name as parameter. this way you would not need to touch the functions in SubCrudService.

Could look something like this:

import PocketBase, { Record, Records } from 'pocketbase'

interface BananaRecord extends Record {
  foo: string
  bar: string
}

const client = new PocketBase('http://127.0.0.1:8090')

const bananaRecordsClient = new Records<BananaRecord>('banana', client) // pass record name, and client here

async function getBananas(): Promise<BananaRecord[]> {
  const result = await bananaRecordsClient.getList() // no need to supply record name here anymore
  return result.items 
}

silberjan avatar Sep 21 '22 21:09 silberjan

It's a little too verbose in my opinion. I think the namespaced approach (aka. client.records.getList<T>) is more readable and easier to use.

ganigeorgiev avatar Sep 21 '22 21:09 ganigeorgiev

Pairing it with sql-ts would also safe you time and errors manually typing the db schema.

jsbrain avatar Sep 25 '22 12:09 jsbrain

I'm currently using casting but having generics would be great, e.g. getList<T>(). Or the client could be modified to allow passing in all of the collection names and corresponding types + allow us to override the generics when calling the methods.

bencun avatar Sep 27 '22 15:09 bencun

In my case to get types I do the following, very easy...

export interface CategorieProps {
   id: string;
   name: string;
}

export interface SectionProps {
   header: string;
   items: CategorieProps[];
}

Example doing a request....

const records = (await client.records.getFullList('sections', 200, {
    sort: 'created',
    expand: 'sections_categorie',
 })) as unknown as CategorieProps[];

Siumauricio avatar Oct 11 '22 00:10 Siumauricio

This will be a game changer for me

gevera avatar Oct 17 '22 17:10 gevera

The same issue, I resolved to copy and exporting the types in another file , needed it to provide generics to a react-query custom hook

the custom hook

import { useQuery, UseQueryOptions } from "@tanstack/react-query"
import { client } from "../../../pocketbase/config";
import {Record} from "pocketbase";
import { ListResult } from "../types/pb-types";

interface T {
  key: string[];
  filter?: string;
  expand?: string;
  rqOptions?: UseQueryOptions<ListResult<Record>,unknown,any,string[]>;
}

export const useCollection =({key,filter="",expand="",rqOptions={}}:T)=>{
    const fetcherFunction = async () => {
      return await client.records.getList(
        key[0],
        1,
        50,
        {
          filter: `${filter}`,
          expand:expand,
        }
      );
    };
  return useQuery< ListResult<Record>,unknown,ListResult<Record>,string[]>(key, fetcherFunction,rqOptions);
}

the external file with types

export declare abstract class BaseModel {
  id: string;
  created: string;
  updated: string;
  constructor(data?: { [key: string]: any });
  /**
   * Loads `data` into the current model.
   */
  load(data: { [key: string]: any }): void;
  /**
   * Returns whether the current loaded data represent a stored db record.
   */
  get isNew(): boolean;
  /**
   * Robust deep clone of a model.
   */
  clone(): BaseModel;
  /**
   * Exports all model properties as a new plain object.
   */
  export(): {
    [key: string]: any;
  };
}

export declare class ListResult<M extends BaseModel> {
  page: number;
  perPage: number;
  totalItems: number;
  totalPages: number;
  items: Array<M>;
  constructor(
    page: number,
    perPage: number,
    totalItems: number,
    totalPages: number,
    items: Array<M>
  );
}

and used it like this


import React from 'react'
import { useParams } from 'react-router-dom';
import { useCollection } from '../Shared/hooks/useCollection';
import { useLocation } from 'react-router-dom';
import { User,Admin,Record} from 'pocketbase';


interface TenantProps {
    user: User | Admin | null
}

export const Tenant: React.FC<TenantProps> = ({}) => {

const params = useParams();
const location = useLocation()

const tenantsQuery = useCollection({ key: ["tenants"],rqOptions:{
select: (data) => {
    return data.items.filter((item) => item.id === params.tenantId)
  }
} });
console.log(" params === ", tenantsQuery?.data)
return (
 <div className='w-full min-h-full flex-center-col'>
   {params.tenantId}
 </div>
);
}

I don't know if this is bad practice but it solves my problem

tigawanna avatar Oct 27 '22 03:10 tigawanna

Generics would be ideal for me. Are they planned?

profispojka avatar Oct 30 '22 09:10 profispojka

@profispojka Yes, actually I've pushed a v0.8.0-rc1 pre-release earlier today and you can give it a try - https://github.com/pocketbase/js-sdk/releases/tag/v0.8.0-rc1.

Please note that this works only with PocketBase v0.8+ APIs.

Since it is still a pre-release, to install the SDK you have to use the next tag:

npm install pocketbase@next --save

Documentation on the new SDK methods could be found in the temporary rc branch readme - https://github.com/pocketbase/js-sdk/blob/rc/README.md.

ganigeorgiev avatar Oct 30 '22 09:10 ganigeorgiev