Skip to content

Conversation

@jackyscript
Copy link

@jackyscript jackyscript commented Nov 29, 2025

#143

Hi @patmood, refering to the issue #143 here. This is an attempt to defer field length metadata from pocketbase to use for example input validation in the frontend. Basically I wanted to provide the possibility to validate field input data in the frontend using the metadata given in the pocketbase backend.
I am not very confident that this handles every edge case, I think this pull request could benefit from more tests.

When quickly testing this locally I did the following:

  • Adjust the Everything collection in the test/pb.schema.json file locally and set the max property to 350 (just for the sake of an example)
  • Run npm run build
  • Run node dist/index.js --json ./test/pb_schema.json --out ./pocketbase-types.ts --constraints

The result is a file called pocketbase-types.constraints.ts (you can adjust the filename using cli param --constraintsOut):

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

export const EverythingFieldConstraints = {
  text_field: { max: 350 }
}

export const UsersFieldConstraints = {
  name: { max: 255 }
}

For context: When working in the frontend with validation you could use this generated metadata to setup the maxLength property of an input field or some validation logic, that the input does not exceed a certain text length, just as an example:

I hope this is somewhat useful. feel free to leave questions and comments, they are much appreciated!

EDIT: After the first review and the scope changes resulting from that, I updated the PR's title.

Copy link
Owner

@patmood patmood left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @jackyscript,

Thanks for the PR! Overall looks good, can you please address the comments and add some tests for the new behavior?

