Skip to content

Commit

Permalink
response intersection types
Browse files Browse the repository at this point in the history
  • Loading branch information
patmood committed Nov 5, 2022
1 parent 828332d commit 7856ea7
Show file tree
Hide file tree
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
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Up @@ -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
Expand Down Expand Up @@ -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 = [
Expand Down Expand Up @@ -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`);
Expand Down Expand Up @@ -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(
Expand Down
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": {
Expand Down
1 change: 1 addition & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Up @@ -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
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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`)
Expand Down
7 changes: 7 additions & 0 deletions test/__snapshots__/integration.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export type BaseRecord = {
updated: IsoDateString
"@collectionId": string
"@collectionName": string
"@expand"?: { [key: string]: any }
}
export enum Collections {
Expand All @@ -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
Expand Down
11 changes: 11 additions & 0 deletions test/__snapshots__/lib.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -41,6 +49,7 @@ export type BaseRecord = {
updated: IsoDateString
"@collectionId": string
"@collectionName": string
"@expand"?: { [key: string]: any }
}
export enum Collections {
Expand All @@ -51,6 +60,8 @@ export type BooksRecord = {
title?: string
}
export type BooksResponse = BooksRecord & BaseRecord
export type CollectionRecords = {
books: BooksRecord
}"
Expand Down
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",
Expand Down Expand Up @@ -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

0 comments on commit 7856ea7

Please sign in to comment.