Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

getOne method doesn't allow for select and $expand #31

Closed
shyakadavis opened this issue Jan 24, 2024 · 16 comments
Closed

getOne method doesn't allow for select and $expand #31

shyakadavis opened this issue Jan 24, 2024 · 16 comments

Comments

@shyakadavis
Copy link

Hi;

Not sure if I'm using the methods/functions in the wrong way, but basically, I'm trying to do something like this:

const project = (await event.locals.pb.collection('projects').getOne(
			event.params.project_id,
			createOptions({
				select: {
					$expand: {
						stage: { name: true },
						user: { id: true, collectionId: true, avatar: true, username: true }
					}
				}
			})
		));

but I get this error:

No overload matches this call.
  Overload 1 of 3, '(options: TypedRecordFullListQueryParams<GenericCollection, SelectWithExpand<GenericCollection>>): TypedRecordFullListQueryParams<...>', gave the following error.
    Type '{ $expand: { stage: { name: true; }; user: { id: true; collectionId: true; avatar: true; username: true; }; }; }' is not assignable to type 'SelectWithExpand<GenericCollection>'.
      Type '{ $expand: { stage: { name: true; }; user: { id: true; collectionId: true; avatar: true; username: true; }; }; }' is not assignable to type 'Select<GenericCollection>'.
        Property '$expand' is incompatible with index signature.
          Type '{ stage: { name: true; }; user: { id: true; collectionId: true; avatar: true; username: true; }; }' is not assignable to type 'boolean | undefined'.
Overload 2 of 3, '(options: TypedRecordListQueryParams<GenericCollection, SelectWithExpand<GenericCollection>>): TypedRecordListQueryParams<...>', gave the following error.
    Type '{ $expand: { stage: { name: true; }; user: { id: true; collectionId: true; avatar: true; username: true; }; }; }' is not assignable to type 'SelectWithExpand<GenericCollection>'.
      Type '{ $expand: { stage: { name: true; }; user: { id: true; collectionId: true; avatar: true; username: true; }; }; }' is not assignable to type 'Select<GenericCollection>'.
        Property '$expand' is incompatible with index signature.
          Type '{ stage: { name: true; }; user: { id: true; collectionId: true; avatar: true; username: true; }; }' is not assignable to type 'boolean | undefined'.
Overload 3 of 3, '(options: TypedRecordQueryParams<SelectWithExpand<GenericCollection>>): TypedRecordQueryParams<SelectWithExpand<GenericCollection>>', gave the following error.
    Type '{ $expand: { stage: { name: true; }; user: { id: true; collectionId: true; avatar: true; username: true; }; }; }' is not assignable to type 'SelectWithExpand<GenericCollection>'.
      Type '{ $expand: { stage: { name: true; }; user: { id: true; collectionId: true; avatar: true; username: true; }; }; }' is not assignable to type 'Select<GenericCollection>'.
        Property '$expand' is incompatible with index signature.
          Type '{ stage: { name: true; }; user: { id: true; collectionId: true; avatar: true; username: true; }; }' is not assignable to type 'boolean | undefined'.ts(2769)

