Skip to content

Commit

Permalink
Support generics for expand fields (#30)
Browse files Browse the repository at this point in the history
* add generics for expand fields

* update naming
  • Loading branch information
patmood authored Jan 22, 2023
1 parent fd61e68 commit 883dc25
Show file tree
Hide file tree
Showing 10 changed files with 170 additions and 60 deletions.
46 changes: 41 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ This will produce types for all your PocketBase collections to use in your front

## Versions

When using PocketBase v0.8.x, use `pocketbase-typegen` v1.1.x
When using PocketBase > v0.8.x, use `pocketbase-typegen` v1.1.x

Users of PocketBase v0.7.x should use `pocketbase-typegen` v1.0.x
Users of PocketBase < v0.7.x should use `pocketbase-typegen` v1.0.x

## Usage

Expand Down Expand Up @@ -40,7 +40,15 @@ URL example:

`npx pocketbase-typegen --url https://myproject.pockethost.io --email [email protected] --password 'secr3tp@ssword!'`

## Example output
Add it to your projects `package.json`:

```
"scripts": {
"typegen": "pocketbase-typegen --db ./pb_data/data.db",
},
```

## Example Output

The output is a typescript file `pocketbase-types.ts` ([example](./test/pocketbase-types-example.ts)) which will contain:

Expand All @@ -50,12 +58,40 @@ The output is a typescript file `pocketbase-types.ts` ([example](./test/pocketba
- `[CollectionName][FieldName]Options` If the collection contains a select field with set values, an enum of the options will be generated.
- `CollectionRecords` A type mapping each collection name to the record type.

## Example usage
## Example Usage

In [PocketBase SDK](https://github.com/pocketbase/js-sdk) v0.8 you can use generic types when fetching records, eg:
In [PocketBase SDK](https://github.com/pocketbase/js-sdk) v0.8+ you can use generic types when fetching records, eg:

```typescript
import { Collections, TasksResponse } from "./pocketbase-types"

pb.collection(Collections.Tasks).getOne<TasksResponse>("RECORD_ID") // -> results in Promise<TaskResponse>
```

## Example Advanced Usage

You can provide types for JSON fields and [expanded relations](https://pocketbase.io/docs/expanding-relations/) by passing generic arguments to the Response types:

```typescript
import { Collections, CommentsResponse, UserResponse } from "./pocketbase-types"

/**
type CommentsRecord<Tmetadata = unknown> = {
text: string
metadata: null | Tmetadata
user: RecordIdString
}
*/
type Tmetadata = {
likes: number
}
type Texpand = {
user: UsersResponse
}
const result = await pb
.collection(Collections.Comments)
.getOne<CommentsResponse<Tmetadata, Texpand>>("RECORD_ID", { expand: "user" })

// Now you can access the expanded relation with type safety and hints in your IDE
result.expand?.user.username
```
36 changes: 24 additions & 12 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,26 +43,27 @@ var EXPORT_COMMENT = `/**
*/`;
var RECORD_TYPE_COMMENT = `// Record types for each collection`;
var RESPONSE_TYPE_COMMENT = `// Response types include system fields and match responses from the PocketBase API`;
var EXPAND_GENERIC_NAME = "expand";
var DATE_STRING_TYPE_NAME = `IsoDateString`;
var RECORD_ID_STRING_NAME = `RecordIdString`;
var ALIAS_TYPE_DEFINITIONS = `// Alias types for improved usability
export type ${DATE_STRING_TYPE_NAME} = string
export type ${RECORD_ID_STRING_NAME} = string`;
var BASE_SYSTEM_FIELDS_DEFINITION = `// System fields
export type BaseSystemFields = {
export type BaseSystemFields<T = never> = {
id: ${RECORD_ID_STRING_NAME}
created: ${DATE_STRING_TYPE_NAME}
updated: ${DATE_STRING_TYPE_NAME}
collectionId: string
collectionName: Collections
expand?: { [key: string]: any }
expand?: T
}`;
var AUTH_SYSTEM_FIELDS_DEFINITION = `export type AuthSystemFields = {
var AUTH_SYSTEM_FIELDS_DEFINITION = `export type AuthSystemFields<T = never> = {
email: string
emailVisibility: boolean
username: string
verified: boolean
} & BaseSystemFields`;
} & BaseSystemFields<T>`;

// src/generics.ts
function fieldNameToGeneric(name) {
Expand All @@ -72,18 +73,24 @@ function getGenericArgList(schema) {
const jsonFields = schema.filter((field) => field.type === "json").map((field) => fieldNameToGeneric(field.name)).sort();
return jsonFields;
}
function getGenericArgString(schema) {
function getGenericArgStringForRecord(schema) {
const argList = getGenericArgList(schema);
if (argList.length === 0)
return "";
return `<${argList.map((name) => `${name}`).join(", ")}>`;
}
function getGenericArgStringWithDefault(schema) {
function getGenericArgStringWithDefault(schema, opts) {
const argList = getGenericArgList(schema);
if (opts.includeExpand && canExpand(schema)) {
argList.push(fieldNameToGeneric(EXPAND_GENERIC_NAME));
}
if (argList.length === 0)
return "";
return `<${argList.map((name) => `${name} = unknown`).join(", ")}>`;
}
function canExpand(schema) {
return !!schema.find((field) => field.type === "relation");
}

// src/utils.ts
import { promises as fs2 } from "fs";
Expand Down Expand Up @@ -130,7 +137,7 @@ var pbSchemaTypescriptMap = {
},
json: (fieldSchema) => `null | ${fieldNameToGeneric(fieldSchema.name)}`,
file: (fieldSchema) => fieldSchema.options.maxSelect && fieldSchema.options.maxSelect > 1 ? "string[]" : "string",
relation: (fieldSchema) => fieldSchema.options.maxSelect && fieldSchema.options.maxSelect > 1 ? `${RECORD_ID_STRING_NAME}[]` : RECORD_ID_STRING_NAME,
relation: (fieldSchema) => fieldSchema.options.maxSelect && fieldSchema.options.maxSelect === 1 ? RECORD_ID_STRING_NAME : `${RECORD_ID_STRING_NAME}[]`,
user: (fieldSchema) => fieldSchema.options.maxSelect && fieldSchema.options.maxSelect > 1 ? `${RECORD_ID_STRING_NAME}[]` : RECORD_ID_STRING_NAME
};
function generate(results) {
Expand Down Expand Up @@ -183,7 +190,9 @@ ${nameRecordMap}
function createRecordType(name, schema) {
const selectOptionEnums = createSelectOptions(name, schema);
const typeName = toPascalCase(name);
const genericArgs = getGenericArgStringWithDefault(schema);
const genericArgs = getGenericArgStringWithDefault(schema, {
includeExpand: false
});
const fields = schema.map((fieldSchema) => createTypeField(name, fieldSchema)).join("\n");
return `${selectOptionEnums}export type ${typeName}Record${genericArgs} = {
${fields}
Expand All @@ -192,10 +201,13 @@ ${fields}
function createResponseType(collectionSchemaEntry) {
const { name, schema, type } = collectionSchemaEntry;
const pascaleName = toPascalCase(name);
const genericArgsWithDefaults = getGenericArgStringWithDefault(schema);
const genericArgs = getGenericArgString(schema);
const genericArgsWithDefaults = getGenericArgStringWithDefault(schema, {
includeExpand: true
});
const genericArgsForRecord = getGenericArgStringForRecord(schema);
const systemFields = getSystemFields(type);
return `export type ${pascaleName}Response${genericArgsWithDefaults} = ${pascaleName}Record${genericArgs} & ${systemFields}`;
const expandArgString = canExpand(schema) ? `<T${EXPAND_GENERIC_NAME}>` : "";
return `export type ${pascaleName}Response${genericArgsWithDefaults} = ${pascaleName}Record${genericArgsForRecord} & ${systemFields}${expandArgString}`;
}
function createTypeField(collectionName, fieldSchema) {
if (!(fieldSchema.type in pbSchemaTypescriptMap)) {
Expand Down Expand Up @@ -241,7 +253,7 @@ async function main(options2) {
import { program } from "commander";

// package.json
var version = "1.1.2";
var version = "1.1.3";

// src/index.ts
program.name("Pocketbase Typegen").version(version).description(
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "pocketbase-typegen",
"version": "1.1.2",
"version": "1.1.3",
"description": "Generate pocketbase record types from your database",
"main": "dist/index.js",
"bin": {
Expand Down Expand Up @@ -59,7 +59,7 @@
"testEnvironment": "node",
"modulePathIgnorePatterns": [
"dist",
"pocketbase-types-examples.ts"
"test/pocketbase-types-example.ts"
]
},
"prettier": {
Expand Down
9 changes: 5 additions & 4 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,26 @@ export const EXPORT_COMMENT = `/**
*/`
export const RECORD_TYPE_COMMENT = `// Record types for each collection`
export const RESPONSE_TYPE_COMMENT = `// Response types include system fields and match responses from the PocketBase API`
export const EXPAND_GENERIC_NAME = "expand"
export const DATE_STRING_TYPE_NAME = `IsoDateString`
export const RECORD_ID_STRING_NAME = `RecordIdString`
export const ALIAS_TYPE_DEFINITIONS = `// Alias types for improved usability
export type ${DATE_STRING_TYPE_NAME} = string
export type ${RECORD_ID_STRING_NAME} = string`

export const BASE_SYSTEM_FIELDS_DEFINITION = `// System fields
export type BaseSystemFields = {
export type BaseSystemFields<T = never> = {
\tid: ${RECORD_ID_STRING_NAME}
\tcreated: ${DATE_STRING_TYPE_NAME}
\tupdated: ${DATE_STRING_TYPE_NAME}
\tcollectionId: string
\tcollectionName: Collections
\texpand?: { [key: string]: any }
\texpand?: T
}`

export const AUTH_SYSTEM_FIELDS_DEFINITION = `export type AuthSystemFields = {
export const AUTH_SYSTEM_FIELDS_DEFINITION = `export type AuthSystemFields<T = never> = {
\temail: string
\temailVisibility: boolean
\tusername: string
\tverified: boolean
} & BaseSystemFields`
} & BaseSystemFields<T>`
18 changes: 16 additions & 2 deletions src/generics.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { EXPAND_GENERIC_NAME } from "./constants"
import { FieldSchema } from "./types"

export function fieldNameToGeneric(name: string) {
Expand All @@ -12,14 +13,27 @@ export function getGenericArgList(schema: FieldSchema[]): string[] {
return jsonFields
}

export function getGenericArgString(schema: FieldSchema[]): string {
export function getGenericArgStringForRecord(schema: FieldSchema[]): string {
const argList = getGenericArgList(schema)
if (argList.length === 0) return ""
return `<${argList.map((name) => `${name}`).join(", ")}>`
}

export function getGenericArgStringWithDefault(schema: FieldSchema[]): string {
export function getGenericArgStringWithDefault(
schema: FieldSchema[],
opts: { includeExpand: boolean }
): string {
const argList = getGenericArgList(schema)

if (opts.includeExpand && canExpand(schema)) {
argList.push(fieldNameToGeneric(EXPAND_GENERIC_NAME))
}

if (argList.length === 0) return ""
return `<${argList.map((name) => `${name} = unknown`).join(", ")}>`
}

// Does the collection have relation fields that can be expanded
export function canExpand(schema: FieldSchema[]) {
return !!schema.find((field) => field.type === "relation")
}
17 changes: 12 additions & 5 deletions src/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,17 @@ import {
AUTH_SYSTEM_FIELDS_DEFINITION,
BASE_SYSTEM_FIELDS_DEFINITION,
DATE_STRING_TYPE_NAME,
EXPAND_GENERIC_NAME,
EXPORT_COMMENT,
RECORD_ID_STRING_NAME,
RECORD_TYPE_COMMENT,
RESPONSE_TYPE_COMMENT,
} from "./constants"
import { CollectionRecord, FieldSchema } from "./types"
import {
canExpand,
fieldNameToGeneric,
getGenericArgString,
getGenericArgStringForRecord,
getGenericArgStringWithDefault,
} from "./generics"
import {
Expand Down Expand Up @@ -119,7 +121,9 @@ export function createRecordType(
): string {
const selectOptionEnums = createSelectOptions(name, schema)
const typeName = toPascalCase(name)
const genericArgs = getGenericArgStringWithDefault(schema)
const genericArgs = getGenericArgStringWithDefault(schema, {
includeExpand: false,
})
const fields = schema
.map((fieldSchema: FieldSchema) => createTypeField(name, fieldSchema))
.join("\n")
Expand All @@ -132,11 +136,14 @@ ${fields}
export function createResponseType(collectionSchemaEntry: CollectionRecord) {
const { name, schema, type } = collectionSchemaEntry
const pascaleName = toPascalCase(name)
const genericArgsWithDefaults = getGenericArgStringWithDefault(schema)
const genericArgs = getGenericArgString(schema)
const genericArgsWithDefaults = getGenericArgStringWithDefault(schema, {
includeExpand: true,
})
const genericArgsForRecord = getGenericArgStringForRecord(schema)
const systemFields = getSystemFields(type)
const expandArgString = canExpand(schema) ? `<T${EXPAND_GENERIC_NAME}>` : ""

return `export type ${pascaleName}Response${genericArgsWithDefaults} = ${pascaleName}Record${genericArgs} & ${systemFields}`
return `export type ${pascaleName}Response${genericArgsWithDefaults} = ${pascaleName}Record${genericArgsForRecord} & ${systemFields}${expandArgString}`
}

export function createTypeField(
Expand Down
10 changes: 5 additions & 5 deletions test/__snapshots__/fromJSON.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -18,21 +18,21 @@ export type IsoDateString = string
export type RecordIdString = string
// System fields
export type BaseSystemFields = {
export type BaseSystemFields<T = never> = {
id: RecordIdString
created: IsoDateString
updated: IsoDateString
collectionId: string
collectionName: Collections
expand?: { [key: string]: any }
expand?: T
}
export type AuthSystemFields = {
export type AuthSystemFields<T = never> = {
email: string
emailVisibility: boolean
username: string
verified: boolean
} & BaseSystemFields
} & BaseSystemFields<T>
// Record types for each collection
Expand Down Expand Up @@ -85,7 +85,7 @@ export type UsersRecord = {
// Response types include system fields and match responses from the PocketBase API
export type BaseResponse = BaseRecord & BaseSystemFields
export type CustomAuthResponse = CustomAuthRecord & AuthSystemFields
export type EverythingResponse<Tanother_json_field = unknown, Tjson_field = unknown> = EverythingRecord<Tanother_json_field, Tjson_field> & BaseSystemFields
export type EverythingResponse<Tanother_json_field = unknown, Tjson_field = unknown, Texpand = unknown> = EverythingRecord<Tanother_json_field, Tjson_field> & BaseSystemFields<Texpand>
export type PostsResponse = PostsRecord & BaseSystemFields
export type UsersResponse = UsersRecord & AuthSystemFields
Expand Down
8 changes: 4 additions & 4 deletions test/__snapshots__/lib.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -58,21 +58,21 @@ export type IsoDateString = string
export type RecordIdString = string
// System fields
export type BaseSystemFields = {
export type BaseSystemFields<T = never> = {
id: RecordIdString
created: IsoDateString
updated: IsoDateString
collectionId: string
collectionName: Collections
expand?: { [key: string]: any }
expand?: T
}
export type AuthSystemFields = {
export type AuthSystemFields<T = never> = {
email: string
emailVisibility: boolean
username: string
verified: boolean
} & BaseSystemFields
} & BaseSystemFields<T>
// Record types for each collection
Expand Down
Loading

0 comments on commit 883dc25

Please sign in to comment.