Skip to content

Support PocketBase version 0.23.0-rc12 #103

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

Merged
merged 10 commits into from
Nov 27, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# 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.0-rc12

WORKDIR /app/output/
WORKDIR /app/
Expand Down
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ 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

Users of PocketBase < v0.7.x should use `pocketbase-typegen` v1.0.x
| PocketBase | pocketbase-typegen |
|------------|--------------------|
| v0.23.x | v2.0.x |
| v0.8.x | v1.1.x |
| v0.7.x | v1.0.x |

## 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.

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
6 changes: 3 additions & 3 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export type FieldSchema = {
| "email"
| "url"
| "date"
| "autodate"
| "select"
| "json"
| "relation"
Expand All @@ -29,20 +30,19 @@ export type FieldSchema = {
system: boolean
required: boolean
unique: boolean
options: RecordOptions
}
} & RecordOptions

export type CollectionRecord = {
id: string
type: "base" | "auth" | "view"
name: string
system: boolean
fields: FieldSchema[]
listRule: string | null
viewRule: string | null
createRule: string | null
updateRule: string | null
deleteRule: string | null
schema: Array<FieldSchema>
}

// Every field is optional
Expand Down
Loading
Loading