Support PocketBase v0.8 (#19)

* add v0.8 schema

* update system fields to handle base and auth collection types

* handle relation fields with multiple items

* create enums for select fields that have values

* additional typecheck

* add user field for backwards compatability

* refactor strings

* refactor system types

* readme

* Add lint and formatting (#17)

* add linting and formatting

* lint codebase

* prettier

* update test workflow

* update test workflow again

* fix: url login for pocketbase 0.8.0-rc2 servers (#16)

Co-authored-by: Ethan Olsen <ethan@crowdhubapps.com>

* e2e integration test (#18)

* add dockerfile to run e2e tests

* add db typegen

* cleanup

* add test

* add github workflow

* remove interactive flag

* intentionally fail integration test

* save artifacts in case of failing tests

* fix output dir

* ignore files

Co-authored-by: Ethan Olsen <46045126+o2dependent@users.noreply.github.com>
Co-authored-by: Ethan Olsen <ethan@crowdhubapps.com>
This commit is contained in:
Patrick Moody
2022-11-18 21:06:55 -08:00
committed by GitHub
parent a9c8156f99
commit d04a445072
26 changed files with 2942 additions and 410 deletions

22
.eslintrc.json Normal file
View File

@@ -0,0 +1,22 @@
{
"env": {
"browser": true,
"es2021": true
},
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"eslint-config-prettier"
],
"overrides": [],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
},
"plugins": ["@typescript-eslint"],
"rules": {
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/ban-ts-comment": "off"
}
}

32
.github/workflows/integration.yml vendored Normal file
View File

@@ -0,0 +1,32 @@
name: Integration Test
on:
push:
branches: [main, rc]
pull_request:
branches: "**"
jobs:
test:
timeout-minutes: 5
runs-on: ubuntu-latest
env:
CI: true
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Run in docker
run: |
docker build . -t pocketbase-typegen:latest
docker run --name integration_test pocketbase-typegen:latest
mkdir -p output
docker cp integration_test:/app/output output
- name: Archive generated type results
uses: actions/upload-artifact@v3
with:
name: generated-types
path: output/*
retention-days: 5

View File

@@ -2,9 +2,9 @@ name: Test
on:
push:
branches: [ main ]
branches: [main, rc]
pull_request:
branches: [ main ]
branches: "**"
jobs:
test:
@@ -17,11 +17,11 @@ jobs:
node-version: [16.x]
steps:
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
- run: npm run build
- run: npm test
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
- run: npm run build
- run: npm test

4
.prettierignore Normal file
View File

@@ -0,0 +1,4 @@
dist
coverage
test/pocketbase-types-example.ts
pocketbase-types.ts

33
Dockerfile Normal file
View File

@@ -0,0 +1,33 @@
# Dockerfile to run e2e integration tests against a test PocketBase server
FROM node:16-alpine3.16
ARG POCKETBASE_VERSION=0.8.0-rc2
WORKDIR /app/output/
WORKDIR /app/
# Install the dependencies
RUN apk add --no-cache \
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
RUN unzip /tmp/pocketbase.zip -d /app/
COPY package.json package-lock.json ./
RUN npm ci
# Copy test files
COPY test/integration ./
COPY test/pocketbase-types-example.ts ./
COPY dist/index.js ./dist/index.js
RUN chmod +x ./pocketbase
RUN chmod +x ./run.sh
EXPOSE 8090
CMD [ "./run.sh" ]

View File

@@ -8,6 +8,12 @@ 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
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
## Usage
```
@@ -38,11 +44,18 @@ URL example:
The output is a typescript file `pocketbase-types.ts` ([example](./test/pocketbase-types-example.ts)) which will contain:
- 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
- `Collections` An enum of all collections/
- `[CollectionName]Record` One type for each collection (eg ProfilesRecord)/
- `[CollectionName]Response` One response type for each collection (eg ProfilesResponse) which includes system fields. This is what is returned from the PocketBase API.
- `[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
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<Task>("RECORD_ID") // -> results in Promise<Task>`
```typescript
import { Collections, TasksResponse } from "./pocketbase-types"
pb.collection(Collections.Tasks).getOne<TasksResponse>("RECORD_ID") // -> results in Promise<TaskResponse>
```

148
dist/index.js vendored
View File

@@ -23,36 +23,46 @@ async function fromJSON(path) {
}
async function fromURL(url, email = "", password = "") {
const formData = new FormData();
formData.append("email", email);
formData.append("identity", email);
formData.append("password", password);
const { token } = await fetch(`${url}/api/admins/auth-via-email`, {
const { token } = await fetch(`${url}/api/admins/auth-with-password`, {
method: "post",
body: formData
}).then((res) => res.json());
const result = await fetch(`${url}/api/collections?perPage=200`, {
headers: {
Authorization: `Admin ${token}`
Authorization: token
}
}).then((res) => res.json());
return result.items;
}
// src/constants.ts
var EXPORT_COMMENT = `// This file was @generated using pocketbase-typegen`;
var EXPORT_COMMENT = `/**
* This file was @generated using pocketbase-typegen
*/`;
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 DATE_STRING_TYPE_NAME = `IsoDateString`;
var DATE_STRING_TYPE_DEFINITION = `export type ${DATE_STRING_TYPE_NAME} = string`;
var RECORD_ID_STRING_NAME = `RecordIdString`;
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 = {
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 = {
id: ${RECORD_ID_STRING_NAME}
created: ${DATE_STRING_TYPE_NAME}
updated: ${DATE_STRING_TYPE_NAME}
"@collectionId": string
"@collectionName": string
"@expand"?: { [key: string]: any }
collectionId: string
collectionName: Collections
expand?: { [key: string]: any }
}`;
var AUTH_SYSTEM_FIELDS_DEFINITION = `export type AuthSystemFields = {
email: string
emailVisibility: boolean
username: string
verified: boolean
} & BaseSystemFields`;
// src/generics.ts
function fieldNameToGeneric(name) {
@@ -93,6 +103,12 @@ async function saveFile(outPath, typeString) {
await fs2.writeFile(outPath, typeString, "utf8");
console.log(`Created typescript definitions at ${outPath}`);
}
function getSystemFields(type) {
return type === "auth" ? "AuthSystemFields" : "BaseSystemFields";
}
function getOptionEnumName(recordName, fieldName) {
return `${toPascalCase(recordName)}${toPascalCase(fieldName)}Options`;
}
// src/lib.ts
var pbSchemaTypescriptMap = {
@@ -102,82 +118,98 @@ var pbSchemaTypescriptMap = {
email: "string",
url: "string",
date: DATE_STRING_TYPE_NAME,
select: (fieldSchema) => fieldSchema.options.values ? fieldSchema.options.values.map((val) => `"${val}"`).join(" | ") : "string",
select: (fieldSchema, collectionName) => fieldSchema.options.values ? getOptionEnumName(collectionName, fieldSchema.name) : "string",
json: (fieldSchema) => `null | ${fieldNameToGeneric(fieldSchema.name)}`,
file: (fieldSchema) => fieldSchema.options.maxSelect && fieldSchema.options.maxSelect > 1 ? "string[]" : "string",
relation: RECORD_ID_STRING_NAME,
user: USER_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) {
const collectionNames = [];
const recordTypes = [];
results.forEach((row) => {
const responseTypes = [RESPONSE_TYPE_COMMENT];
results.sort((a, b) => {
if (a.name < b.name) {
return -1;
}
if (a.name > b.name) {
return 1;
}
return 0;
}).forEach((row) => {
if (row.name)
collectionNames.push(row.name);
if (row.schema) {
recordTypes.push(createRecordType(row.name, row.schema));
recordTypes.push(createResponseType(row.name, row.schema));
responseTypes.push(createResponseType(row));
}
});
const sortedCollectionNames = collectionNames.sort();
const sortedCollectionNames = collectionNames;
const fileParts = [
EXPORT_COMMENT,
DATE_STRING_TYPE_DEFINITION,
RECORD_ID_STRING_DEFINITION,
USER_ID_STRING_DEFINITION,
BASE_RECORD_DEFINITION,
createCollectionEnum(sortedCollectionNames),
...recordTypes.sort(),
createCollectionRecord(sortedCollectionNames)
ALIAS_TYPE_DEFINITIONS,
BASE_SYSTEM_FIELDS_DEFINITION,
AUTH_SYSTEM_FIELDS_DEFINITION,
RECORD_TYPE_COMMENT,
...recordTypes,
responseTypes.join("\n"),
createCollectionRecords(sortedCollectionNames)
];
return fileParts.join("\n\n");
}
function createCollectionEnum(collectionNames) {
let typeString = `export enum Collections {
`;
collectionNames.forEach((name) => {
typeString += ` ${toPascalCase(name)} = "${name}",
`;
});
typeString += `}`;
const collections = collectionNames.map((name) => ` ${toPascalCase(name)} = "${name}",`).join("\n");
const typeString = `export enum Collections {
${collections}
}`;
return typeString;
}
function createCollectionRecord(collectionNames) {
let typeString = `export type CollectionRecords = {
`;
collectionNames.forEach((name) => {
typeString += ` ${name}: ${toPascalCase(name)}Record
`;
});
typeString += `}`;
return typeString;
function createCollectionRecords(collectionNames) {
const nameRecordMap = collectionNames.map((name) => ` ${name}: ${toPascalCase(name)}Record`).join("\n");
return `export type CollectionRecords = {
${nameRecordMap}
}`;
}
function createRecordType(name, schema) {
let typeString = `export type ${toPascalCase(
name
)}Record${getGenericArgStringWithDefault(schema)} = {
`;
schema.forEach((fieldSchema) => {
typeString += createTypeField(fieldSchema);
});
typeString += `}`;
return typeString;
const selectOptionEnums = createSelectOptions(name, schema);
const typeName = toPascalCase(name);
const genericArgs = getGenericArgStringWithDefault(schema);
const fields = schema.map((fieldSchema) => createTypeField(name, fieldSchema)).join("\n");
return `${selectOptionEnums}export type ${typeName}Record${genericArgs} = {
${fields}
}`;
}
function createResponseType(name, schema) {
function createResponseType(collectionSchemaEntry) {
const { name, schema, type } = collectionSchemaEntry;
const pascaleName = toPascalCase(name);
let typeString = `export type ${pascaleName}Response${getGenericArgStringWithDefault(
schema
)} = ${pascaleName}Record${getGenericArgString(schema)} & BaseRecord`;
return typeString;
const genericArgsWithDefaults = getGenericArgStringWithDefault(schema);
const genericArgs = getGenericArgString(schema);
const systemFields = getSystemFields(type);
return `export type ${pascaleName}Response${genericArgsWithDefaults} = ${pascaleName}Record${genericArgs} & ${systemFields}`;
}
function createTypeField(fieldSchema) {
function createTypeField(collectionName, fieldSchema) {
if (!(fieldSchema.type in pbSchemaTypescriptMap)) {
throw new Error(`unknown type ${fieldSchema.type} found in schema`);
}
const typeStringOrFunc = pbSchemaTypescriptMap[fieldSchema.type];
const typeString = typeof typeStringOrFunc === "function" ? typeStringOrFunc(fieldSchema) : typeStringOrFunc;
return ` ${sanitizeFieldName(fieldSchema.name)}${fieldSchema.required ? "" : "?"}: ${typeString}
const typeString = typeof typeStringOrFunc === "function" ? typeStringOrFunc(fieldSchema, collectionName) : typeStringOrFunc;
const fieldName = sanitizeFieldName(fieldSchema.name);
const required = fieldSchema.required ? "" : "?";
return ` ${fieldName}${required}: ${typeString}`;
}
function createSelectOptions(recordName, schema) {
const selectFields = schema.filter((field) => field.type === "select");
const typestring = selectFields.map(
(field) => {
var _a;
return `export enum ${getOptionEnumName(recordName, field.name)} {
${(_a = field.options.values) == null ? void 0 : _a.map((val) => ` ${val} = "${val}",`).join("\n")}
}
`;
}
).join("\n");
return typestring;
}
// src/cli.ts
@@ -203,7 +235,7 @@ async function main(options2) {
import { program } from "commander";
// package.json
var version = "1.0.12";
var version = "1.1.0";
// src/index.ts
program.name("Pocketbase Typegen").version(version).description(

1985
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "pocketbase-typegen",
"version": "1.0.13",
"version": "1.1.0",
"description": "Generate pocketbase record types from your database",
"main": "dist/index.js",
"bin": {
@@ -23,7 +23,12 @@
"test:update": "jest -u",
"build": "rm -rf dist && node build.js",
"prepublishOnly": "tsc && npm run test && npm run build",
"typecheck": "tsc"
"typecheck": "tsc",
"lint": "eslint src test",
"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"
},
"author": "@patmood",
"license": "ISC",
@@ -37,14 +42,30 @@
"devDependencies": {
"@types/jest": "^29.1.2",
"@types/node": "^18.8.3",
"@typescript-eslint/eslint-plugin": "^5.42.1",
"@typescript-eslint/parser": "^5.42.1",
"esbuild": "^0.15.11",
"esbuild-node-externals": "^1.5.0",
"eslint": "^8.27.0",
"eslint-config-prettier": "^8.5.0",
"jest": "^29.1.2",
"prettier": "^2.7.1",
"ts-jest": "^29.0.3",
"tslint-config-prettier": "^1.18.0",
"typescript": "^4.8.4"
},
"jest": {
"preset": "ts-jest",
"testEnvironment": "node"
"testEnvironment": "node",
"modulePathIgnorePatterns": [
"<rootDir>/dist/",
"<rootDir>/test/pocketbase-types-examples.ts"
]
},
"prettier": {
"trailingComma": "es5",
"tabWidth": 2,
"semi": false,
"singleQuote": false
}
}

View File

@@ -1,15 +1,27 @@
export const EXPORT_COMMENT = `// This file was @generated using pocketbase-typegen`
export const EXPORT_COMMENT = `/**
* This file was @generated using pocketbase-typegen
*/`
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 DATE_STRING_TYPE_NAME = `IsoDateString`
export const DATE_STRING_TYPE_DEFINITION = `export type ${DATE_STRING_TYPE_NAME} = string`
export const RECORD_ID_STRING_NAME = `RecordIdString`
export const RECORD_ID_STRING_DEFINITION = `export type ${RECORD_ID_STRING_NAME} = string`
export const USER_ID_STRING_NAME = `UserIdString`
export const USER_ID_STRING_DEFINITION = `export type ${USER_ID_STRING_NAME} = string`
export const BASE_RECORD_DEFINITION = `export type BaseRecord = {
export const ALIAS_TYPE_DEFINITIONS = `// Alias types for improved usability
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 = {
\tid: ${RECORD_ID_STRING_NAME}
\tcreated: ${DATE_STRING_TYPE_NAME}
\tupdated: ${DATE_STRING_TYPE_NAME}
\t"@collectionId": string
\t"@collectionName": string
\t"@expand"?: { [key: string]: any }
\tcollectionId: string
\tcollectionName: Collections
\texpand?: { [key: string]: any }
}`
export const AUTH_SYSTEM_FIELDS_DEFINITION = `export type AuthSystemFields = {
\temail: string
\temailVisibility: boolean
\tusername: string
\tverified: boolean
} & BaseSystemFields`

View File

@@ -1,12 +1,12 @@
import {
BASE_RECORD_DEFINITION,
DATE_STRING_TYPE_DEFINITION,
ALIAS_TYPE_DEFINITIONS,
AUTH_SYSTEM_FIELDS_DEFINITION,
BASE_SYSTEM_FIELDS_DEFINITION,
DATE_STRING_TYPE_NAME,
EXPORT_COMMENT,
RECORD_ID_STRING_DEFINITION,
RECORD_ID_STRING_NAME,
USER_ID_STRING_DEFINITION,
USER_ID_STRING_NAME,
RECORD_TYPE_COMMENT,
RESPONSE_TYPE_COMMENT,
} from "./constants"
import { CollectionRecord, FieldSchema } from "./types"
import {
@@ -14,7 +14,12 @@ import {
getGenericArgString,
getGenericArgStringWithDefault,
} from "./generics"
import { sanitizeFieldName, toPascalCase } from "./utils"
import {
getOptionEnumName,
getSystemFields,
sanitizeFieldName,
toPascalCase,
} from "./utils"
const pbSchemaTypescriptMap = {
text: "string",
@@ -23,9 +28,9 @@ const pbSchemaTypescriptMap = {
email: "string",
url: "string",
date: DATE_STRING_TYPE_NAME,
select: (fieldSchema: FieldSchema) =>
select: (fieldSchema: FieldSchema, collectionName: string) =>
fieldSchema.options.values
? fieldSchema.options.values.map((val) => `"${val}"`).join(" | ")
? getOptionEnumName(collectionName, fieldSchema.name)
: "string",
json: (fieldSchema: FieldSchema) =>
`null | ${fieldNameToGeneric(fieldSchema.name)}`,
@@ -33,78 +38,105 @@ const pbSchemaTypescriptMap = {
fieldSchema.options.maxSelect && fieldSchema.options.maxSelect > 1
? "string[]"
: "string",
relation: RECORD_ID_STRING_NAME,
user: USER_ID_STRING_NAME,
relation: (fieldSchema: FieldSchema) =>
fieldSchema.options.maxSelect && fieldSchema.options.maxSelect > 1
? `${RECORD_ID_STRING_NAME}[]`
: RECORD_ID_STRING_NAME,
// DEPRECATED: PocketBase v0.8 does not have a dedicated user relation
user: (fieldSchema: FieldSchema) =>
fieldSchema.options.maxSelect && fieldSchema.options.maxSelect > 1
? `${RECORD_ID_STRING_NAME}[]`
: RECORD_ID_STRING_NAME,
}
export function generate(results: Array<CollectionRecord>) {
const collectionNames: Array<string> = []
const recordTypes: Array<string> = []
const responseTypes: Array<string> = [RESPONSE_TYPE_COMMENT]
results.forEach((row) => {
if (row.name) collectionNames.push(row.name)
if (row.schema) {
recordTypes.push(createRecordType(row.name, row.schema))
recordTypes.push(createResponseType(row.name, row.schema))
}
})
const sortedCollectionNames = collectionNames.sort()
results
.sort((a, b) => {
if (a.name < b.name) {
return -1
}
if (a.name > b.name) {
return 1
}
return 0
})
.forEach((row) => {
if (row.name) collectionNames.push(row.name)
if (row.schema) {
recordTypes.push(createRecordType(row.name, row.schema))
responseTypes.push(createResponseType(row))
}
})
const sortedCollectionNames = collectionNames
const fileParts = [
EXPORT_COMMENT,
DATE_STRING_TYPE_DEFINITION,
RECORD_ID_STRING_DEFINITION,
USER_ID_STRING_DEFINITION,
BASE_RECORD_DEFINITION,
createCollectionEnum(sortedCollectionNames),
...recordTypes.sort(),
createCollectionRecord(sortedCollectionNames),
ALIAS_TYPE_DEFINITIONS,
BASE_SYSTEM_FIELDS_DEFINITION,
AUTH_SYSTEM_FIELDS_DEFINITION,
RECORD_TYPE_COMMENT,
...recordTypes,
responseTypes.join("\n"),
createCollectionRecords(sortedCollectionNames),
]
return fileParts.join("\n\n")
}
export function createCollectionEnum(collectionNames: Array<string>) {
let typeString = `export enum Collections {\n`
collectionNames.forEach((name) => {
typeString += `\t${toPascalCase(name)} = "${name}",\n`
})
typeString += `}`
const collections = collectionNames
.map((name) => `\t${toPascalCase(name)} = "${name}",`)
.join("\n")
const typeString = `export enum Collections {
${collections}
}`
return typeString
}
export function createCollectionRecord(collectionNames: Array<string>) {
let typeString = `export type CollectionRecords = {\n`
collectionNames.forEach((name) => {
typeString += `\t${name}: ${toPascalCase(name)}Record\n`
})
typeString += `}`
return typeString
export function createCollectionRecords(collectionNames: Array<string>) {
const nameRecordMap = collectionNames
.map((name) => `\t${name}: ${toPascalCase(name)}Record`)
.join("\n")
return `export type CollectionRecords = {
${nameRecordMap}
}`
}
export function createRecordType(
name: string,
schema: Array<FieldSchema>
): string {
let typeString = `export type ${toPascalCase(
name
)}Record${getGenericArgStringWithDefault(schema)} = {\n`
schema.forEach((fieldSchema: FieldSchema) => {
typeString += createTypeField(fieldSchema)
})
typeString += `}`
return typeString
const selectOptionEnums = createSelectOptions(name, schema)
const typeName = toPascalCase(name)
const genericArgs = getGenericArgStringWithDefault(schema)
const fields = schema
.map((fieldSchema: FieldSchema) => createTypeField(name, fieldSchema))
.join("\n")
return `${selectOptionEnums}export type ${typeName}Record${genericArgs} = {
${fields}
}`
}
export function createResponseType(name: string, schema: Array<FieldSchema>) {
export function createResponseType(collectionSchemaEntry: CollectionRecord) {
const { name, schema, type } = collectionSchemaEntry
const pascaleName = toPascalCase(name)
let typeString = `export type ${pascaleName}Response${getGenericArgStringWithDefault(
schema
)} = ${pascaleName}Record${getGenericArgString(schema)} & BaseRecord`
return typeString
const genericArgsWithDefaults = getGenericArgStringWithDefault(schema)
const genericArgs = getGenericArgString(schema)
const systemFields = getSystemFields(type)
return `export type ${pascaleName}Response${genericArgsWithDefaults} = ${pascaleName}Record${genericArgs} & ${systemFields}`
}
export function createTypeField(fieldSchema: FieldSchema) {
export function createTypeField(
collectionName: string,
fieldSchema: FieldSchema
) {
if (!(fieldSchema.type in pbSchemaTypescriptMap)) {
throw new Error(`unknown type ${fieldSchema.type} found in schema`)
}
@@ -115,9 +147,26 @@ export function createTypeField(fieldSchema: FieldSchema) {
const typeString =
typeof typeStringOrFunc === "function"
? typeStringOrFunc(fieldSchema)
? typeStringOrFunc(fieldSchema, collectionName)
: typeStringOrFunc
return `\t${sanitizeFieldName(fieldSchema.name)}${
fieldSchema.required ? "" : "?"
}: ${typeString}\n`
const fieldName = sanitizeFieldName(fieldSchema.name)
const required = fieldSchema.required ? "" : "?"
return `\t${fieldName}${required}: ${typeString}`
}
export function createSelectOptions(
recordName: string,
schema: Array<FieldSchema>
) {
const selectFields = schema.filter((field) => field.type === "select")
const typestring = selectFields
.map(
(field) => `export enum ${getOptionEnumName(recordName, field.name)} {
${field.options.values?.map((val) => `\t${val} = "${val}",`).join("\n")}
}\n`
)
.join("\n")
return typestring
}

View File

@@ -26,15 +26,15 @@ export async function fromJSON(path: string): Promise<Array<CollectionRecord>> {
export async function fromURL(
url: string,
email: string = "",
password: string = ""
email = "",
password = ""
): Promise<Array<CollectionRecord>> {
const formData = new FormData()
formData.append("email", email)
formData.append("identity", email)
formData.append("password", password)
// Login
const { token } = await fetch(`${url}/api/admins/auth-via-email`, {
const { token } = await fetch(`${url}/api/admins/auth-with-password`, {
method: "post",
// @ts-ignore
body: formData,
@@ -43,7 +43,7 @@ export async function fromURL(
// Get the collection
const result = await fetch(`${url}/api/collections?perPage=200`, {
headers: {
Authorization: `Admin ${token}`,
Authorization: token,
},
}).then((res) => res.json())

View File

@@ -31,6 +31,7 @@ export type FieldSchema = {
export type CollectionRecord = {
id: string
type: "base" | "auth"
name: string
system: boolean
listRule: string | null

View File

@@ -1,3 +1,4 @@
import { CollectionRecord } from "./types"
import { promises as fs } from "fs"
export function toPascalCase(str: string) {
@@ -22,3 +23,11 @@ export async function saveFile(outPath: string, typeString: string) {
await fs.writeFile(outPath, typeString, "utf8")
console.log(`Created typescript definitions at ${outPath}`)
}
export function getSystemFields(type: CollectionRecord["type"]) {
return type === "auth" ? "AuthSystemFields" : "BaseSystemFields"
}
export function getOptionEnumName(recordName: string, fieldName: string) {
return `${toPascalCase(recordName)}${toPascalCase(fieldName)}Options`
}

View File

@@ -0,0 +1,96 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`creates a type file from json schema 1`] = `
"/**
* This file was @generated using pocketbase-typegen
*/
export enum Collections {
Base = "base",
CustomAuth = "custom_auth",
Everything = "everything",
Posts = "posts",
Users = "users",
}
// Alias types for improved usability
export type IsoDateString = string
export type RecordIdString = string
// System fields
export type BaseSystemFields = {
id: RecordIdString
created: IsoDateString
updated: IsoDateString
collectionId: string
collectionName: Collections
expand?: { [key: string]: any }
}
export type AuthSystemFields = {
email: string
emailVisibility: boolean
username: string
verified: boolean
} & BaseSystemFields
// Record types for each collection
export type BaseRecord = {
field?: string
}
export type CustomAuthRecord = {
custom_field?: string
}
export enum EverythingSelectFieldOptions {
optionA = "optionA",
optionB = "optionB",
optionC = "optionC",
}
export type EverythingRecord<Tanother_json_field = unknown, Tjson_field = unknown> = {
text_field?: string
number_field?: number
bool_field?: boolean
email_field?: string
url_field?: string
date_field?: IsoDateString
select_field?: EverythingSelectFieldOptions
json_field?: null | Tjson_field
another_json_field?: null | Tanother_json_field
file_field?: string
three_files_field?: string[]
user_relation_field?: RecordIdString
custom_relation_field?: RecordIdString[]
post_relation_field?: RecordIdString
select_field_no_values?: string
}
export type PostsRecord = {
field?: string
nonempty_field: string
nonempty_bool: boolean
field1?: number
}
export type UsersRecord = {
name?: string
avatar?: string
}
// 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<Tanother_json_field = unknown, Tjson_field = unknown> = EverythingRecord<Tanother_json_field, Tjson_field> & BaseSystemFields
export type PostsResponse = PostsRecord & BaseSystemFields
export type UsersResponse = UsersRecord & AuthSystemFields
export type CollectionRecords = {
base: BaseRecord
custom_auth: CustomAuthRecord
everything: EverythingRecord
posts: PostsRecord
users: UsersRecord
}"
`;

View File

@@ -1,65 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`creates a type file from json schema 1`] = `
"// This file was @generated using pocketbase-typegen
export type IsoDateString = string
export type RecordIdString = string
export type UserIdString = string
export type BaseRecord = {
id: RecordIdString
created: IsoDateString
updated: IsoDateString
"@collectionId": string
"@collectionName": string
"@expand"?: { [key: string]: any }
}
export enum Collections {
EveryType = "every_type",
Orders = "orders",
Profiles = "profiles",
}
export type EveryTypeRecord<Tjson_field = unknown> = {
text_field: string
number_field: number
bool_field: boolean
email_field?: string
url_field?: string
date_field?: IsoDateString
select_field?: "optionA" | "optionB" | "optionC"
json_field?: null | Tjson_field
file_field?: string
relation_field?: RecordIdString
user_field?: UserIdString
}
export type EveryTypeResponse<Tjson_field = unknown> = EveryTypeRecord<Tjson_field> & BaseRecord
export type OrdersRecord = {
amount: number
payment_type: "credit card" | "paypal" | "crypto"
user: UserIdString
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
profiles: ProfilesRecord
}"
`;

View File

@@ -26,7 +26,7 @@ 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 creates type definition for a response 1`] = `"export type BooksResponse = BooksRecord & BaseSystemFields"`;
exports[`createResponseType handles file fields with multiple files 1`] = `
"export type BooksRecord = {
@@ -35,32 +35,43 @@ exports[`createResponseType handles file fields with multiple files 1`] = `
`;
exports[`generate generates correct output given db input 1`] = `
"// This file was @generated using pocketbase-typegen
export type IsoDateString = string
export type RecordIdString = string
export type UserIdString = string
export type BaseRecord = {
id: RecordIdString
created: IsoDateString
updated: IsoDateString
"@collectionId": string
"@collectionName": string
"@expand"?: { [key: string]: any }
}
"/**
* This file was @generated using pocketbase-typegen
*/
export enum Collections {
Books = "books",
}
// Alias types for improved usability
export type IsoDateString = string
export type RecordIdString = string
// System fields
export type BaseSystemFields = {
id: RecordIdString
created: IsoDateString
updated: IsoDateString
collectionId: string
collectionName: Collections
expand?: { [key: string]: any }
}
export type AuthSystemFields = {
email: string
emailVisibility: boolean
username: string
verified: boolean
} & BaseSystemFields
// Record types for each collection
export type BooksRecord = {
title?: string
}
export type BooksResponse = BooksRecord & BaseRecord
// Response types include system fields and match responses from the PocketBase API
export type BooksResponse = BooksRecord & BaseSystemFields
export type CollectionRecords = {
books: BooksRecord

View File

@@ -0,0 +1,24 @@
import assert from "node:assert"
import fs from "fs/promises"
// Known good types from repo
const controlTypes = await fs.readFile("pocketbase-types-example.ts", {
encoding: "utf8",
})
async function testCreateFromUrl() {
const typesFromUrl = await fs.readFile("output/pocketbase-types-url.ts", {
encoding: "utf8",
})
assert.equal(typesFromUrl, controlTypes)
}
async function testCreateFromDb() {
const typesFromDb = await fs.readFile("output/pocketbase-types-db.ts", {
encoding: "utf8",
})
assert.equal(typesFromDb, controlTypes)
}
await testCreateFromUrl()
await testCreateFromDb()

Binary file not shown.

14
test/integration/run.sh Normal file
View File

@@ -0,0 +1,14 @@
#!/bin/sh
echo "Starting integration test."
# Start pocketbase server
/app/pocketbase serve --http=0.0.0.0:8090 &
echo "Waiting for server to start."
while ! nc -z localhost 8090 </dev/null; do sleep 1; done
node ./dist/index.js --url http://0.0.0.0:8090 --email test@test.com --password testpassword --out output/pocketbase-types-url.ts
node ./dist/index.js --db pb_data/data.db --out output/pocketbase-types-db.ts
node integration.js
echo "Integration tests pass"

View File

@@ -1,7 +1,7 @@
import { CollectionRecord, FieldSchema } from "../src/types"
import {
createCollectionEnum,
createCollectionRecord,
createCollectionRecords,
createRecordType,
createResponseType,
createTypeField,
@@ -24,6 +24,7 @@ describe("generate", () => {
{
name: "books",
id: "123",
type: "base",
system: false,
listRule: null,
viewRule: null,
@@ -58,7 +59,7 @@ describe("createCollectionEnum", () => {
describe("createCollectionRecord", () => {
it("creates mapping of collection name to record type", () => {
const names = ["book", "magazine"]
expect(createCollectionRecord(names)).toMatchSnapshot()
expect(createCollectionRecords(names)).toMatchSnapshot()
})
})
@@ -100,19 +101,30 @@ 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)
const row: CollectionRecord = {
type: "base",
id: "123",
system: false,
listRule: null,
viewRule: null,
createRule: null,
updateRule: null,
deleteRule: null,
name: "books",
schema: [
{
system: false,
id: "hhnwjkke",
name: "title",
type: "text",
required: false,
unique: false,
options: { min: null, max: null, pattern: "" },
},
],
}
const result = createResponseType(row)
expect(result).toMatchSnapshot()
})
@@ -137,75 +149,96 @@ describe("createResponseType", () => {
describe("createTypeField", () => {
it("handles required and optional fields", () => {
expect(
createTypeField({
createTypeField("test_collection", {
...defaultFieldSchema,
required: false,
})
).toEqual("\tdefaultName?: string\n")
).toEqual("\tdefaultName?: string")
expect(
createTypeField({
createTypeField("test_collection", {
...defaultFieldSchema,
required: true,
})
).toEqual("\tdefaultName: string\n")
).toEqual("\tdefaultName: string")
})
it("converts pocketbase schema types to typescript", () => {
it("converts default types to typescript", () => {
expect(
createTypeField({
createTypeField("test_collection", {
...defaultFieldSchema,
})
).toEqual("\tdefaultName: string\n")
).toEqual("\tdefaultName: string")
expect(
createTypeField({
createTypeField("test_collection", {
...defaultFieldSchema,
name: "textField",
})
).toEqual("\ttextField: string\n")
).toEqual("\ttextField: string")
})
it("converts number type", () => {
expect(
createTypeField({
createTypeField("test_collection", {
...defaultFieldSchema,
name: "numberField",
type: "number",
})
).toEqual("\tnumberField: number\n")
).toEqual("\tnumberField: number")
})
it("converts bool type", () => {
expect(
createTypeField({
createTypeField("test_collection", {
...defaultFieldSchema,
name: "boolField",
type: "bool",
})
).toEqual("\tboolField: boolean\n")
).toEqual("\tboolField: boolean")
})
it("converts email type", () => {
expect(
createTypeField({
createTypeField("test_collection", {
...defaultFieldSchema,
name: "emailField",
type: "email",
})
).toEqual("\temailField: string\n")
).toEqual("\temailField: string")
})
it("converts url type", () => {
expect(
createTypeField({
createTypeField("test_collection", {
...defaultFieldSchema,
name: "urlField",
type: "url",
})
).toEqual("\turlField: string\n")
).toEqual("\turlField: string")
})
it("converts date type", () => {
expect(
createTypeField({
createTypeField("test_collection", {
...defaultFieldSchema,
name: "dateField",
type: "date",
})
).toEqual("\tdateField: IsoDateString\n")
).toEqual("\tdateField: IsoDateString")
})
it("converts select type", () => {
expect(
createTypeField({
createTypeField("test_collection", {
...defaultFieldSchema,
name: "selectField",
type: "select",
})
).toEqual("\tselectField: string\n")
).toEqual("\tselectField: string")
})
it("converts select type with value", () => {
expect(
createTypeField({
createTypeField("test_collection", {
...defaultFieldSchema,
name: "selectFieldWithOpts",
type: "select",
@@ -213,23 +246,32 @@ describe("createTypeField", () => {
values: ["one", "two", "three"],
},
})
).toEqual(`\tselectFieldWithOpts: "one" | "two" | "three"\n`)
).toEqual(`\tselectFieldWithOpts: TestCollectionSelectFieldWithOptsOptions`)
})
it("converts json type", () => {
expect(
createTypeField({
createTypeField("test_collection", {
...defaultFieldSchema,
name: "jsonField",
type: "json",
})
).toEqual("\tjsonField: null | TjsonField\n")
).toEqual("\tjsonField: null | TjsonField")
})
it("converts file type", () => {
expect(
createTypeField({
createTypeField("test_collection", {
...defaultFieldSchema,
name: "fileField",
type: "file",
})
).toEqual("\tfileField: string\n")
).toEqual("\tfileField: string")
})
it("converts file type with multiple files", () => {
expect(
createTypeField({
createTypeField("test_collection", {
...defaultFieldSchema,
name: "fileField",
type: "file",
@@ -237,27 +279,50 @@ describe("createTypeField", () => {
maxSelect: 3,
},
})
).toEqual("\tfileField: string[]\n")
).toEqual("\tfileField: string[]")
})
it("converts relation type", () => {
expect(
createTypeField({
createTypeField("test_collection", {
...defaultFieldSchema,
name: "relationField",
type: "relation",
})
).toEqual("\trelationField: RecordIdString\n")
).toEqual("\trelationField: RecordIdString")
})
it("converts relation type with multiple options", () => {
expect(
createTypeField({
createTypeField("test_collection", {
...defaultFieldSchema,
name: "userField",
name: "relationFieldMany",
type: "relation",
options: {
maxSelect: 3,
},
})
).toEqual("\trelationFieldMany: RecordIdString[]")
})
// DEPRECATED: This was removed in PocketBase v0.8
it("converts user relation type", () => {
expect(
createTypeField("test_collection", {
...defaultFieldSchema,
name: "userRelationField",
type: "user",
})
).toEqual("\tuserField: UserIdString\n")
).toEqual("\tuserRelationField: RecordIdString")
})
it("throws for unexpected types", () => {
expect(() =>
// @ts-ignore
createTypeField({ ...defaultFieldSchema, type: "unknowntype" })
createTypeField("test_collection", {
...defaultFieldSchema,
// @ts-ignore
type: "unknowntype",
})
).toThrowError("unknown type unknowntype found in schema")
})
})

View File

@@ -1,28 +1,12 @@
[
{
"id": "systemprofiles0",
"name": "profiles",
"system": true,
"listRule": "userId = @request.user.id",
"viewRule": "userId = @request.user.id",
"createRule": "userId = @request.user.id",
"updateRule": "userId = @request.user.id",
"deleteRule": null,
"id": "_pb_users_auth_",
"name": "users",
"type": "auth",
"system": false,
"schema": [
{
"id": "pbfielduser",
"name": "userId",
"type": "user",
"system": true,
"required": true,
"unique": true,
"options": {
"maxSelect": 1,
"cascadeDelete": true
}
},
{
"id": "pbfieldname",
"id": "users_name",
"name": "name",
"type": "text",
"system": false,
@@ -35,7 +19,7 @@
}
},
{
"id": "pbfieldavatar",
"id": "users_avatar",
"name": "avatar",
"type": "file",
"system": false,
@@ -54,24 +38,35 @@
"thumbs": null
}
}
]
],
"listRule": "id = @request.auth.id",
"viewRule": "id = @request.auth.id",
"createRule": "",
"updateRule": "id = @request.auth.id",
"deleteRule": "id = @request.auth.id",
"options": {
"allowEmailAuth": true,
"allowOAuth2Auth": true,
"allowUsernameAuth": true,
"exceptEmailDomains": null,
"manageRule": null,
"minPasswordLength": 8,
"onlyEmailDomains": null,
"requireEmail": false
}
},
{
"id": "l9oq1jy97be69be",
"name": "every_type",
"id": "8uexthr74u6jat4",
"name": "everything",
"type": "base",
"system": false,
"listRule": null,
"viewRule": null,
"createRule": null,
"updateRule": null,
"deleteRule": null,
"schema": [
{
"id": "locu2fqr",
"id": "ze7zu2ji",
"name": "text_field",
"type": "text",
"system": false,
"required": true,
"required": false,
"unique": false,
"options": {
"min": null,
@@ -80,11 +75,11 @@
}
},
{
"id": "4pqdzxck",
"id": "6chpapqa",
"name": "number_field",
"type": "number",
"system": false,
"required": true,
"required": false,
"unique": false,
"options": {
"min": null,
@@ -92,28 +87,28 @@
}
},
{
"id": "xtjqakgy",
"id": "bunghw2b",
"name": "bool_field",
"type": "bool",
"system": false,
"required": true,
"required": false,
"unique": false,
"options": {}
},
{
"id": "dc9wxgad",
"id": "kgt2vwcr",
"name": "email_field",
"type": "email",
"system": false,
"required": false,
"unique": false,
"options": {
"exceptDomains": null,
"onlyDomains": null
"exceptDomains": [],
"onlyDomains": []
}
},
{
"id": "ptemvsvm",
"id": "pbyqwc6g",
"name": "url_field",
"type": "url",
"system": false,
@@ -125,7 +120,7 @@
}
},
{
"id": "6reugpzq",
"id": "erxbavbq",
"name": "date_field",
"type": "date",
"system": false,
@@ -137,7 +132,7 @@
}
},
{
"id": "5tsmp1az",
"id": "hy5g988n",
"name": "select_field",
"type": "select",
"system": false,
@@ -149,7 +144,7 @@
}
},
{
"id": "jo91e9vw",
"id": "pbwoyo77",
"name": "json_field",
"type": "json",
"system": false,
@@ -158,7 +153,16 @@
"options": {}
},
{
"id": "f2x5ly7x",
"id": "balhjgn8",
"name": "another_json_field",
"type": "json",
"system": false,
"required": false,
"unique": false,
"options": {}
},
{
"id": "cdblcmro",
"name": "file_field",
"type": "file",
"system": false,
@@ -172,81 +176,101 @@
}
},
{
"id": "uky0rgym",
"name": "relation_field",
"id": "uxeyxkfd",
"name": "three_files_field",
"type": "file",
"system": false,
"required": false,
"unique": false,
"options": {
"maxSelect": 3,
"maxSize": 5242880,
"mimeTypes": [],
"thumbs": []
}
},
{
"id": "vyuzrvxm",
"name": "user_relation_field",
"type": "relation",
"system": false,
"required": false,
"unique": false,
"options": {
"maxSelect": 1,
"collectionId": "dkrwccg04gaf6n0",
"collectionId": "_pb_users_auth_",
"cascadeDelete": false
}
},
{
"id": "qerbl31d",
"name": "user_field",
"type": "user",
"id": "fjzpmh9i",
"name": "custom_relation_field",
"type": "relation",
"system": false,
"required": false,
"unique": false,
"options": {
"maxSelect": 5,
"collectionId": "rs7hepu8zl6kr8e",
"cascadeDelete": false
}
},
{
"id": "iwh5jvyg",
"name": "post_relation_field",
"type": "relation",
"system": false,
"required": false,
"unique": false,
"options": {
"maxSelect": 1,
"collectionId": "z6b9mssubo9megi",
"cascadeDelete": false
}
},
{
"id": "tccaq6g6",
"name": "select_field_no_values",
"type": "text",
"system": false,
"required": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
}
]
},
{
"id": "dkrwccg04gaf6n0",
"name": "orders",
"system": false,
],
"listRule": null,
"viewRule": null,
"createRule": null,
"updateRule": null,
"deleteRule": null,
"options": {}
},
{
"id": "z6b9mssubo9megi",
"name": "posts",
"type": "base",
"system": false,
"schema": [
{
"id": "fbipzebf",
"name": "amount",
"type": "number",
"id": "wzasqdgc",
"name": "field",
"type": "text",
"system": false,
"required": true,
"required": false,
"unique": false,
"options": {
"min": null,
"max": null
"max": null,
"pattern": ""
}
},
{
"id": "gvuptuxz",
"name": "payment_type",
"type": "select",
"system": false,
"required": true,
"unique": false,
"options": {
"maxSelect": 1,
"values": ["credit card", "paypal", "crypto"]
}
},
{
"id": "bnji5emw",
"name": "user",
"type": "user",
"system": false,
"required": true,
"unique": false,
"options": {
"maxSelect": 1,
"cascadeDelete": false
}
},
{
"id": "pfelzqqv",
"name": "product",
"id": "175adqww",
"name": "nonempty_field",
"type": "text",
"system": false,
"required": true,
@@ -256,7 +280,97 @@
"max": null,
"pattern": ""
}
},
{
"id": "s3cl0rdp",
"name": "nonempty_bool",
"type": "bool",
"system": false,
"required": true,
"unique": false,
"options": {}
},
{
"id": "36buozcb",
"name": "field1",
"type": "number",
"system": false,
"required": false,
"unique": false,
"options": {
"min": null,
"max": null
}
}
]
],
"listRule": null,
"viewRule": null,
"createRule": null,
"updateRule": null,
"deleteRule": null,
"options": {}
},
{
"id": "rs7hepu8zl6kr8e",
"name": "custom_auth",
"type": "auth",
"system": false,
"schema": [
{
"id": "zj6cku68",
"name": "custom_field",
"type": "text",
"system": false,
"required": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
}
],
"listRule": null,
"viewRule": null,
"createRule": null,
"updateRule": null,
"deleteRule": null,
"options": {
"allowEmailAuth": true,
"allowOAuth2Auth": true,
"allowUsernameAuth": true,
"exceptEmailDomains": null,
"manageRule": null,
"minPasswordLength": 8,
"onlyEmailDomains": null,
"requireEmail": false
}
},
{
"id": "kr8109mcfuu18qq",
"name": "base",
"type": "base",
"system": false,
"schema": [
{
"id": "epgo3hyb",
"name": "field",
"type": "text",
"system": false,
"required": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
}
],
"listRule": null,
"viewRule": null,
"createRule": null,
"updateRule": null,
"deleteRule": null,
"options": {}
}
]

View File

@@ -1,61 +1,92 @@
// This file was @generated using pocketbase-typegen
/**
* This file was @generated using pocketbase-typegen
*/
export enum Collections {
Base = "base",
CustomAuth = "custom_auth",
Everything = "everything",
Posts = "posts",
Users = "users",
}
// Alias types for improved usability
export type IsoDateString = string
export type RecordIdString = string
export type UserIdString = string
export type BaseRecord = {
// System fields
export type BaseSystemFields = {
id: RecordIdString
created: IsoDateString
updated: IsoDateString
"@collectionId": string
"@collectionName": string
"@expand"?: { [key: string]: any }
collectionId: string
collectionName: Collections
expand?: { [key: string]: any }
}
export enum Collections {
EveryType = "every_type",
Orders = "orders",
Profiles = "profiles",
export type AuthSystemFields = {
email: string
emailVisibility: boolean
username: string
verified: boolean
} & BaseSystemFields
// Record types for each collection
export type BaseRecord = {
field?: string
}
export type EveryTypeRecord<Tjson_field = unknown> = {
text_field: string
number_field: number
bool_field: boolean
export type CustomAuthRecord = {
custom_field?: string
}
export enum EverythingSelectFieldOptions {
optionA = "optionA",
optionB = "optionB",
optionC = "optionC",
}
export type EverythingRecord<Tanother_json_field = unknown, Tjson_field = unknown> = {
text_field?: string
number_field?: number
bool_field?: boolean
email_field?: string
url_field?: string
date_field?: IsoDateString
select_field?: "optionA" | "optionB" | "optionC"
select_field?: EverythingSelectFieldOptions
json_field?: null | Tjson_field
another_json_field?: null | Tanother_json_field
file_field?: string
relation_field?: RecordIdString
user_field?: UserIdString
three_files_field?: string[]
user_relation_field?: RecordIdString
custom_relation_field?: RecordIdString[]
post_relation_field?: RecordIdString
select_field_no_values?: string
}
export type EveryTypeResponse<Tjson_field = unknown> = EveryTypeRecord<Tjson_field> & BaseRecord
export type OrdersRecord = {
amount: number
payment_type: "credit card" | "paypal" | "crypto"
user: UserIdString
product: string
export type PostsRecord = {
field?: string
nonempty_field: string
nonempty_bool: boolean
field1?: number
}
export type OrdersResponse = OrdersRecord & BaseRecord
export type ProfilesRecord = {
userId: UserIdString
export type UsersRecord = {
name?: string
avatar?: string
}
export type ProfilesResponse = ProfilesRecord & BaseRecord
// 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<Tanother_json_field = unknown, Tjson_field = unknown> = EverythingRecord<Tanother_json_field, Tjson_field> & BaseSystemFields
export type PostsResponse = PostsRecord & BaseSystemFields
export type UsersResponse = UsersRecord & AuthSystemFields
export type CollectionRecords = {
every_type: EveryTypeRecord
orders: OrdersRecord
profiles: ProfilesRecord
base: BaseRecord
custom_auth: CustomAuthRecord
everything: EverythingRecord
posts: PostsRecord
users: UsersRecord
}

View File

@@ -5,7 +5,8 @@
import {
CollectionRecords,
Collections,
EveryTypeRecord,
EverythingRecord,
EverythingSelectFieldOptions,
} from "./pocketbase-types-example"
// Utility function can to infer collection type
@@ -13,26 +14,37 @@ function getOne<T extends keyof CollectionRecords>(
collection: T,
id: string
): CollectionRecords[T] {
console.log(collection, id)
return JSON.parse("id") as CollectionRecords[T]
}
// Return type is correctly inferred
let thing = getOne(Collections.EveryType, "a")
const thing = getOne(Collections.Everything, "a")
// Works when passing in JSON generic
const everythingRecordWithGeneric: EveryTypeRecord<{ a: "some string" }> = {
const everythingRecordWithGeneric: EverythingRecord<{ a: "some string" }> = {
json_field: { a: "some string" },
text_field: "string",
number_field: 1,
bool_field: true,
}
// Works without passing in JSON generic
const everythingRecordWithoutGeneric: EveryTypeRecord = {
const everythingRecordWithoutGeneric: EverythingRecord = {
json_field: { a: "some string" },
text_field: "string",
number_field: 1,
bool_field: true,
}
console.log(thing, everythingRecordWithGeneric, everythingRecordWithoutGeneric)
// Test select option enums
const selectOptions: EverythingRecord = {
select_field: EverythingSelectFieldOptions.optionA,
select_field_no_values: "foo",
}
// Reference the created variables
console.log(
thing,
everythingRecordWithGeneric,
everythingRecordWithoutGeneric,
selectOptions
)

View File

@@ -1,4 +1,9 @@
import { sanitizeFieldName, toPascalCase } from "../src/utils"
import {
getOptionEnumName,
getSystemFields,
sanitizeFieldName,
toPascalCase,
} from "../src/utils"
describe("toPascalCase", () => {
it("return pascal case string", () => {
@@ -22,3 +27,19 @@ describe("sanitizeFieldName", () => {
expect(sanitizeFieldName("4number")).toEqual('"4number"')
})
})
describe("getSystemFields", () => {
it("returns the system field type name for a given collection type", () => {
expect(getSystemFields("base")).toBe("BaseSystemFields")
expect(getSystemFields("auth")).toBe("AuthSystemFields")
})
})
describe("getOptionEnumName", () => {
it("returns the enum name for select field options", () => {
expect(getOptionEnumName("orders", "type")).toBe("OrdersTypeOptions")
expect(getOptionEnumName("orders_with_underscore", "type_underscore")).toBe(
"OrdersWithUnderscoreTypeUnderscoreOptions"
)
})
})