Generate typed PocketBase type (#82)

* generate typed pocketbase

* update package-lock.json

* flag for toggling sdk generation

* fix typo

* update documentation

* add newline to end of output

* fix pr comments
This commit is contained in:
Malcolm Nihlén
2023-10-18 01:35:17 +02:00
committed by GitHub
parent 3ab9ca6cf4
commit df1344f67b
15 changed files with 166 additions and 16 deletions

View File

@@ -25,7 +25,8 @@ Options:
-e, --email <char> email for an admin pocketbase user. Use this with the --url option
-p, --password <char> password for an admin pocketbase user. Use this with the --url option
-o, --out <char> path to save the typescript output file (default: "pocketbase-types.ts")
-e, --env flag to use environment variables for configuration, add PB_TYPEGEN_URL, PB_TYPEGEN_EMAIL, PB_TYPEGEN_PASSWORD to your .env file
--no-sdk remove the pocketbase package dependency. A typed version of the SDK will not be generated.
-e, --env [path] flag to use environment variables for configuration. Add PB_TYPEGEN_URL, PB_TYPEGEN_EMAIL, PB_TYPEGEN_PASSWORD to your .env file. Optionally provide a path to your .env file
-h, --help display help for command
```
@@ -71,15 +72,27 @@ 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.
- `CollectionResponses` A type mapping each collection name to the response type.
- `TypedPocketBase` A type for usage with type asserted PocketBase instance.
## Example Usage
In [PocketBase SDK](https://github.com/pocketbase/js-sdk) v0.8+ you can use generic types when fetching records, eg:
Using PocketBase SDK v0.18.3+, collections can be [automatically typed](https://github.com/pocketbase/js-sdk#specify-typescript-definitions) using the generated `TypedPocketBase` type:
```typescript
import { TypedPocketBase } from "./pocketbase-types"
const pb = new PocketBase('http://127.0.0.1:8090') as TypedPocketBase
await pb.collection('tasks').getOne("RECORD_ID") // -> results in TaskResponse
await pb.collection('posts').getOne("RECORD_ID") // -> results in PostResponse
```
Alternatively, you can use generic types for each request, eg:
```typescript
import { Collections, TasksResponse } from "./pocketbase-types"
pb.collection(Collections.Tasks).getOne<TasksResponse>("RECORD_ID") // -> results in Promise<TaskResponse>
await pb.collection(Collections.Tasks).getOne<TasksResponse>("RECORD_ID") // -> results in TaskResponse
```
## Example Advanced Usage

26
dist/index.js vendored
View File

@@ -59,9 +59,13 @@ async function fromURL(url, email = "", password = "") {
var EXPORT_COMMENT = `/**
* This file was @generated using pocketbase-typegen
*/`;
var IMPORTS = `import type PocketBase from 'pocketbase'
import { type RecordService } from 'pocketbase'`;
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 ALL_RECORD_RESPONSE_COMMENT = `// Types containing all Records and Responses, useful for creating typing helper functions`;
var TYPED_POCKETBASE_COMMENT = `// Type for usage with type asserted PocketBase instance
// https://github.com/pocketbase/js-sdk#specify-typescript-definitions`;
var EXPAND_GENERIC_NAME = "expand";
var DATE_STRING_TYPE_NAME = `IsoDateString`;
var RECORD_ID_STRING_NAME = `RecordIdString`;
@@ -142,6 +146,12 @@ function createCollectionResponses(collectionNames) {
${nameRecordMap}
}`;
}
function createTypedPocketbase(collectionNames) {
const nameRecordMap = collectionNames.map((name) => ` collection(idOrName: '${name}'): RecordService<${toPascalCase(name)}Response>`).join("\n");
return `export type TypedPocketBase = PocketBase & {
${nameRecordMap}
}`;
}
// src/generics.ts
function fieldNameToGeneric(name) {
@@ -217,7 +227,7 @@ function getSelectOptionEnumName(val) {
}
// src/lib.ts
function generate(results) {
function generate(results, options2) {
const collectionNames = [];
const recordTypes = [];
const responseTypes = [RESPONSE_TYPE_COMMENT];
@@ -232,6 +242,7 @@ function generate(results) {
const sortedCollectionNames = collectionNames;
const fileParts = [
EXPORT_COMMENT,
options2.sdk && IMPORTS,
createCollectionEnum(sortedCollectionNames),
ALIAS_TYPE_DEFINITIONS,
BASE_SYSTEM_FIELDS_DEFINITION,
@@ -241,9 +252,11 @@ function generate(results) {
responseTypes.join("\n"),
ALL_RECORD_RESPONSE_COMMENT,
createCollectionRecords(sortedCollectionNames),
createCollectionResponses(sortedCollectionNames)
createCollectionResponses(sortedCollectionNames),
options2.sdk && TYPED_POCKETBASE_COMMENT,
options2.sdk && createTypedPocketbase(sortedCollectionNames)
];
return fileParts.join("\n\n");
return fileParts.filter(Boolean).join("\n\n") + "\n";
}
function createRecordType(name, schema) {
const selectOptionEnums = createSelectOptions(name, schema);
@@ -295,7 +308,9 @@ async function main(options2) {
"Missing schema path. Check options: pocketbase-typegen --help"
);
}
const typeString = generate(schema);
const typeString = generate(schema, {
sdk: options2.sdk ?? true
});
await saveFile(options2.out, typeString);
return typeString;
}
@@ -325,6 +340,9 @@ program.name("Pocketbase Typegen").version(version).description(
"-o, --out <char>",
"path to save the typescript output file",
"pocketbase-types.ts"
).option(
"--no-sdk",
"remove the pocketbase package dependency. A typed version of the SDK will not be generated."
).option(
"-e, --env [path]",
"flag to use environment variables for configuration. Add PB_TYPEGEN_URL, PB_TYPEGEN_EMAIL, PB_TYPEGEN_PASSWORD to your .env file. Optionally provide a path to your .env file"

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "pocketbase-typegen",
"version": "1.1.10",
"version": "1.1.13",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "pocketbase-typegen",
"version": "1.1.10",
"version": "1.1.13",
"license": "ISC",
"dependencies": {
"commander": "^9.4.1",

View File

@@ -35,7 +35,9 @@ export async function main(options: Options) {
"Missing schema path. Check options: pocketbase-typegen --help"
)
}
const typeString = generate(schema)
const typeString = generate(schema, {
sdk: options.sdk ?? true
})
await saveFile(options.out, typeString)
return typeString
}

View File

@@ -31,3 +31,14 @@ export function createCollectionResponses(
${nameRecordMap}
}`
}
export function createTypedPocketbase(
collectionNames: Array<string>
): string {
const nameRecordMap = collectionNames
.map((name) => `\tcollection(idOrName: '${name}'): RecordService<${toPascalCase(name)}Response>`)
.join("\n")
return `export type TypedPocketBase = PocketBase & {
${nameRecordMap}
}`
}

