Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support PocketBase version 0.23.0-rc12 #103

Merged
merged 10 commits into from
Nov 27, 2024
Merged
12 changes: 6 additions & 6 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
# Dockerfile to run e2e integration tests against a test PocketBase server
FROM node:16-alpine3.16

ARG POCKETBASE_VERSION=0.15.0
ARG POCKETBASE_VERSION=0.23.1

WORKDIR /app/output/
WORKDIR /app/

# Install the dependencies
RUN apk add --no-cache \
ca-certificates \
unzip \
wget \
zip \
zlib-dev
ca-certificates \
unzip \
wget \
zip \
zlib-dev

# Download Pocketbase and install it
ADD https://github.com/pocketbase/pocketbase/releases/download/v${POCKETBASE_VERSION}/pocketbase_${POCKETBASE_VERSION}_linux_amd64.zip /tmp/pocketbase.zip
Expand Down
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@ Generate typescript definitions from your [pocketbase.io](https://pocketbase.io/

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

## Versions
## Version Support

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
| PocketBase | pocketbase-typegen | npx command |
|------------|--------------------| ------------------------------------------------------------------------------ |
| v0.23.x | v1.2.x | npx pocketbase-typegen --db ./pb_data/data.db --out pocketbase-types.ts |
| v0.8.x | v1.1.x | npx [email protected] --db ./pb_data/data.db --out pocketbase-types.ts |
| v0.7.x | v1.0.x | npx [email protected] --db ./pb_data/data.db --out pocketbase-types.ts |

## Usage

Expand Down
49 changes: 28 additions & 21 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ async function fromDatabase(dbPath) {
const result = await db.all("SELECT * FROM _collections");
return result.map((collection) => ({
...collection,
schema: JSON.parse(collection.schema)
fields: JSON.parse(collection.fields)
}));
}
async function fromJSON(path) {
Expand All @@ -30,10 +30,13 @@ async function fromURL(url, email = "", password = "") {
formData.append("password", password);
let collections = [];
try {
const { token } = await fetch(`${url}/api/admins/auth-with-password`, {
body: formData,
method: "post"
}).then((res) => {
const { token } = await fetch(
`${url}/api/collections/_superusers/auth-with-password`,
{
body: formData,
method: "post"
}
).then((res) => {
if (!res.ok)
throw res;
return res.json();
Expand Down Expand Up @@ -77,8 +80,6 @@ export type ${HTML_STRING_NAME} = string`;
var BASE_SYSTEM_FIELDS_DEFINITION = `// System fields
export type BaseSystemFields<T = never> = {
id: ${RECORD_ID_STRING_NAME}
created: ${DATE_STRING_TYPE_NAME}
updated: ${DATE_STRING_TYPE_NAME}
collectionId: string
collectionName: Collections
expand?: T
Expand Down Expand Up @@ -120,7 +121,7 @@ function getOptionEnumName(recordName, fieldName) {
return `${toPascalCase(recordName)}${toPascalCase(fieldName)}Options`;
}
function getOptionValues(field) {
const values = field.options.values;
const values = field.values;
if (!values)
return [];
return values.filter((val, i) => values.indexOf(val) === i);
Expand All @@ -147,7 +148,11 @@ ${nameRecordMap}
}`;
}
function createTypedPocketbase(collectionNames) {
const nameRecordMap = collectionNames.map((name) => ` collection(idOrName: '${name}'): RecordService<${toPascalCase(name)}Response>`).join("\n");
const nameRecordMap = collectionNames.map(
(name) => ` collection(idOrName: '${name}'): RecordService<${toPascalCase(
name
)}Response>`
).join("\n");
return `export type TypedPocketBase = PocketBase & {
${nameRecordMap}
}`;
Expand Down Expand Up @@ -181,19 +186,21 @@ function getGenericArgStringWithDefault(schema, opts) {
var pbSchemaTypescriptMap = {
bool: "boolean",
date: DATE_STRING_TYPE_NAME,
autodate: DATE_STRING_TYPE_NAME,
editor: HTML_STRING_NAME,
email: "string",
text: "string",
url: "string",
password: "string",
number: "number",
file: (fieldSchema) => fieldSchema.options.maxSelect && fieldSchema.options.maxSelect > 1 ? "string[]" : "string",
file: (fieldSchema) => fieldSchema.maxSelect && fieldSchema.maxSelect > 1 ? "string[]" : "string",
json: (fieldSchema) => `null | ${fieldNameToGeneric(fieldSchema.name)}`,
relation: (fieldSchema) => fieldSchema.options.maxSelect && fieldSchema.options.maxSelect === 1 ? RECORD_ID_STRING_NAME : `${RECORD_ID_STRING_NAME}[]`,
relation: (fieldSchema) => fieldSchema.maxSelect && fieldSchema.maxSelect === 1 ? RECORD_ID_STRING_NAME : `${RECORD_ID_STRING_NAME}[]`,
select: (fieldSchema, collectionName) => {
const valueType = fieldSchema.options.values ? getOptionEnumName(collectionName, fieldSchema.name) : "string";
return fieldSchema.options.maxSelect && fieldSchema.options.maxSelect > 1 ? `${valueType}[]` : valueType;
const valueType = fieldSchema.values ? getOptionEnumName(collectionName, fieldSchema.name) : "string";
return fieldSchema.maxSelect && fieldSchema.maxSelect > 1 ? `${valueType}[]` : valueType;
},
user: (fieldSchema) => fieldSchema.options.maxSelect && fieldSchema.options.maxSelect > 1 ? `${RECORD_ID_STRING_NAME}[]` : RECORD_ID_STRING_NAME
user: (fieldSchema) => fieldSchema.maxSelect && fieldSchema.maxSelect > 1 ? `${RECORD_ID_STRING_NAME}[]` : RECORD_ID_STRING_NAME
};
function createTypeField(collectionName, fieldSchema) {
let typeStringOrFunc;
Expand All @@ -208,8 +215,8 @@ function createTypeField(collectionName, fieldSchema) {
const required = fieldSchema.required ? "" : "?";
return ` ${fieldName}${required}: ${typeString}`;
}
function createSelectOptions(recordName, schema) {
const selectFields = schema.filter((field) => field.type === "select");
function createSelectOptions(recordName, fields) {
const selectFields = fields.filter((field) => field.type === "select");
const typestring = selectFields.map(
(field) => `export enum ${getOptionEnumName(recordName, field.name)} {
${getOptionValues(field).map((val) => ` "${getSelectOptionEnumName(val)}" = "${val}",`).join("\n")}
Expand All @@ -234,8 +241,8 @@ function generate(results, options2) {
results.sort((a, b) => a.name <= b.name ? -1 : 1).forEach((row) => {
if (row.name)
collectionNames.push(row.name);
if (row.schema) {
recordTypes.push(createRecordType(row.name, row.schema));
if (row.fields) {
recordTypes.push(createRecordType(row.name, row.fields));
responseTypes.push(createResponseType(row));
}
});
Expand Down Expand Up @@ -270,12 +277,12 @@ ${fields}
}` : "never"}`;
}
function createResponseType(collectionSchemaEntry) {
const { name, schema, type } = collectionSchemaEntry;
const { name, fields, type } = collectionSchemaEntry;
const pascaleName = toPascalCase(name);
const genericArgsWithDefaults = getGenericArgStringWithDefault(schema, {
const genericArgsWithDefaults = getGenericArgStringWithDefault(fields, {
includeExpand: true
});
const genericArgsForRecord = getGenericArgStringForRecord(schema);
const genericArgsForRecord = getGenericArgStringForRecord(fields);
const systemFields = getSystemFields(type);
const expandArgString = `<T${EXPAND_GENERIC_NAME}>`;
return `export type ${pascaleName}Response${genericArgsWithDefaults} = Required<${pascaleName}Record${genericArgsForRecord}> & ${systemFields}${expandArgString}`;
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@
"lint:fix": "npm run lint -- --fix",
"prettier": "prettier src test --check",
"prettier:fix": "npm run prettier -- --write",
"format": "npm run prettier:fix && npm run lint:fix"
"format": "npm run prettier:fix && npm run lint:fix",
"integration:local": "docker build . -t pocketbase-typegen:latest && docker run --name integration_test pocketbase-typegen:latest"
},
"author": "@patmood",
"license": "ISC",
Expand Down
13 changes: 7 additions & 6 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import dotenv from "dotenv"
import type { CollectionRecord, Options } from "./types"
import { fromDatabase, fromJSON, fromURL } from "./schema"


import { generate } from "./lib"
import { saveFile } from "./utils"

Expand All @@ -16,11 +15,13 @@ export async function main(options: Options) {
} else if (options.url) {
schema = await fromURL(options.url, options.email, options.password)
} else if (options.env) {
const path: string = typeof options.env === "string"
? options.env
: ".env"
const path: string = typeof options.env === "string" ? options.env : ".env"
dotenv.config({ path: path })
if (!process.env.PB_TYPEGEN_URL || !process.env.PB_TYPEGEN_EMAIL || !process.env.PB_TYPEGEN_PASSWORD) {
if (
!process.env.PB_TYPEGEN_URL ||
!process.env.PB_TYPEGEN_EMAIL ||
!process.env.PB_TYPEGEN_PASSWORD
) {
return console.error(
"Missing environment variables. Check options: pocketbase-typegen --help"
)
Expand All @@ -36,7 +37,7 @@ export async function main(options: Options) {
)
}
const typeString = generate(schema, {
sdk: options.sdk ?? true
sdk: options.sdk ?? true,
})
await saveFile(options.out, typeString)
return typeString
Expand Down
11 changes: 7 additions & 4 deletions src/collections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,14 @@ ${nameRecordMap}
}`
}

export function createTypedPocketbase(
collectionNames: Array<string>
): string {
export function createTypedPocketbase(collectionNames: Array<string>): string {
const nameRecordMap = collectionNames
.map((name) => `\tcollection(idOrName: '${name}'): RecordService<${toPascalCase(name)}Response>`)
.map(
(name) =>
`\tcollection(idOrName: '${name}'): RecordService<${toPascalCase(
name
)}Response>`
)
.join("\n")
return `export type TypedPocketBase = PocketBase & {
${nameRecordMap}
Expand Down
2 changes: 0 additions & 2 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@ export type ${HTML_STRING_NAME} = string`
export const BASE_SYSTEM_FIELDS_DEFINITION = `// System fields
export type BaseSystemFields<T = never> = {
\tid: ${RECORD_ID_STRING_NAME}
\tcreated: ${DATE_STRING_TYPE_NAME}
\tupdated: ${DATE_STRING_TYPE_NAME}
patmood marked this conversation as resolved.
Show resolved Hide resolved
\tcollectionId: string
\tcollectionName: Collections
\texpand?: T
Expand Down
18 changes: 9 additions & 9 deletions src/fields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,36 +15,36 @@ export const pbSchemaTypescriptMap = {
// Basic fields
bool: "boolean",
date: DATE_STRING_TYPE_NAME,
autodate: DATE_STRING_TYPE_NAME,
editor: HTML_STRING_NAME,
email: "string",
text: "string",
url: "string",
password: "string",
number: "number",

// Dependent on schema
file: (fieldSchema: FieldSchema) =>
fieldSchema.options.maxSelect && fieldSchema.options.maxSelect > 1
? "string[]"
: "string",
fieldSchema.maxSelect && fieldSchema.maxSelect > 1 ? "string[]" : "string",
json: (fieldSchema: FieldSchema) =>
`null | ${fieldNameToGeneric(fieldSchema.name)}`,
relation: (fieldSchema: FieldSchema) =>
fieldSchema.options.maxSelect && fieldSchema.options.maxSelect === 1
fieldSchema.maxSelect && fieldSchema.maxSelect === 1
? RECORD_ID_STRING_NAME
: `${RECORD_ID_STRING_NAME}[]`,
select: (fieldSchema: FieldSchema, collectionName: string) => {
// pocketbase v0.8+ values are required
const valueType = fieldSchema.options.values
const valueType = fieldSchema.values
? getOptionEnumName(collectionName, fieldSchema.name)
: "string"
return fieldSchema.options.maxSelect && fieldSchema.options.maxSelect > 1
return fieldSchema.maxSelect && fieldSchema.maxSelect > 1
? `${valueType}[]`
: valueType
},

// DEPRECATED: PocketBase v0.8 does not have a dedicated user relation
user: (fieldSchema: FieldSchema) =>
fieldSchema.options.maxSelect && fieldSchema.options.maxSelect > 1
fieldSchema.maxSelect && fieldSchema.maxSelect > 1
? `${RECORD_ID_STRING_NAME}[]`
: RECORD_ID_STRING_NAME,
}
Expand Down Expand Up @@ -80,9 +80,9 @@ export function createTypeField(

export function createSelectOptions(
recordName: string,
schema: Array<FieldSchema>
fields: Array<FieldSchema>
): string {
const selectFields = schema.filter((field) => field.type === "select")
const selectFields = fields.filter((field) => field.type === "select")
const typestring = selectFields
.map(
(field) => `export enum ${getOptionEnumName(recordName, field.name)} {
Expand Down
21 changes: 11 additions & 10 deletions src/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@ type GenerateOptions = {
sdk: boolean
}

export function generate(results: Array<CollectionRecord>, options: GenerateOptions): string {
export function generate(
results: Array<CollectionRecord>,
options: GenerateOptions
): string {
const collectionNames: Array<string> = []
const recordTypes: Array<string> = []
const responseTypes: Array<string> = [RESPONSE_TYPE_COMMENT]
Expand All @@ -37,8 +40,8 @@ export function generate(results: Array<CollectionRecord>, options: GenerateOpti
.sort((a, b) => (a.name <= b.name ? -1 : 1))
.forEach((row) => {
if (row.name) collectionNames.push(row.name)
if (row.schema) {
recordTypes.push(createRecordType(row.name, row.schema))
if (row.fields) {
recordTypes.push(createRecordType(row.name, row.fields))
responseTypes.push(createResponseType(row))
}
})
Expand All @@ -58,12 +61,10 @@ export function generate(results: Array<CollectionRecord>, options: GenerateOpti
createCollectionRecords(sortedCollectionNames),
createCollectionResponses(sortedCollectionNames),
options.sdk && TYPED_POCKETBASE_COMMENT,
options.sdk && createTypedPocketbase(sortedCollectionNames)
options.sdk && createTypedPocketbase(sortedCollectionNames),
]

return fileParts
.filter(Boolean)
.join("\n\n") + '\n'
return fileParts.filter(Boolean).join("\n\n") + "\n"
}

export function createRecordType(
Expand Down Expand Up @@ -92,12 +93,12 @@ ${fields}
export function createResponseType(
collectionSchemaEntry: CollectionRecord
): string {
const { name, schema, type } = collectionSchemaEntry
const { name, fields, type } = collectionSchemaEntry
const pascaleName = toPascalCase(name)
const genericArgsWithDefaults = getGenericArgStringWithDefault(schema, {
const genericArgsWithDefaults = getGenericArgStringWithDefault(fields, {
includeExpand: true,
})
const genericArgsForRecord = getGenericArgStringForRecord(schema)
const genericArgsForRecord = getGenericArgStringForRecord(fields)
const systemFields = getSystemFields(type)
const expandArgString = `<T${EXPAND_GENERIC_NAME}>`

Expand Down
16 changes: 10 additions & 6 deletions src/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,11 @@ export async function fromDatabase(
driver: sqlite3.Database,
filename: dbPath,
})

const result = await db.all("SELECT * FROM _collections")
return result.map((collection) => ({
...collection,
schema: JSON.parse(collection.schema),
fields: JSON.parse(collection.fields),
}))
}

Expand All @@ -35,11 +36,14 @@ export async function fromURL(
let collections: Array<CollectionRecord> = []
try {
// Login
const { token } = await fetch(`${url}/api/admins/auth-with-password`, {
// @ts-ignore
body: formData,
method: "post",
}).then((res) => {
const { token } = await fetch(
`${url}/api/collections/_superusers/auth-with-password`,
{
// @ts-ignore
body: formData,
method: "post",
}
).then((res) => {
if (!res.ok) throw res
return res.json()
})
Expand Down
Loading