src/lib.ts Outdated
parts.push(`min: ${field.min}`)
}
if (typeof field.max === "number" && field.max !== 0) {
parts.push(`max: ${field.max}`)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what about other fields? eg maxSelect, maxSize, required?

Copy link
Author

@jackyscript jackyscript Dec 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, I think this pull request currently only addresses text input field types:

  const fieldsToIgnore = ["id", "password", "tokenKey", "token"]
  const textBasedTypes = ["text", "email", "url", "number"]

  const metadata = schema
    .filter((field) => {
      if (field.system || fieldsToIgnore.includes(field.name)) return false
      if (!textBasedTypes.includes(field.type)) return false
    })

I gather maxSelect and maxSize apply for relation fields and json fields respectively. Do you think we should add them here with this pull request too? Or should we add those in a subsequent PR, what do you think? I will add the required metadata though now, as this is useful for validation too.

Edit, see my answer in the next comment.

Copy link
Author

@jackyscript jackyscript Dec 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nevermind, for the sake of illustrating the metadata content, I included all fields in and discarded the current filter, then there is a lot of metadata outputted (see below). Should we keep that or skip over some of the metadata?

I think we could use some degree of filtering, as I feel we do not need everything in the frontend or the metadata generated is already provided elsewhere (for instance the select field values).

EDIT: After I reflected on this for a while, I think generally this is fine and it is consistent with the output of the pocketbase-types.ts file. Maybe for the time being, we could remove the select field values output, as this is already provided with the current enum Options types in pocketbase-types.ts. In a later PR we could merge them, i.e. in the Metadata we reference the Options accordingly. What do you think?

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

export const AuthoriginsFieldMetadata = {
	id: {
		min: 15,
		max: 15,
		required: true,
		pattern: "^[a-z0-9]+$",
	},
	collectionRef: {
		required: true,
	},
	recordRef: {
		required: true,
	},
	fingerprint: {
		required: true,
	},
	created: {
		onCreate: true,
		onUpdate: false,
	},
	updated: {
		onCreate: true,
		onUpdate: true,
	},
} as const
export const ExternalauthsFieldMetadata = {
	id: {
		min: 15,
		max: 15,
		required: true,
		pattern: "^[a-z0-9]+$",
	},
	collectionRef: {
		required: true,
	},
	recordRef: {
		required: true,
	},
	provider: {
		required: true,
	},
	providerId: {
		required: true,
	},
	created: {
		onCreate: true,
		onUpdate: false,
	},
	updated: {
		onCreate: true,
		onUpdate: true,
	},
} as const
export const MfasFieldMetadata = {
	id: {
		min: 15,
		max: 15,
		required: true,
		pattern: "^[a-z0-9]+$",
	},
	collectionRef: {
		required: true,
	},
	recordRef: {
		required: true,
	},
	method: {
		required: true,
	},
	created: {
		onCreate: true,
		onUpdate: false,
	},
	updated: {
		onCreate: true,
		onUpdate: true,
	},
} as const
export const OtpsFieldMetadata = {
	id: {
		min: 15,
		max: 15,
		required: true,
		pattern: "^[a-z0-9]+$",
	},
	collectionRef: {
		required: true,
	},
	recordRef: {
		required: true,
	},
	password: {
		required: true,
	},
	created: {
		onCreate: true,
		onUpdate: false,
	},
	updated: {
		onCreate: true,
		onUpdate: true,
	},
	sentTo: {
		required: false,
	},
} as const
export const SuperusersFieldMetadata = {
	id: {
		min: 15,
		max: 15,
		required: true,
		pattern: "^[a-z0-9]+$",
	},
	password: {
		min: 8,
		required: true,
	},
	tokenKey: {
		min: 30,
		max: 60,
		required: true,
	},
	email: {
		required: true,
	},
	emailVisibility: {
		required: false,
	},
	verified: {
		required: false,
	},
	created: {
		onCreate: true,
		onUpdate: false,
	},
	updated: {
		onCreate: true,
		onUpdate: true,
	},
} as const
export const BaseFieldMetadata = {
	id: {
		min: 15,
		max: 15,
		required: true,
		pattern: "^[a-z0-9]+$",
	},
	field: {
		required: false,
	},
	created: {
		onCreate: true,
		onUpdate: false,
	},
	updated: {
		onCreate: true,
		onUpdate: true,
	},
} as const
export const CustomAuthFieldMetadata = {
	id: {
		min: 15,
		max: 15,
		required: true,
		pattern: "^[a-z0-9]+$",
	},
	password: {
		min: 8,
		required: true,
	},
	tokenKey: {
		min: 30,
		max: 60,
		required: true,
	},
	email: {
		required: true,
	},
	emailVisibility: {
		required: false,
	},
	verified: {
		required: false,
	},
	custom_field: {
		required: false,
	},
	created: {
		onCreate: true,
		onUpdate: false,
	},
	updated: {
		onCreate: true,
		onUpdate: true,
	},
} as const
export const EverythingFieldMetadata = {
	id: {
		min: 15,
		max: 15,
		required: true,
		pattern: "^[a-z0-9]+$",
	},
	text_field: {
		required: false,
	},
	number_field: {
		required: false,
	},
	bool_field: {
		required: false,
	},
	email_field: {
		required: false,
	},
	url_field: {
		required: false,
	},
	date_field: {
		required: false,
	},
	select_field: {
		maxSelect: 1,
		required: false,
		values: ["optionA","optionA","OptionA","optionB","optionC","option with space","sy?mb@!$"],
	},
	json_field: {
		maxSize: 2000000,
		required: false,
	},
	another_json_field: {
		maxSize: 2000000,
		required: false,
	},
	file_field: {
		maxSelect: 1,
		required: false,
	},
	three_files_field: {
		maxSelect: 99,
		required: false,
	},
	user_relation_field: {
		maxSelect: 1,
		required: false,
	},
	custom_relation_field: {
		maxSelect: 999,
		required: false,
	},
	select_field_no_values: {
		required: false,
	},
	rich_editor_field: {
		required: false,
	},
	post_relation_field: {
		maxSelect: 1,
		required: false,
	},
	geopoint_field: {
		required: false,
	},
	created: {
		onCreate: true,
		onUpdate: false,
	},
	updated: {
		onCreate: false,
		onUpdate: true,
	},
} as const
export const MyViewFieldMetadata = {
	id: {
		required: true,
		pattern: "^[a-z0-9]+$",
	},
	post_relation_field: {
		maxSelect: 1,
		required: false,
	},
	text_field: {
		required: false,
	},
	json_field: {
		maxSize: 2000000,
		required: false,
	},
} as const
export const PostsFieldMetadata = {
	id: {
		min: 15,
		max: 15,
		required: true,
		pattern: "^[a-z0-9]+$",
	},
	nonempty_field: {
		required: true,
	},
	nonempty_bool: {
		required: true,
	},
	field1: {
		required: false,
	},
	created: {
		onCreate: true,
		onUpdate: false,
	},
	updated: {
		onCreate: false,
		onUpdate: true,
	},
} as const
export const UsersFieldMetadata = {
	id: {
		min: 15,
		max: 15,
		required: true,
		pattern: "^[a-z0-9]+$",
	},
	password: {
		min: 8,
		required: true,
	},
	tokenKey: {
		min: 30,
		max: 60,
		required: true,
	},
	email: {
		required: true,
	},
	emailVisibility: {
		required: false,
	},
	verified: {
		required: false,
	},
	name: {
		max: 255,
		required: false,
	},
	avatar: {
		maxSelect: 1,
		mimeTypes: ["image/jpeg","image/png","image/svg+xml","image/gif","image/webp"],
		required: false,
	},
	created: {
		onCreate: true,
		onUpdate: false,
	},
	updated: {
		onCreate: true,
		onUpdate: true,
	},
} as const

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For example the EverythingCollectionsMetadata could look like this (in a separate PR):

...
export const EverythingFieldMetadata = {
	id: {
		min: 15,
		max: 15,
		required: true,
		pattern: "^[a-z0-9]+$",
	},
	text_field: {
		required: false,
	},
	number_field: {
		required: false,
	},
	bool_field: {
		required: false,
	},
	email_field: {
		required: false,
	},
	url_field: {
		required: false,
	},
	date_field: {
		required: false,
	},
	select_field: {
		maxSelect: 1,
		required: false,
		values: EverythingSelectFieldOptions,
	},
	json_field: {
		maxSize: 2000000,
		required: false,
	},
	another_json_field: {
		maxSize: 2000000,
		required: false,
	},
	file_field: {
		maxSelect: 1,
		required: false,
	},
	three_files_field: {
		maxSelect: 99,
		required: false,
	},
	user_relation_field: {
		maxSelect: 1,
		required: false,
	},
	custom_relation_field: {
		maxSelect: 999,
		required: false,
	},
	select_field_no_values: {
		required: false,
	},
	rich_editor_field: {
		required: false,
	},
	post_relation_field: {
		maxSelect: 1,
		required: false,
	},
	geopoint_field: {
		required: false,
	},
	created: {
		onCreate: true,
		onUpdate: false,
	},
	updated: {
		onCreate: false,
		onUpdate: true,
	},
} as const

export enum EverythingSelectFieldOptions {
	"optionA" = "optionA",
	"OptionA" = "OptionA",
	"optionB" = "optionB",
	"optionC" = "optionC",
	"option with space" = "option with space",
	"sy?mb@!$" = "sy?mb@!$",
}
...

@jackyscript
Copy link
Author

jackyscript commented Dec 7, 2025

@patmood thank you for your feedback, I adjusted the PR accordingly and added some tests. There is an open question left regarding the content of the pocketbase-metadata.ts file and filtering.

EDIT: Additionally when I created the tests, typescript requested me to add the properties maxSize and mimeTypes to the RecordOptions, so that the FieldSchema declarations in the metadata.test.ts file would stop showing type errors, hence I added them to the RecordOptions type in types.ts. I am not 100% sure, if this is really necessary, feel free to let me know, if I should change that.

@jackyscript jackyscript changed the title generate field constraints generate metadata Dec 7, 2025
@patmood
Copy link
Owner

patmood commented Dec 26, 2025

Hey @jackyscript, after seeing the latest version with all fields, I realise it's exposing a lot of your schema in JS. I think a better option is to fetch the collection from the API on the client (if it has permission), and use the data from that to validate.

If there's no user permission, you could have a simple script in your project to fetch the collections and filter them by only the collection/fields you want.

How does that sound?

@jackyscript
Copy link
Author

Hi @patmood thank you for your thoughts, I can see your concerns regarding information exposure.

Given I had a very specific use case in mind, this approach here seems to be a bit too much also for the scope of this project, I will go with your script idea, that way, frontend developers can utilize the metadata for validation right away.

Let us close this pull request then, again thank you for your feedback and review.

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

Successfully merging this pull request may close these issues.

2 participants