View File

@@ -1,9 +1,12 @@
export const EXPORT_COMMENT = `/**
* This file was @generated using pocketbase-typegen
*/`
export const IMPORTS = `import type PocketBase from 'pocketbase'
import { type RecordService } from 'pocketbase'`
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 ALL_RECORD_RESPONSE_COMMENT = `// Types containing all Records and Responses, useful for creating typing helper functions`
export const TYPED_POCKETBASE_COMMENT = `// Type for usage with type asserted PocketBase instance\n// https://github.com/pocketbase/js-sdk#specify-typescript-definitions`
export const EXPAND_GENERIC_NAME = "expand"
export const DATE_STRING_TYPE_NAME = `IsoDateString`
export const RECORD_ID_STRING_NAME = `RecordIdString`

View File

@@ -33,6 +33,10 @@ program
"path to save the typescript output file",
"pocketbase-types.ts"
)
.option(
"--no-sdk",
"remove the pocketbase package dependency. A typed version of the SDK will not be generated."
)
.option(
"-e, --env [path]",
"flag to use environment variables for configuration. Add PB_TYPEGEN_URL, PB_TYPEGEN_EMAIL, PB_TYPEGEN_PASSWORD to your .env file. Optionally provide a path to your .env file"

View File

@@ -1,18 +1,21 @@
import {
ALIAS_TYPE_DEFINITIONS,
ALL_RECORD_RESPONSE_COMMENT,
TYPED_POCKETBASE_COMMENT,
AUTH_SYSTEM_FIELDS_DEFINITION,
BASE_SYSTEM_FIELDS_DEFINITION,
EXPAND_GENERIC_NAME,
EXPORT_COMMENT,
RECORD_TYPE_COMMENT,
RESPONSE_TYPE_COMMENT,
IMPORTS,
} from "./constants"
import { CollectionRecord, FieldSchema } from "./types"
import {
createCollectionEnum,
createCollectionRecords,
createCollectionResponses,
createTypedPocketbase,
} from "./collections"
import { createSelectOptions, createTypeField } from "./fields"
import {
@@ -21,7 +24,11 @@ import {
} from "./generics"
import { getSystemFields, toPascalCase } from "./utils"
export function generate(results: Array<CollectionRecord>): string {
type GenerateOptions = {
sdk: boolean
}
export function generate(results: Array<CollectionRecord>, options: GenerateOptions): string {
const collectionNames: Array<string> = []
const recordTypes: Array<string> = []
const responseTypes: Array<string> = [RESPONSE_TYPE_COMMENT]
@@ -39,6 +46,7 @@ export function generate(results: Array<CollectionRecord>): string {
const fileParts = [
EXPORT_COMMENT,
options.sdk && IMPORTS,
createCollectionEnum(sortedCollectionNames),
ALIAS_TYPE_DEFINITIONS,
BASE_SYSTEM_FIELDS_DEFINITION,
@@ -49,9 +57,13 @@ export function generate(results: Array<CollectionRecord>): string {
ALL_RECORD_RESPONSE_COMMENT,
createCollectionRecords(sortedCollectionNames),
createCollectionResponses(sortedCollectionNames),
options.sdk && TYPED_POCKETBASE_COMMENT,
options.sdk && createTypedPocketbase(sortedCollectionNames)
]
return fileParts.join("\n\n")
return fileParts
.filter(Boolean)
.join("\n\n") + '\n'
}
export function createRecordType(

View File

@@ -5,6 +5,7 @@ export type Options = {
json?: string
email?: string
password?: string
sdk?: boolean
env?: boolean | string
}

View File

@@ -20,3 +20,10 @@ exports[`createCollectionResponses creates mapping of collection name to respons
magazine: MagazineResponse
}"
`;
exports[`createTypedPocketBase creates typed variant of PocketBase client 1`] = `
"export type TypedPocketBase = PocketBase & {
collection(idOrName: 'book'): RecordService<BookResponse>
collection(idOrName: 'magazine'): RecordService<MagazineResponse>
}"
`;

View File

@@ -5,6 +5,9 @@ exports[`creates a type file from json schema 1`] = `
* This file was @generated using pocketbase-typegen
*/
import type PocketBase from 'pocketbase'
import { type RecordService } from 'pocketbase'
export enum Collections {
Base = "base",
CustomAuth = "custom_auth",
@@ -117,5 +120,18 @@ export type CollectionResponses = {
my_view: MyViewResponse
posts: PostsResponse
users: UsersResponse
}"
}
// Type for usage with type asserted PocketBase instance
// https://github.com/pocketbase/js-sdk#specify-typescript-definitions
export type TypedPocketBase = PocketBase & {
collection(idOrName: 'base'): RecordService<BaseResponse>
collection(idOrName: 'custom_auth'): RecordService<CustomAuthResponse>
collection(idOrName: 'everything'): RecordService<EverythingResponse>
collection(idOrName: 'my_view'): RecordService<MyViewResponse>
collection(idOrName: 'posts'): RecordService<PostsResponse>
collection(idOrName: 'users'): RecordService<UsersResponse>
}
"
`;

View File

@@ -25,6 +25,9 @@ exports[`generate generates correct output given db input 1`] = `
* This file was @generated using pocketbase-typegen
*/
import type PocketBase from 'pocketbase'
import { type RecordService } from 'pocketbase'
export enum Collections {
Books = "books",
}
@@ -68,5 +71,13 @@ export type CollectionRecords = {
export type CollectionResponses = {
books: BooksResponse
}"
}
// Type for usage with type asserted PocketBase instance
// https://github.com/pocketbase/js-sdk#specify-typescript-definitions
export type TypedPocketBase = PocketBase & {
collection(idOrName: 'books'): RecordService<BooksResponse>
}
"
`;

View File

@@ -2,6 +2,7 @@ import {
createCollectionEnum,
createCollectionRecords,
createCollectionResponses,
createTypedPocketbase,
} from "../src/collections"
describe("createCollectionEnum", () => {
@@ -24,3 +25,10 @@ describe("createCollectionResponses", () => {
expect(createCollectionResponses(names)).toMatchSnapshot()
})
})
describe("createTypedPocketBase", () => {
it("creates typed variant of PocketBase client", () => {
const names = ["book", "magazine"]
expect(createTypedPocketbase(names)).toMatchSnapshot()
})
})

View File

@@ -27,9 +27,38 @@ describe("generate", () => {
viewRule: null,
},
]
const result = generate(collections)
const result = generate(collections, { sdk: true })
expect(result).toMatchSnapshot()
})
it("skips generatic sdk if told not to", () => {
const collections: Array<CollectionRecord> = [
{
createRule: null,
deleteRule: null,
id: "123",
listRule: null,
name: "books",
schema: [
{
id: "xyz",
name: "title",
options: {},
required: false,
system: false,
type: "text",
unique: false,
},
],
system: false,
type: "base",
updateRule: null,
viewRule: null,
},
]
const result = generate(collections, { sdk: false })
expect(result).not.toMatch(/import .* from 'pocketbase'/)
})
})
describe("createRecordType", () => {

View File

@@ -2,6 +2,9 @@
* This file was @generated using pocketbase-typegen
*/
import type PocketBase from 'pocketbase'
import { type RecordService } from 'pocketbase'
export enum Collections {
Base = "base",
CustomAuth = "custom_auth",
@@ -114,4 +117,16 @@ export type CollectionResponses = {
my_view: MyViewResponse
posts: PostsResponse
users: UsersResponse
}
}
// Type for usage with type asserted PocketBase instance
// https://github.com/pocketbase/js-sdk#specify-typescript-definitions
export type TypedPocketBase = PocketBase & {
collection(idOrName: 'base'): RecordService<BaseResponse>
collection(idOrName: 'custom_auth'): RecordService<CustomAuthResponse>
collection(idOrName: 'everything'): RecordService<EverythingResponse>
collection(idOrName: 'my_view'): RecordService<MyViewResponse>
collection(idOrName: 'posts'): RecordService<PostsResponse>
collection(idOrName: 'users'): RecordService<UsersResponse>
}