diff --git a/README.md b/README.md index 7982c82..f9b4224 100644 --- a/README.md +++ b/README.md @@ -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 @@ -40,7 +40,15 @@ URL example: `npx pocketbase-typegen --url https://myproject.pockethost.io --email admin@myproject.com --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: @@ -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("RECORD_ID") // -> results in Promise ``` + +## 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 = { + text: string + metadata: null | Tmetadata + user: RecordIdString + } +*/ +type Tmetadata = { + likes: number +} +type Texpand = { + user: UsersResponse +} +const result = await pb + .collection(Collections.Comments) + .getOne>("RECORD_ID", { expand: "user" }) + +// Now you can access the expanded relation with type safety and hints in your IDE +result.expand?.user.username +``` diff --git a/dist/index.js b/dist/index.js index c9b3274..5d7d286 100755 --- a/dist/index.js +++ b/dist/index.js @@ -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 = { 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 = { email: string emailVisibility: boolean username: string verified: boolean -} & BaseSystemFields`; +} & BaseSystemFields`; // src/generics.ts function fieldNameToGeneric(name) { @@ -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"; @@ -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) { @@ -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} @@ -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) ? `` : ""; + return `export type ${pascaleName}Response${genericArgsWithDefaults} = ${pascaleName}Record${genericArgsForRecord} & ${systemFields}${expandArgString}`; } function createTypeField(collectionName, fieldSchema) { if (!(fieldSchema.type in pbSchemaTypescriptMap)) { @@ -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( diff --git a/package.json b/package.json index 393ef34..59f7fbd 100644 --- a/package.json +++ b/package.json @@ -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": { @@ -59,7 +59,7 @@ "testEnvironment": "node", "modulePathIgnorePatterns": [ "dist", - "pocketbase-types-examples.ts" + "test/pocketbase-types-example.ts" ] }, "prettier": { diff --git a/src/constants.ts b/src/constants.ts index ff9e6e5..db3a46e 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -3,6 +3,7 @@ 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 @@ -10,18 +11,18 @@ 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 = { \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 = { \temail: string \temailVisibility: boolean \tusername: string \tverified: boolean -} & BaseSystemFields` +} & BaseSystemFields` diff --git a/src/generics.ts b/src/generics.ts index a697bfe..7e71870 100644 --- a/src/generics.ts +++ b/src/generics.ts @@ -1,3 +1,4 @@ +import { EXPAND_GENERIC_NAME } from "./constants" import { FieldSchema } from "./types" export function fieldNameToGeneric(name: string) { @@ -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") +} diff --git a/src/lib.ts b/src/lib.ts index 5801ed6..4598a38 100644 --- a/src/lib.ts +++ b/src/lib.ts @@ -3,6 +3,7 @@ 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, @@ -10,8 +11,9 @@ import { } from "./constants" import { CollectionRecord, FieldSchema } from "./types" import { + canExpand, fieldNameToGeneric, - getGenericArgString, + getGenericArgStringForRecord, getGenericArgStringWithDefault, } from "./generics" import { @@ -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") @@ -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) ? `` : "" - return `export type ${pascaleName}Response${genericArgsWithDefaults} = ${pascaleName}Record${genericArgs} & ${systemFields}` + return `export type ${pascaleName}Response${genericArgsWithDefaults} = ${pascaleName}Record${genericArgsForRecord} & ${systemFields}${expandArgString}` } export function createTypeField( diff --git a/test/__snapshots__/fromJSON.test.ts.snap b/test/__snapshots__/fromJSON.test.ts.snap index 17cf9e1..2f88bc8 100644 --- a/test/__snapshots__/fromJSON.test.ts.snap +++ b/test/__snapshots__/fromJSON.test.ts.snap @@ -18,21 +18,21 @@ export type IsoDateString = string export type RecordIdString = string // System fields -export type BaseSystemFields = { +export type BaseSystemFields = { id: RecordIdString created: IsoDateString updated: IsoDateString collectionId: string collectionName: Collections - expand?: { [key: string]: any } + expand?: T } -export type AuthSystemFields = { +export type AuthSystemFields = { email: string emailVisibility: boolean username: string verified: boolean -} & BaseSystemFields +} & BaseSystemFields // Record types for each collection @@ -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 = EverythingRecord & BaseSystemFields +export type EverythingResponse = EverythingRecord & BaseSystemFields export type PostsResponse = PostsRecord & BaseSystemFields export type UsersResponse = UsersRecord & AuthSystemFields diff --git a/test/__snapshots__/lib.test.ts.snap b/test/__snapshots__/lib.test.ts.snap index 27644c6..fbecb0f 100644 --- a/test/__snapshots__/lib.test.ts.snap +++ b/test/__snapshots__/lib.test.ts.snap @@ -58,21 +58,21 @@ export type IsoDateString = string export type RecordIdString = string // System fields -export type BaseSystemFields = { +export type BaseSystemFields = { id: RecordIdString created: IsoDateString updated: IsoDateString collectionId: string collectionName: Collections - expand?: { [key: string]: any } + expand?: T } -export type AuthSystemFields = { +export type AuthSystemFields = { email: string emailVisibility: boolean username: string verified: boolean -} & BaseSystemFields +} & BaseSystemFields // Record types for each collection diff --git a/test/generics.test.ts b/test/generics.test.ts index 0e4de25..57a1e9e 100644 --- a/test/generics.test.ts +++ b/test/generics.test.ts @@ -1,6 +1,7 @@ import { + canExpand, getGenericArgList, - getGenericArgString, + getGenericArgStringForRecord, getGenericArgStringWithDefault, } from "../src/generics" @@ -33,6 +34,15 @@ const jsonField2: FieldSchema = { required: true, type: "json", } +const expandField: FieldSchema = { + id: "4", + system: false, + unique: false, + options: {}, + name: "post_relation_field", + required: true, + type: "relation", +} describe("getGenericArgList", () => { it("returns a list of generic args", () => { @@ -53,46 +63,76 @@ describe("getGenericArgList", () => { describe("getGenericArgStringWithDefault", () => { it("empty string when no generic fields", () => { - expect(getGenericArgStringWithDefault([textField])).toEqual("") + expect( + getGenericArgStringWithDefault([textField], { includeExpand: false }) + ).toEqual("") }) it("returns a single generic string", () => { - expect(getGenericArgStringWithDefault([textField, jsonField1])).toEqual( - "" - ) + expect( + getGenericArgStringWithDefault([textField, jsonField1], { + includeExpand: false, + }) + ).toEqual("") }) it("multiple generics with a record", () => { expect( - getGenericArgStringWithDefault([textField, jsonField1, jsonField2]) + getGenericArgStringWithDefault([textField, jsonField1, jsonField2], { + includeExpand: false, + }) ).toEqual("") }) it("sorts the arguments", () => { expect( - getGenericArgStringWithDefault([textField, jsonField2, jsonField1]) + getGenericArgStringWithDefault([textField, jsonField2, jsonField1], { + includeExpand: false, + }) ).toEqual("") }) + + it("includes generic arg for expand fields", () => { + expect( + getGenericArgStringWithDefault( + [textField, jsonField2, jsonField1, expandField], + { + includeExpand: true, + } + ) + ).toEqual("") + }) }) -describe("getGenericArgString", () => { +describe("getGenericArgStringForRecord", () => { it("empty string when no generic fields", () => { - expect(getGenericArgString([textField])).toEqual("") + expect(getGenericArgStringForRecord([textField])).toEqual("") }) it("returns a single generic string", () => { - expect(getGenericArgString([textField, jsonField1])).toEqual("") + expect(getGenericArgStringForRecord([textField, jsonField1])).toEqual( + "" + ) }) it("multiple generics with a record", () => { - expect(getGenericArgString([textField, jsonField1, jsonField2])).toEqual( - "" - ) + expect( + getGenericArgStringForRecord([textField, jsonField1, jsonField2]) + ).toEqual("") }) it("sorts the arguments", () => { - expect(getGenericArgString([textField, jsonField2, jsonField1])).toEqual( - "" - ) + expect( + getGenericArgStringForRecord([textField, jsonField2, jsonField1]) + ).toEqual("") + }) +}) + +describe("canExpand", () => { + it("detects collections that can be expanded", () => { + expect(canExpand([textField, jsonField1, expandField])).toEqual(true) + }) + it("detects collections that cannot be expanded", () => { + expect(canExpand([textField, jsonField1])).toEqual(false) }) }) diff --git a/test/pocketbase-types-example.ts b/test/pocketbase-types-example.ts index ba37840..2092425 100644 --- a/test/pocketbase-types-example.ts +++ b/test/pocketbase-types-example.ts @@ -15,21 +15,21 @@ export type IsoDateString = string export type RecordIdString = string // System fields -export type BaseSystemFields = { +export type BaseSystemFields = { id: RecordIdString created: IsoDateString updated: IsoDateString collectionId: string collectionName: Collections - expand?: { [key: string]: any } + expand?: T } -export type AuthSystemFields = { +export type AuthSystemFields = { email: string emailVisibility: boolean username: string verified: boolean -} & BaseSystemFields +} & BaseSystemFields // Record types for each collection @@ -82,7 +82,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 = EverythingRecord & BaseSystemFields +export type EverythingResponse = EverythingRecord & BaseSystemFields export type PostsResponse = PostsRecord & BaseSystemFields export type UsersResponse = UsersRecord & AuthSystemFields