I also tried doing this (sorry, but I couldn't find clear intructions from the readme.md about using the lib) but this, although TS doesn't complain, it just returns the surface-level fields, but nothing for the expanded ones.

const project = await event.locals.pb.collection('projects').getOne(event.params.project_id, {
			select: {
				$expand: {
					stage: { name: true },
					user: { id: true, collectionId: true, avatar: true, username: true }
				}
			}
		});

Don't know if I'm being clear enough, but anyone mind helping me out?

Thanks.

@david-plugge
Copy link
Owner

Hi @shyakadavis,
can you create a minimal reproduction that includes your schema? I don't think i can help you otherwise.
Maybe try using the beta version where you don't even need createOptions.

Take a look at it here: https://github.com/david-plugge/typed-pocketbase/tree/custom-client
The version is 0.1.0-pre.0

@shyakadavis
Copy link
Author

Hey, @david-plugge

I don't mind trying out the new version with the Client, but it seems it's broken-ish.

Doing

yields this error in several places:

Cannot find module 'typed-pocketbase' or its corresponding type declarations.ts(2307)

Vite/SvelteKit has a better description:

[plugin:vite:import-analysis] Failed to resolve entry for package "typed-pocketbase". The package may have incorrect main/module/exports specified in its package.json.

Are you sure it works fine and it's not something wrong on my part?

Thanks.

@david-plugge
Copy link
Owner

Hey, thanks for reporting the issue. I released a new version 0.1.0-pre.2 which should resolve the type issue. There are a few more additions aswell like support for realtime events which where missing before. Give it a try and let me know if there are still some issues. My goal ist to release 0.1.0 at the end of this week.

@shyakadavis
Copy link
Author

Hey, @david-plugge

Thanks for your continued work on the lib.

The new version works great overall, but there are some wrong/flawed cases. For example, take this;

(Note: If sharing my entire generated schema is simpler, please let me know) 🙂

I have a projects collection, with a user field, which in turn is a multiple relation to the users collection. Thus, I should expect the user field to be typed as string[] which is does splendidly.

The problem: When I do expand on this field when calling the project, it's typed as

expand?: {
    user?: {
        id: string;
        collectionId: string;
        avatar: string;
        username: string;
    } | undefined;

which breaks in my Svelte component where I'm iterating like so:

{#if data.project.expand && data.project.expand.user}
	<span>
		<dt>Founder(s)</dt>
		<dd class="flex flex-wrap -space-x-3">
			{#each data.project.expand.user as user}
				<Avatar.Root class={cn(`ring-background size-9 ring-2`)}>
					<Avatar.Image
						src={get_image_url({
							collection_id: user.collectionId,
							record_id: user.id,
							file_name: user.avatar,
							size: '50x50'
						})}
						alt={`@${user.username}`}
					/>
					<Avatar.Fallback>
						{user.username.slice(0, 1).toUpperCase()}
					</Avatar.Fallback>
				</Avatar.Root>
			{/each}
		</dd>
	</span>
{/if}

with this error:

Argument of type '{ id: string; collectionId: string; avatar: string; username: string; }' is not assignable to parameter of type 'ArrayLike<unknown> | Iterable<unknown>'.ts(2345)

It should be typed as string[] on expand as well, right?

P.S: If I encounter other issues, will let you know. 🙂

@shyakadavis
Copy link
Author

Btw, how can I tell typescript that I know for certain that the expand object in not undefined, and that it has these fields... preferably at the query-building-step.

There are a lot of TS comp errors that have arisen because of the undefined cases, some due to that same issue above of not typing arrays during expand, and some are too vague to track down. For example, when working with svelte-headless-table here's one.

Type 'TableViewModel<RecordModel, { select: TablePlugin<any, SelectedRowsState<any>, Record<string, never>, SelectedRowsPropSet>; ... 4 more ...; hide: TablePlugin<...>; }>' is not assignable to type 'TableViewModel<{ name: string; id: string; created: string; expand: { user: { name: string; id: string; collectionId: string; avatar: string; username: string; }[]; stage: { name: string; }; }; }, AnyPlugins>'.
  Types of property 'flatColumns' are incompatible.
    Type 'FlatColumn<RecordModel, { select: TablePlugin<any, SelectedRowsState<any>, Record<string, never>, SelectedRowsPropSet>; ... 4 more ...; hide: TablePlugin<...>; }, any>[]' is not assignable to type 'FlatColumn<{ name: string; id: string; created: string; expand: { user: { name: string; id: string; collectionId: string; avatar: string; username: string; }[]; stage: { name: string; }; }; }, AnyPlugins, any>[]'.
          Type 'FlatColumn<RecordModel, { select: TablePlugin<any, SelectedRowsState<any>, Record<string, never>, SelectedRowsPropSet>; ... 4 more ...; hide: TablePlugin<...>; }, any>' is not assignable to type 'FlatColumn<{ name: string; id: string; created: string; expand: { user: { name: string; id: string; collectionId: string; avatar: string; username: string; }[]; stage: { name: string; }; }; }, AnyPlugins, any>'.
                  Types of property 'header' are incompatible.
          Type 'HeaderLabel<RecordModel, { select: TablePlugin<any, SelectedRowsState<any>, Record<string, never>, SelectedRowsPropSet>; ... 4 more ...; hide: TablePlugin<...>; }>' is not assignable to type 'HeaderLabel<{ name: string; id: string; created: string; expand: { user: { name: string; id: string; collectionId: string; avatar: string; username: string; }[]; stage: { name: string; }; }; }, AnyPlugins>'.
                      Type '(cell: HeaderCell<RecordModel, AnyPlugins>, state: TableState<RecordModel, { select: TablePlugin<any, SelectedRowsState<any>, Record<...>, SelectedRowsPropSet>; ... 4 more ...; hide: TablePlugin<...>; }>) => RenderConfig' is not assignable to type 'HeaderLabel<{ name: string; id: string; created: string; expand: { user: { name: string; id: string; collectionId: string; avatar: string; username: string; }[]; stage: { name: string; }; }; }, AnyPlugins>'.
                                    Type '(cell: HeaderCell<RecordModel, AnyPlugins>, state: TableState<RecordModel, { select: TablePlugin<any, SelectedRowsState<any>, Record<...>, SelectedRowsPropSet>; ... 4 more ...; hide: TablePlugin<...>; }>) => RenderConfig' is not assignable to type '(cell: HeaderCell<{ name: string; id: string; created: string; expand: { user: { name: string; id: string; collectionId: string; avatar: string; username: string; }[]; stage: { name: string; }; }; }, AnyPlugins>, state: TableState<...>) => RenderConfig'.
                                                    Types of parameters 'cell' and 'cell' are incompatible.
                  Type 'HeaderCell<{ name: string; id: string; created: string; expand: { user: { name: string; id: string; collectionId: string; avatar: string; username: string; }[]; stage: { name: string; }; }; }, AnyPlugins>' is not assignable to type 'HeaderCell<RecordModel, AnyPlugins>'.
                                      Types of property 'label' are incompatible.
                      Type 'HeaderLabel<{ name: string; id: string; created: string; expand: { user: { name: string; id: string; collectionId: string; avatar: string; username: string; }[]; stage: { name: string; }; }; }, AnyPlugins>' is not assignable to type 'HeaderLabel<RecordModel, AnyPlugins>'.
                        Type '(cell: HeaderCell<{ name: string; id: string; created: string; expand: { user: { name: string; id: string; collectionId: string; avatar: string; username: string; }[]; stage: { name: string; }; }; }, AnyPlugins>, state: TableState<...>) => RenderConfig' is not assignable to type 'HeaderLabel<RecordModel, AnyPlugins>'.

@shyakadavis
Copy link
Author

Another issue (sorry if they are becoming too much under this issue #31 🙂)

Problem: Updating the users schema, with some fields I want, refuses said fields if some other fields are missing. For example, trying to update the user's username like so:

// form.data just contains `username` key-value pair
await event.locals.pb.from('users').update(event.locals.user.id, form.data);

errors like so:

Argument of type '{ username: string; }' is not assignable to parameter of type 'UsersUpdate'.
  Type '{ username: string; }' is missing the following properties from type 'UsersUpdate': password, passwordConfirmts(2345)

I tried having a look at UsersUpdate and not much there, but, it seems password & passwordConfirm in the parent AuthCollectionUpdate are always required, thus affecting UsersUpdate. They should be overwritten when defining the UsersUpdate interface, right?

@shyakadavis
Copy link
Author

shyakadavis commented Jan 31, 2024

Another issue: 😅

Problem: createSort generates an invalid sort field.

Update: sort simply doesn't work. Any previous place where I was using sort, and updated with the new syntax, .from(<place>) will not generate any sort query whatsoever.

I'm simply doing like specified in the example of your P.R #29 like so:

	const sort = event.locals.pb.from('projects').createSort('+created');

	const filter = event.locals.pb
		.from('projects')
		.createFilter(`user.id ?= "${event.locals.user.id}"`);

	const select = event.locals.pb.from('projects').createSelect({
		id: true,
		name: true,
		created: true,
		expand: {
			stage: { name: true },
			user: { collectionId: true, id: true, avatar: true, username: true, name: true }
		}
	});

	const projects = await event.locals.pb.from('projects').getFullList({ select, filter, sort });

From the PB dashboard under logs, this is the generated data.url:

/api/collections/projects/records?page=1&perPage=500&skipTotal=1&fields=id%2Cname%2Ccreated%2Cexpand.stage.name%2Cexpand.user.collectionId%2Cexpand.user.id%2Cexpand.user.avatar%2Cexpand.user.username%2Cexpand.user.name&expand=stage%2Cuser&sort=undefined

I don't know why it's not being generated, and/or how to inspect where it's going wrong.


Btw, is there no built-in helpers for this complex kind of filter:

	const filter = event.locals.pb
		.from('projects')
		.createFilter(`user.id ?= "${event.locals.user.id}"`);

@david-plugge
Copy link
Owner

david-plugge commented Jan 31, 2024

Another issue (sorry if they are becoming too much under this issue #31 🙂)

Problem: Updating the users schema, with some fields I want, refuses said fields if some other fields are missing. For example, trying to update the user's username like so:

// form.data just contains `username` key-value pair
await event.locals.pb.from('users').update(event.locals.user.id, form.data);

errors like so:

Argument of type '{ username: string; }' is not assignable to parameter of type 'UsersUpdate'.
  Type '{ username: string; }' is missing the following properties from type 'UsersUpdate': password, passwordConfirmts(2345)

I tried having a look at UsersUpdate and not much there, but, it seems password & passwordConfirm in the parent AuthCollectionUpdate are always required, thus affecting UsersUpdate. They should be overwritten when defining the UsersUpdate interface, right?

good catch, the update type should be optional

Mhm, they should be optional already... Can you send your whole schema?

@shyakadavis
Copy link
Author

Here you go, @david-plugge

/**
 * This file was @generated using typed-pocketbase
 */

// https://pocketbase.io/docs/collections/#base-collection
export interface BaseCollectionResponse {
	/**
	 * 15 characters string to store as record ID.
	 */
	id: string;
	/**
	 * Date string representation for the creation date.
	 */
	created: string;
	/**
	 * Date string representation for the creation date.
	 */
	updated: string;
	/**
	 * The collection id.
	 */
	collectionId: string;
	/**
	 * The collection name.
	 */
	collectionName: string;
}

// https://pocketbase.io/docs/api-records/#create-record
export interface BaseCollectionCreate {
	/**
	 * 15 characters string to store as record ID.
	 * If not set, it will be auto generated.
	 */
	id?: string;
}

// https://pocketbase.io/docs/api-records/#update-record
export interface BaseCollectionUpdate {}

// https://pocketbase.io/docs/collections/#auth-collection
export interface AuthCollectionResponse extends BaseCollectionResponse {
	/**
	 * The username of the auth record.
	 */
	username: string;
	/**
	 * Auth record email address.
	 */
	email: string;
	/**
	 * Whether to show/hide the auth record email when fetching the record data.
	 */
	emailVisibility: boolean;
	/**
	 * Indicates whether the auth record is verified or not.
	 */
	verified: boolean;
}

// https://pocketbase.io/docs/api-records/#create-record
export interface AuthCollectionCreate extends BaseCollectionCreate {
	/**
	 * The username of the auth record.
	 * If not set, it will be auto generated.
	 */
	username?: string;
	/**
	 * Auth record email address.
	 */
	email?: string;
	/**
	 * Whether to show/hide the auth record email when fetching the record data.
	 */
	emailVisibility?: boolean;
	/**
	 * Auth record password.
	 */
	password: string;
	/**
	 * Auth record password confirmation.
	 */
	passwordConfirm: string;
	/**
	 * Indicates whether the auth record is verified or not.
	 * This field can be set only by admins or auth records with "Manage" access.
	 */
	verified?: boolean;
}

// https://pocketbase.io/docs/api-records/#update-record
export interface AuthCollectionUpdate {
	/**
	 * The username of the auth record.
	 */
	username?: string;
	/**
	 * The auth record email address.
	 * This field can be updated only by admins or auth records with "Manage" access.
	 * Regular accounts can update their email by calling "Request email change".
	 */
	email?: string;
	/**
	 * Whether to show/hide the auth record email when fetching the record data.
	 */
	emailVisibility?: boolean;
	/**
	 * Old auth record password.
	 * This field is required only when changing the record password. Admins and auth records with "Manage" access can skip this field.
	 */
	oldPassword?: string;
	/**
	 * New auth record password.
	 */
	password: string;
	/**
	 * New auth record password confirmation.
	 */
	passwordConfirm: string;
	/**
	 * Indicates whether the auth record is verified or not.
	 * This field can be set only by admins or auth records with "Manage" access.
	 */
	verified?: boolean;
}

// https://pocketbase.io/docs/collections/#view-collection
export interface ViewCollectionRecord {
	id: string;
}

// utilities

type MaybeArray<T> = T | T[];

// ===== users =====

export interface UsersResponse extends AuthCollectionResponse {
	collectionName: 'users';
	name: string;
	avatar: string;
}

export interface UsersCreate extends AuthCollectionCreate {
	name?: string;
	avatar?: File;
}

export interface UsersUpdate extends AuthCollectionUpdate {
	name?: string;
	avatar?: File;
}

export interface UsersCollection {
	type: 'auth';
	collectionId: string;
	collectionName: 'users';
	response: UsersResponse;
	create: UsersCreate;
	update: UsersUpdate;
	relations: {
		'posts(user)': PostsCollection;
		'projects(user)': ProjectsCollection;
	};
}

// ===== tags =====

export interface TagsResponse extends BaseCollectionResponse {
	collectionName: 'tags';
	name: string;
}

export interface TagsCreate extends BaseCollectionCreate {
	name: string;
}

export interface TagsUpdate extends BaseCollectionUpdate {
	name?: string;
}

export interface TagsCollection {
	type: 'base';
	collectionId: string;
	collectionName: 'tags';
	response: TagsResponse;
	create: TagsCreate;
	update: TagsUpdate;
	relations: {
		'posts(tags)': PostsCollection;
	};
}

// ===== posts =====

export interface PostsResponse extends BaseCollectionResponse {
	collectionName: 'posts';
	post: string;
	image: string;
	user: string;
	tags: Array<string>;
}

export interface PostsCreate extends BaseCollectionCreate {
	post: string;
	image?: File;
	user?: string;
	tags?: MaybeArray<string>;
}

export interface PostsUpdate extends BaseCollectionUpdate {
	post?: string;
	image?: File;
	user?: string;
	tags?: MaybeArray<string>;
	'tags+'?: MaybeArray<string>;
	'tags-'?: MaybeArray<string>;
}

export interface PostsCollection {
	type: 'base';
	collectionId: string;
	collectionName: 'posts';
	response: PostsResponse;
	create: PostsCreate;
	update: PostsUpdate;
	relations: {
		user: UsersCollection;
		tags: TagsCollection;
	};
}

// ===== projects =====

export interface ProjectsResponse extends BaseCollectionResponse {
	collectionName: 'projects';
	name: string;
	tagline: string;
	description: string;
	url: string;
	category: string;
	stage: string;
	user: Array<string>;
	thumbnail: string;
	twitter: string;
	github: string;
	mrr: number;
}

export interface ProjectsCreate extends BaseCollectionCreate {
	name: string;
	tagline: string;
	description: string;
	url: string | URL;
	category: string;
	stage: string;
	user: MaybeArray<string>;
	thumbnail?: File;
	twitter?: string | URL;
	github?: string | URL;
	mrr?: number;
}

export interface ProjectsUpdate extends BaseCollectionUpdate {
	name?: string;
	tagline?: string;
	description?: string;
	url?: string | URL;
	category?: string;
	stage?: string;
	user?: MaybeArray<string>;
	'user+'?: MaybeArray<string>;
	'user-'?: MaybeArray<string>;
	thumbnail?: File;
	twitter?: string | URL;
	github?: string | URL;
	mrr?: number;
	'mrr+'?: number;
	'mrr-'?: number;
}

export interface ProjectsCollection {
	type: 'base';
	collectionId: string;
	collectionName: 'projects';
	response: ProjectsResponse;
	create: ProjectsCreate;
	update: ProjectsUpdate;
	relations: {
		stage: StagesCollection;
		user: UsersCollection;
	};
}

// ===== stages =====

export interface StagesResponse extends BaseCollectionResponse {
	collectionName: 'stages';
	name: string;
}

export interface StagesCreate extends BaseCollectionCreate {
	name: string;
}

export interface StagesUpdate extends BaseCollectionUpdate {
	name?: string;
}

export interface StagesCollection {
	type: 'base';
	collectionId: string;
	collectionName: 'stages';
	response: StagesResponse;
	create: StagesCreate;
	update: StagesUpdate;
	relations: {
		'projects(stage)': ProjectsCollection;
	};
}

// ===== Schema =====

export type Schema = {
	users: UsersCollection;
	tags: TagsCollection;
	posts: PostsCollection;
	projects: ProjectsCollection;
	stages: StagesCollection;
};

@david-plugge
Copy link
Owner

It looks good to me and sort also returns the correct string in my case. Can you also create a minimal reproduction that i can take a look at?

@david-plugge
Copy link
Owner

david-plugge commented Jan 31, 2024

Btw, is there no built-in helpers for this complex kind of filter:

There is, you can use the array syntax:

	const filter = event.locals.pb
		.from('projects')
		.createFilter(["user.id", "?=", event.locals.user.id]);

There is no good name for this filter i think, if you have one, feel free to suggest it :)

@david-plugge
Copy link
Owner

david-plugge commented Jan 31, 2024

I found the issue regarding sort, the new version 0.1.0-pre.3 should fix that

@shyakadavis
Copy link
Author

Hey. Thanks for the quick patches; sadly, they didn't work.

There is no good name for this filter i think, if you have one, feel free to suggest it :)

So, this is the any/at-least-one-of operator (?=), and to be honest, I don't see how possible it is to abbreviate the thing. 😅
I can't come up with anything right now, but will ping you if I do. 🙂

I found the issue regarding sort, the new version 0.1.0-pre.3 should fix that

It didn't work 🙈
Btw, is there any way to inspect locally the output of the formulated query? Might help me help you pinpoint what's wrong.

Can you also create a minimal reproduction that i can take a look at?

Hmm... I can try. Will let you know. 🙂


Any updates on the other 2 issues; i.e.

  1. Expand doesn't correctly type multiple foreign relation as Array<of_thing> but outputs of_thing | undefined
  2. password & passwordConfirm in the parent AuthCollectionUpdate are always required, thus affecting UsersUpdate

@david-plugge
Copy link
Owner

Any updates on the other 2 issues; i.e.

  1. Expand doesn't correctly type multiple foreign relation as Array<of_thing> but outputs of_thing | undefined
  2. password & passwordConfirm in the parent AuthCollectionUpdate are always required, thus affecting UsersUpdate

This was fixed in the new version, thanks for pointing out!

sadly, they didn't work.

Somehow the package wasn't build before publishing, you can try out the new version

Btw, is there any way to inspect locally the output of the formulated query? Might help me help you pinpoint what's wrong.

You can check in the network tab if you are using a browser. Or you can supply your own fetch function that logs the url

@shyakadavis
Copy link
Author

Thanks a lot @david-plugge

AFAICT, everything is working smoothly on my end with the new custom client (as of pre.6).

The only remark/annoyance is the undefined possibility on all expands and the fields contained within, but I managed to work my way around it. It was not that much of a big a deal.

Again, thanks a ton for the lib. 🙂

@david-plugge
Copy link
Owner

Awesome! I agree that it's somewhat annoying having to check for result.expand.whatever, but there are situatuations where it can be empty.

Glad you like it. Thank you for reporting all the issues and helping me figure out how to make this package even better.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants