diff --git a/README.md b/README.md index df310b0..719f320 100644 --- a/README.md +++ b/README.md @@ -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(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("RECORD_ID") // -> results in Promise` diff --git a/dist/index.js b/dist/index.js index d6cb6b5..22d106a 100755 --- a/dist/index.js +++ b/dist/index.js @@ -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,7 +155,7 @@ 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); @@ -151,6 +163,13 @@ function createRecordType(name, schema) { 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( diff --git a/package.json b/package.json index a0743ef..54eab16 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/src/constants.ts b/src/constants.ts index 68fec57..e6a1dae 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -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 } }` diff --git a/src/generics.ts b/src/generics.ts index c2c7b2e..a697bfe 100644 --- a/src/generics.ts +++ b/src/generics.ts @@ -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(", ")}>` } diff --git a/src/lib.ts b/src/lib.ts index 9fd08eb..32cf459 100644 --- a/src/lib.ts +++ b/src/lib.ts @@ -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) { 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,7 +88,7 @@ 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) }) @@ -89,6 +96,14 @@ export function createRecordType( return typeString } +export function createResponseType(name: string, schema: Array) { + 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`) diff --git a/test/__snapshots__/integration.test.ts.snap b/test/__snapshots__/integration.test.ts.snap index 987fc32..0c26ac9 100644 --- a/test/__snapshots__/integration.test.ts.snap +++ b/test/__snapshots__/integration.test.ts.snap @@ -15,6 +15,7 @@ export type BaseRecord = { updated: IsoDateString "@collectionId": string "@collectionName": string + "@expand"?: { [key: string]: any } } export enum Collections { @@ -37,6 +38,8 @@ export type EveryTypeRecord = { user_field?: UserIdString } +export type EveryTypeResponse = EveryTypeRecord & BaseRecord + export type OrdersRecord = { amount: number payment_type: "credit card" | "paypal" | "crypto" @@ -44,12 +47,16 @@ export type OrdersRecord = { 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 diff --git a/test/__snapshots__/lib.test.ts.snap b/test/__snapshots__/lib.test.ts.snap index e7fbf6e..83d2ec6 100644 --- a/test/__snapshots__/lib.test.ts.snap +++ b/test/__snapshots__/lib.test.ts.snap @@ -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 }" diff --git a/test/generics.test.ts b/test/generics.test.ts index c868588..0e4de25 100644 --- a/test/generics.test.ts +++ b/test/generics.test.ts @@ -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( "" ) }) it("multiple generics with a record", () => { - expect(getGenericArgString([textField, jsonField1, jsonField2])).toBe( - "" + expect( + getGenericArgStringWithDefault([textField, jsonField1, jsonField2]) + ).toEqual("") + }) + + it("sorts the arguments", () => { + expect( + getGenericArgStringWithDefault([textField, jsonField2, jsonField1]) + ).toEqual("") + }) +}) + +describe("getGenericArgString", () => { + it("empty string when no generic fields", () => { + expect(getGenericArgString([textField])).toEqual("") + }) + + it("returns a single generic string", () => { + expect(getGenericArgString([textField, jsonField1])).toEqual("") + }) + + it("multiple generics with a record", () => { + expect(getGenericArgString([textField, jsonField1, jsonField2])).toEqual( + "" + ) + }) + + it("sorts the arguments", () => { + expect(getGenericArgString([textField, jsonField2, jsonField1])).toEqual( + "" ) }) }) diff --git a/test/lib.test.ts b/test/lib.test.ts index 853da62..c8f91b0 100644 --- a/test/lib.test.ts +++ b/test/lib.test.ts @@ -3,6 +3,7 @@ import { createCollectionEnum, createCollectionRecord, createRecordType, + createResponseType, createTypeField, generate, } from "../src/lib" @@ -97,6 +98,42 @@ describe("createRecordType", () => { }) }) +describe("createResponseType", () => { + it("creates type definition for a response", () => { + const name = "books" + const schema: FieldSchema[] = [ + { + system: false, + id: "hhnwjkke", + name: "title", + type: "text", + required: false, + unique: false, + options: { min: null, max: null, pattern: "" }, + }, + ] + const result = createResponseType(name, schema) + expect(result).toMatchSnapshot() + }) + + it("handles file fields with multiple files", () => { + const name = "books" + const schema: FieldSchema[] = [ + { + system: false, + id: "hhnwjkke", + name: "avatars", + type: "file", + required: false, + unique: false, + options: { maxSelect: 2 }, + }, + ] + const result = createRecordType(name, schema) + expect(result).toMatchSnapshot() + }) +}) + describe("createTypeField", () => { it("handles required and optional fields", () => { expect( diff --git a/test/pocketbase-types-example.ts b/test/pocketbase-types-example.ts index 07e07d0..d341b97 100644 --- a/test/pocketbase-types-example.ts +++ b/test/pocketbase-types-example.ts @@ -12,6 +12,7 @@ export type BaseRecord = { updated: IsoDateString "@collectionId": string "@collectionName": string + "@expand"?: { [key: string]: any } } export enum Collections { @@ -34,6 +35,8 @@ export type EveryTypeRecord = { user_field?: UserIdString } +export type EveryTypeResponse = EveryTypeRecord & BaseRecord + export type OrdersRecord = { amount: number payment_type: "credit card" | "paypal" | "crypto" @@ -41,12 +44,16 @@ export type OrdersRecord = { 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