Skip to content

Commit

Permalink
response intersection types
Browse files Browse the repository at this point in the history
patmood committed Nov 5, 2022
1 parent 828332d commit 7856ea7
Showing 11 changed files with 193 additions and 50 deletions.
28 changes: 8 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
@@ -6,7 +6,7 @@ Generate typescript definitions from your [pocketbase.io](https://pocketbase.io/

`npx pocketbase-typegen --db ./pb_data/data.db --out pocketbase-types.ts`

This will produce types for all your pocketbase collections to use in your frontend typescript codebase.
This will produce types for all your PocketBase collections to use in your frontend typescript codebase.

## Usage

@@ -36,24 +36,12 @@ URL example:

## Example output

The output is a typescript file `pocketbase-types.ts` ([example](./test/pocketbase-types-example.ts)) which will contain one type for each collection and an enum of all collections.
The output is a typescript file `pocketbase-types.ts` ([example](./test/pocketbase-types-example.ts)) which will contain:

For example an "order" collection record might look like this:
- An enum of all collections
- One type for each collection (eg `ProfilesRecord`)
- One response type for each collection (eg `ProfilesResponse`) which includes base fields like id, updated, created
- A type `CollectionRecords` mapping each collection name to the record type

```typescript
export type OrdersRecord = {
amount: number
payment_type: "credit card" | "paypal" | "crypto"
user: UserIdString
product: string
}
```
Using the [pocketbase SDK](https://github.com/pocketbase/js-sdk) (v0.8.x onwards), you can then type your responses like this:
```typescript
import type { Collections, ProfilesRecord } from "./path/to/pocketbase-types.ts"
await client.records.getList<ProfilesRecord>(Collections.Profiles, 1, 50)
```

Now the `result` of the data fetch will be accurately typed throughout your codebase!
In the upcoming [PocketBase SDK](https://github.com/pocketbase/js-sdk) v0.8 you will be able to use generic types when fetching records, eg:
`pb.collection('tasks').getOne<Task>("RECORD_ID") // -> results in Promise<Task>`
43 changes: 31 additions & 12 deletions dist/index.js
Original file line number Diff line number Diff line change
@@ -46,23 +46,33 @@ var RECORD_ID_STRING_DEFINITION = `export type ${RECORD_ID_STRING_NAME} = string
var USER_ID_STRING_NAME = `UserIdString`;
var USER_ID_STRING_DEFINITION = `export type ${USER_ID_STRING_NAME} = string`;
var BASE_RECORD_DEFINITION = `export type BaseRecord = {
id: ${RECORD_ID_STRING_NAME}
created: ${DATE_STRING_TYPE_NAME}
updated: ${DATE_STRING_TYPE_NAME}
"@collectionId": string
"@collectionName": string
id: ${RECORD_ID_STRING_NAME}
created: ${DATE_STRING_TYPE_NAME}
updated: ${DATE_STRING_TYPE_NAME}
"@collectionId": string
"@collectionName": string
"@expand"?: { [key: string]: any }
}`;

// src/generics.ts
function fieldNameToGeneric(name) {
return `T${name}`;
}
function getGenericArgList(schema) {
const jsonFields = schema.filter((field) => field.type === "json").map((field) => fieldNameToGeneric(field.name)).sort();
return jsonFields;
}
function getGenericArgString(schema) {
const jsonFields = schema.filter((field) => field.type === "json").map((field) => field.name).sort();
if (jsonFields.length === 0) {
const argList = getGenericArgList(schema);
if (argList.length === 0)
return "";
}
return `<${jsonFields.map((name) => `${fieldNameToGeneric(name)} = unknown`).join(", ")}>`;
return `<${argList.map((name) => `${name}`).join(", ")}>`;
}
function getGenericArgStringWithDefault(schema) {
const argList = getGenericArgList(schema);
if (argList.length === 0)
return "";
return `<${argList.map((name) => `${name} = unknown`).join(", ")}>`;
}

// src/utils.ts
@@ -104,8 +114,10 @@ function generate(results) {
results.forEach((row) => {
if (row.name)
collectionNames.push(row.name);
if (row.schema)
if (row.schema) {
recordTypes.push(createRecordType(row.name, row.schema));
recordTypes.push(createResponseType(row.name, row.schema));
}
});
const sortedCollectionNames = collectionNames.sort();
const fileParts = [
@@ -143,14 +155,21 @@ function createCollectionRecord(collectionNames) {
function createRecordType(name, schema) {
let typeString = `export type ${toPascalCase(
name
)}Record${getGenericArgString(schema)} = {
)}Record${getGenericArgStringWithDefault(schema)} = {
`;
schema.forEach((fieldSchema) => {
typeString += createTypeField(fieldSchema);
});
typeString += `}`;
return typeString;
}
function createResponseType(name, schema) {
const pascaleName = toPascalCase(name);
let typeString = `export type ${pascaleName}Response${getGenericArgStringWithDefault(
schema
)} = ${pascaleName}Record${getGenericArgString(schema)} & BaseRecord`;
return typeString;
}
function createTypeField(fieldSchema) {
if (!(fieldSchema.type in pbSchemaTypescriptMap)) {
throw new Error(`unknown type ${fieldSchema.type} found in schema`);
@@ -184,7 +203,7 @@ async function main(options2) {
import { program } from "commander";

// package.json
var version = "1.0.11";
var version = "1.0.12";

// src/index.ts
program.name("Pocketbase Typegen").version(version).description(
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "pocketbase-typegen",
"version": "1.0.11",
"version": "1.0.12",
"description": "Generate pocketbase record types from your database",
"main": "dist/index.js",
"bin": {
1 change: 1 addition & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
@@ -11,4 +11,5 @@ export const BASE_RECORD_DEFINITION = `export type BaseRecord = {
\tupdated: ${DATE_STRING_TYPE_NAME}
\t"@collectionId": string
\t"@collectionName": string
\t"@expand"?: { [key: string]: any }
}`
23 changes: 15 additions & 8 deletions src/generics.ts
Original file line number Diff line number Diff line change
@@ -4,15 +4,22 @@ export function fieldNameToGeneric(name: string) {
return `T${name}`
}

export function getGenericArgString(schema: FieldSchema[]) {
export function getGenericArgList(schema: FieldSchema[]): string[] {
const jsonFields = schema
.filter((field) => field.type === "json")
.map((field) => field.name)
.map((field) => fieldNameToGeneric(field.name))
.sort()
if (jsonFields.length === 0) {
return ""
}
return `<${jsonFields
.map((name) => `${fieldNameToGeneric(name)} = unknown`)
.join(", ")}>`
return jsonFields
}

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

export function getGenericArgStringWithDefault(schema: FieldSchema[]): string {
const argList = getGenericArgList(schema)
if (argList.length === 0) return ""
return `<${argList.map((name) => `${name} = unknown`).join(", ")}>`
}
21 changes: 18 additions & 3 deletions src/lib.ts
Original file line number Diff line number Diff line change
@@ -9,7 +9,11 @@ import {
USER_ID_STRING_NAME,
} from "./constants"
import { CollectionRecord, FieldSchema } from "./types"
import { fieldNameToGeneric, getGenericArgString } from "./generics"
import {
fieldNameToGeneric,
getGenericArgString,
getGenericArgStringWithDefault,
} from "./generics"
import { sanitizeFieldName, toPascalCase } from "./utils"

const pbSchemaTypescriptMap = {
@@ -39,7 +43,10 @@ export function generate(results: Array<CollectionRecord>) {

results.forEach((row) => {
if (row.name) collectionNames.push(row.name)
if (row.schema) recordTypes.push(createRecordType(row.name, row.schema))
if (row.schema) {
recordTypes.push(createRecordType(row.name, row.schema))
recordTypes.push(createResponseType(row.name, row.schema))
}
})
const sortedCollectionNames = collectionNames.sort()

@@ -81,14 +88,22 @@ export function createRecordType(
): string {
let typeString = `export type ${toPascalCase(
name
)}Record${getGenericArgString(schema)} = {\n`
)}Record${getGenericArgStringWithDefault(schema)} = {\n`
schema.forEach((fieldSchema: FieldSchema) => {
typeString += createTypeField(fieldSchema)
})
typeString += `}`
return typeString
}

export function createResponseType(name: string, schema: Array<FieldSchema>) {
const pascaleName = toPascalCase(name)
let typeString = `export type ${pascaleName}Response${getGenericArgStringWithDefault(
schema
)} = ${pascaleName}Record${getGenericArgString(schema)} & BaseRecord`
return typeString
}

export function createTypeField(fieldSchema: FieldSchema) {
if (!(fieldSchema.type in pbSchemaTypescriptMap)) {
throw new Error(`unknown type ${fieldSchema.type} found in schema`)
7 changes: 7 additions & 0 deletions test/__snapshots__/integration.test.ts.snap
Original file line number Diff line number Diff line change
@@ -15,6 +15,7 @@ export type BaseRecord = {
updated: IsoDateString
"@collectionId": string
"@collectionName": string
"@expand"?: { [key: string]: any }
}
export enum Collections {
@@ -37,19 +38,25 @@ export type EveryTypeRecord<Tjson_field = unknown> = {
user_field?: UserIdString
}
export type EveryTypeResponse<Tjson_field = unknown> = EveryTypeRecord<Tjson_field> & BaseRecord
export type OrdersRecord = {
amount: number
payment_type: "credit card" | "paypal" | "crypto"
user: UserIdString
product: string
}
export type OrdersResponse = OrdersRecord & BaseRecord
export type ProfilesRecord = {
userId: UserIdString
name?: string
avatar?: string
}
export type ProfilesResponse = ProfilesRecord & BaseRecord
export type CollectionRecords = {
every_type: EveryTypeRecord
orders: OrdersRecord
11 changes: 11 additions & 0 deletions test/__snapshots__/lib.test.ts.snap
Original file line number Diff line number Diff line change
@@ -26,6 +26,14 @@ exports[`createRecordType handles file fields with multiple files 1`] = `
}"
`;

exports[`createResponseType creates type definition for a response 1`] = `"export type BooksResponse = BooksRecord & BaseRecord"`;

exports[`createResponseType handles file fields with multiple files 1`] = `
"export type BooksRecord = {
avatars?: string[]
}"
`;

exports[`generate generates correct output given db input 1`] = `
"// This file was @generated using pocketbase-typegen
@@ -41,6 +49,7 @@ export type BaseRecord = {
updated: IsoDateString
"@collectionId": string
"@collectionName": string
"@expand"?: { [key: string]: any }
}
export enum Collections {
@@ -51,6 +60,8 @@ export type BooksRecord = {
title?: string
}
export type BooksResponse = BooksRecord & BaseRecord
export type CollectionRecords = {
books: BooksRecord
}"
63 changes: 57 additions & 6 deletions test/generics.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import {
getGenericArgList,
getGenericArgString,
getGenericArgStringWithDefault,
} from "../src/generics"

import { FieldSchema } from "../src/types"
import { getGenericArgString } from "../src/generics"

const textField: FieldSchema = {
id: "1",
@@ -28,20 +33,66 @@ const jsonField2: FieldSchema = {
required: true,
type: "json",
}
describe("getGenericArgString", () => {

describe("getGenericArgList", () => {
it("returns a list of generic args", () => {
expect(getGenericArgList([jsonField1])).toEqual(["Tdata1"])
expect(getGenericArgList([textField, jsonField1, jsonField2])).toEqual([
"Tdata1",
"Tdata2",
])
})

it("sorts the arg list", () => {
expect(getGenericArgList([jsonField2, jsonField1])).toEqual([
"Tdata1",
"Tdata2",
])
})
})

describe("getGenericArgStringWithDefault", () => {
it("empty string when no generic fields", () => {
expect(getGenericArgString([textField])).toBe("")
expect(getGenericArgStringWithDefault([textField])).toEqual("")
})

it("returns a single generic string", () => {
expect(getGenericArgString([textField, jsonField1])).toBe(
expect(getGenericArgStringWithDefault([textField, jsonField1])).toEqual(
"<Tdata1 = unknown>"
)
})

it("multiple generics with a record", () => {
expect(getGenericArgString([textField, jsonField1, jsonField2])).toBe(
"<Tdata1 = unknown, Tdata2 = unknown>"
expect(
getGenericArgStringWithDefault([textField, jsonField1, jsonField2])
).toEqual("<Tdata1 = unknown, Tdata2 = unknown>")
})

it("sorts the arguments", () => {
expect(
getGenericArgStringWithDefault([textField, jsonField2, jsonField1])
).toEqual("<Tdata1 = unknown, Tdata2 = unknown>")
})
})

describe("getGenericArgString", () => {
it("empty string when no generic fields", () => {
expect(getGenericArgString([textField])).toEqual("")
})

it("returns a single generic string", () => {
expect(getGenericArgString([textField, jsonField1])).toEqual("<Tdata1>")
})

it("multiple generics with a record", () => {
expect(getGenericArgString([textField, jsonField1, jsonField2])).toEqual(
"<Tdata1, Tdata2>"
)
})

it("sorts the arguments", () => {
expect(getGenericArgString([textField, jsonField2, jsonField1])).toEqual(
"<Tdata1, Tdata2>"
)
})
})
Loading
Oops, something went wrong.

0 comments on commit 7856ea7

Please sign in to comment.