Skip to content

Commit

Permalink
Support PocketBase version 0.23.0-rc12 (#103)
Browse files Browse the repository at this point in the history
* feat(*): upgrade to schema version 0.23.0 of pocketbase

* test(*): update all snapshots and test-db/schema

* test(Dockerfile): use pocketbase version 0.23.0-rc12 for integration tests

* refactor(constants): remove updated/created fields from baserecords

PocketBase no longer adds those fields by default.

* feat(*): use new _superusers collection for url generation

* refactor(test): remove auxiliary.db

* docs(readme): update target version and add npx command example with version

* test(Dockerfile): upgrade rc version to 0.23.0-rc13

* test(*): fix integration tests to be compatible with 0.23.0-rc14

* refactor(Dockerfile): upgrade to 0.23.1
  • Loading branch information
skowrons authored Nov 27, 2024
1 parent b50cdb8 commit 8744267
Show file tree
Hide file tree
Showing 24 changed files with 26,024 additions and 437 deletions.
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 pocketbase-typegen@1.2.1 --db ./pb_data/data.db --out pocketbase-types.ts |
| v0.7.x | v1.0.x | npx pocketbase-typegen@1.0.13 --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}
\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

0 comments on commit 8744267

Please sign in to comment.