npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

pb-option-builder

v0.1.4

Published

Option builder for the official PocketBase SDK that also helps with typing the response

Downloads

4

Readme

PocketBase Type-safe Option Builder

Option builder for PocketBase JavaScript SDK, that also helps with typing the response.

This is how you would normally write options for the PocketBase SDK:

{
    expand: 'comments_via_post,tags',
    fields: 'id,title,expand.comments_via_post.user,expand.comments_via_post.message,expand.tags.id,expand.tags.name'
}

Writing options manually like this is very error-prone, and makes the code very hard to read/maintain.

This option builder allows you to write it like this instead:

{
    key: 'posts',
    fields: ['id', 'title'],
    expand: [
        { key: 'comments_via_post', fields: ['user', 'message'] },
        { key: 'tags', fields: ['id', 'name'] }
    ]
}

It comes with autocomplete for key, fields, expand and the basic sort options, and also provides you a way to type the response.

Installation

npm install pb-option-builder

Usage

Defining schema and relations

Below is an example of how you would define the schema for this in the PocketBase docs.

interface PocketbaseCollection {
    id: string
    created: string
    updated: string
}

interface User extends PocketBaseCollection {
    name: string
}

interface Post extends PocketBaseCollection {
    title: string
    tags: Array<string>
}

interface Tag extends PocketBaseCollection {
    name: string
}

interface Comment extends PocketBaseCollection {
    post: string
    user: string
    message: string
}

// You need to use "type" instead of "interface" for these as interfaces are "mutable"
// TypeScript needs to know the keys are guaranteed to be of type "string"
type Schema = {
    // Table names as keys
    users: User
    posts: Post
    tags: Tag
    comments: Comment
}

type Relations = {
    // column names as keys
    user: User
    post: Post // if you have view collections, use union like "post: Post | ViewCollectionName"

    // if the relation is one-to-many or many-to-many, use Array<>
    tags: Array<Tag>

    // back-relations
    posts_via_tags: Array<Post>
    // OR
    "posts(tags)": Array<Post> // if you're using PB < 0.22.0
    // the old syntax will be supported until PB hard-deprecates it or it gets too annoying to maintain for whatever reason


    // Add "?" modifier to annotate optional relation fields
    comments_via_post?: Array<Comment> // i.e. post might not have any comments
    comments_via_user?: Array<Comment> // i.e. user might not have any comments
}

Initializing builder

import { initializeBuilder } from 'pb-option-builder'

const optionBuilder = initializeBuilder<Schema, Relations>()

Building query

const [optionsObj, typeObj] = optionBuilder({
    key: 'posts',
    // you can specify fields to be returned in the response
    fields: ['id', 'title', 'tags'],
    expand: [
        {
            key: 'tags'
            // returns all fields if not specified
        },
        {
            key: 'comments_via_post',
            // you can use :excerpt modifier on string fields
            fields: ["message:excerpt(20)"],
            // nesting "expand" is supported
            expand: [{ key: 'user', fields: ['name'] }]
        }
    ]
})

const result = await pb.collection('posts').getOne(optionsObj);

Typing response:

The second item in the returned array (typeObj in the example above) is an empty object type cast as the type of the response.
You can use it to type the response:

const result = await pb.collection('posts').getOne<typeof typeObj>(optionsObj);

Now result will be correctly typed as:

Pick<Post, "tags" | "id" | "title"> & {
    expand: {
        tags: Array<Tag>
        comments_via_post?: (Pick<Comment, "message"> & {
            expand: {
                user: Pick<User, "name">
            }
        })[]
    }
}

It's a bit hacky and not very pretty, but does the job.

Parameter type for the option builder:

{
    // Table name as defined in "Schema"
    key: keyof Schema 

    // Array of fields you want to be returned in the response 
    fields?: Array<keyof Schema[key]> // defaults to all fields if not specified
    
    // Array of relations you want to be returned in the response
    expand?: Array<ExpandItem>

    // These will be passed to the SDK as is
    sort?: string
    filter?: string
    requestKey?: string
}

ExpandItem {
    // Relation name as defined in "Relations"
    key: keyof Relations

    fields?: // same as above
    expand?: // same as above
}

Fields

You might run into a situation where you have a component that requires a specific set of fields to be passed to it, and it makes sense to fetch the item directly in one route, but in another, it makes sense to do so through expand.

Because of the way the parameter for the option builder is structured, the fields array is portable.
You can define the fields in one place, and use it either at the top level, or in the expand option as is .

Example:

// CommentBlock.svelte
export const commentFields = ["user", "message"] satisfies Array<keyof Comment>
// [comment]/+page.ts
import { commentFields } from '$lib/CommentBlock.svelte'

const [optionsObj, typeObj] = optionBuilder({
    key: "comments",
    // you can use the imported fields here
    fields: commentFields
})
// [post]/+page.ts
import { commentFields } from '$lib/CommentBlock.svelte'

const [optionsObj, typeObj] = optionBuilder({
    key: "posts",
    fields: ["id", "title", "tags"],
    expand: [
        {
            key: comments_via_post,
            // or here. No need to alter the imported fields
            fields: commentFields
        }
    ]
})

Handling of optional relation fields

Let's say you want to get a post with its comments using expand.
When the post doesn't have any comments, the SDK (or PocketBase itself rather) returns:

{
    id: "1",
    title: "Lorem ipsum",
    tags: ["lorem", "ipsum"],
    created: "2024-01-01T00:00:00.000Z",
    updated: "2024-01-01T00:00:00.000Z"
}

The response will not have

{
    expand: {
        comments_via_post: []
    }
}
// or not even { expand: undefined } for that matter

So you will get runtime error if you try to access post.expand[comments_via_post] on a post with no comments.

To handle cases like this, the option builder will add ? modifier to the expand field itself if all the specified expands are for optional relation fields.

Post & {
    expand?: {
        comments_via_post: Comment[]
    }
}
// or with multiple optional relations
Post & {
    expand?: {
        foo?: Foo
        comments_via_post?: Comment[]
    }
}

If you expand it along with fields that are not optional like tag, expand will be there regardless of whether the post has comments or not.

So the respose will be typed as:

Post & {
    expand: {
        tag: Array<Tag>
        comments_via_post?: Comment[]
    }
}

Caveat:

In order for back-relations to work, you need to have the forward-relations defined as well.

type Relations = {
    // This alone is not enough
    comments_via_post: Array<Comment>

    // You need to have this defined as well
    post: Post
}

const [optionsObj, typeObj] = optionBuilder({
    key: "posts",
    expand: [
        {
            // Without "post: Post", TS will complain and you won't get autocomplete or typesafety
            key: "comments_via_post",
        }
    ]
})

Why not just integrate this into the SDK?

  • This way, you can start using this in existing projects without having to change anything. I think most of the time, you don't need to pass in any options to the SDK, so installing a new custom SDK for a very few instances where you need to seems like an overkill.
  • There are many functionalities of the official SDK that I don't use or understand fully, and I don't want to maintain a fork of it just for this.