Skip to content

Instantly share code, notes, and snippets.

@RepComm
Last active August 8, 2024 16:25
Show Gist options
  • Save RepComm/c1a2f1d8d8dc52d954eb01ab88866153 to your computer and use it in GitHub Desktop.
Save RepComm/c1a2f1d8d8dc52d954eb01ab88866153 to your computer and use it in GitHub Desktop.
pocketbase pb_schema.json to typescript definitions ( ex usage: ./schema2jsdoc.mjs > schema.d.ts ) - runs where-ever current directory is and finds pb_schema.json, outputs to stdout, pipe to a file to save, or copy paste text
export interface pb_schema_entry_schema_options {
min: number;
max: number;
pattern: string;
}
export interface pb_schema_entry_schema {
system: boolean;
id: string;
name: string;
type: string;
required: boolean;
presentable: boolean;
unique: boolean;
options: pb_schema_entry_schema_options;
}
export interface pb_schema_entry_options {
allowEmailAuth: boolean;
allowOAuth2Auth: boolean;
allowUsernameAuth: boolean;
exceptEmailDomains: boolean;
manageRule: string;
minPasswordLength: number;
onlyEmailDomains: boolean;
onlyVerified: boolean;
requireEmail: boolean;
}
export interface pb_schema_entry {
id: string;
name: string;
type: string;
system: boolean;
schema: pb_schema_entry_schema[];
indexes: any[];
listRule: string;
viewRule: string;
createRule: string;
updateRule: string;
deleteRule: string;
options: pb_schema_entry_options;
}
export type pb_schema = pb_schema_entry[];
#!/usr/bin/env node
import { readFileSync } from "fs";
import { exit } from "process";
function log(...msgs) {
console.log("//", ...msgs);
}
function warn(...msgs) {
log("WARN:", ...msgs);
}
log(
"schema2jsdoc - https://gist.github.com/RepComm/c1a2f1d8d8dc52d954eb01ab88866153",
);
function error(...msgs) {
console.error("//ERROR: ", ...msgs);
}
let schemaData = "";
try {
//read stdin (aka 0 in the first arg) fully to text instead of an actual file
//https://stackoverflow.com/a/56012724/8112809
schemaData = readFileSync(0, { encoding: "utf-8" });
} catch (ex) {
error(
"couldn't read schema from standard input, try: cat pb_schema.json | ./schema2jsdoc.mjs",
ex,
);
exit(2);
}
/**@typedef {import("./schema2jsdoc").pb_schema} pb_schema*/
/**@type {pb_schema}*/
let schemaJson = {};
try {
schemaJson = JSON.parse(schemaData);
} catch (ex) {
error(
"couldn't parse schema json, try: cat pb_schema.json | ./schema2jsdoc.mjs",
ex,
);
exit(3);
}
if (!Array.isArray(schemaJson)) {
error("schema root is not an array");
exit(4);
}
if (schemaJson.length < 1) {
error("schema root array length < 1");
exit(5);
}
function fieldToPropType(field_type) {
switch (field_type) {
case "text":
return "string";
case "number":
return "number";
case "relation":
return "string";
case "editor":
return "string";
case "file":
return "string";
case "select":
return "string";
case "bool":
return "boolean";
default:
warn(`unknown field type '${field_type}' using 'any' as a catch-all`);
return "any";
}
}
/** May be incomplete, but should handle many cases
* Used as a reference
* https://www.w3schools.com/js/js_reserved.asp
*/
const JS_RESERVED_WORDS = new Set([
"arguments",
"await",
"break",
"case",
"catch",
"class",
"const",
"continue",
"debugger",
"default",
"delete",
"do",
"else",
"enum",
"eval",
"export",
"extends",
"false",
"finally",
"for",
"function",
"if",
"implements",
"import",
"in",
"instanceof",
"interface",
"let",
"native",
"new",
"null",
"package",
"private",
"protected",
"public",
"return",
"static",
"super",
"switch",
"this",
"throw",
"throws",
"true",
"try",
"typeof",
"var",
"void",
"while",
"with",
"yield",
]);
function resolveName(field_name) {
if (JS_RESERVED_WORDS.has(field_name)) {
return `__${field_name}`;
} else {
return field_name;
}
}
let output = "";
output +=
'declare import PocketBaseImport, {RecordService,RecordModel} from "pocketbase";\n';
const collectionToInterfaceNameMap = new Map();
const collectionNameToIdMap = new Map();
for (const entry of schemaJson) {
//not all strings are valid typescript interface names, quell some common issues here
const ifname = resolveName(entry.name);
//track collection id - used for relation mapping of 'expand' property in typescript definition output
collectionNameToIdMap.set(ifname, entry.id);
//save the remapping for later for output type pb_schema_map
collectionToInterfaceNameMap.set(entry.name, ifname);
//begin writing the interface
output += `interface ${ifname} extends RecordModel {\n`;
const fieldNameToRelationIdMap = new Map();
const fieldNameToRelationExpandIsArray = new Map();
//output props of collection types
for (const field of entry.schema) {
if (field.type === "relation") {
fieldNameToRelationIdMap.set(field.name, field.options.collectionId);
output += ` /**relation id, use .expand property*/\n`;
}
if (!field.options.maxSelect || field.options.maxSelect !== 1) {
fieldNameToRelationExpandIsArray.set(field.name, true);
}
const ft = fieldToPropType(field.type);
output += ` ${field.name}: ${ft};\n`;
}
//output expand prop if necessary
if (fieldNameToRelationIdMap.size > 1) {
output += ` expand?: {\n`;
for (const [name, collectionId] of fieldNameToRelationIdMap) {
//use the relation collection id to avoid extra looping thru schema for lookups
//typescript definitions are good at this anyways, plus it looks cool
if (fieldNameToRelationExpandIsArray.get(name) === true) {
output += ` ${name}: CollectionIdNameMap["${collectionId}"][];\n`;
} else {
output += ` ${name}: CollectionIdNameMap["${collectionId}"];\n`;
}
}
output += " }\n";
}
//end the interface
output += "}\n";
}
//output pb_schema_map for mapping collection names to interface names
output += "export interface pb_schema_map {\n";
for (const [k, v] of collectionToInterfaceNameMap) {
output += ` "${k}": ${v};\n`;
}
output += "}\n";
//output TypedPocketBase
output += "export interface TypedPocketBase extends PocketBaseImport {\n";
output += " collection(idOrName: string): RecordService;\n";
for (const [k, v] of collectionToInterfaceNameMap) {
output += ` collection(idOrName: "${k}"): RecordService<${v}>;\n`;
}
output += "}\n";
//output CollectionIdNameMap for mapping collection ids to interfaces
output += "interface CollectionIdNameMap {\n";
for (const [name, id] of collectionNameToIdMap) {
output += ` "${id}": ${name};\n`;
}
output += "}\n";
//output result
console.log(output);
@RepComm
Copy link
Author

RepComm commented May 16, 2024

revision 3 - remaps most reserved js keywords in interface names to __${name} to prevent issues such as naming a pocketbase table "class" (shame on you, you should be using plural for collection names anyways! ;-) )

also outputs exported interface pb_schema_map with all the fields mapped with original names as string keys, and the interfaces as values

@RepComm
Copy link
Author

RepComm commented May 16, 2024

TODO - map "expand" props for relation field types

@RepComm
Copy link
Author

RepComm commented May 16, 2024

revision 4
map expands complete! and it is sick.

no longer tries to read pb_schema.json from current directory, instead expects to be passed pb_schema.json contents passed to standard input, example:

cat ./pb_schema.json | ./schema2jsdoc.mjs > schema.d.ts

this above line uses 'cat' command to echo content from a file 'pb_schema.json', pipes the output to schema2jsdoc.mjs shell script from this gist, and diverts output from that to schema.d.ts file where type defs will be written

example output as of now
// schema2jsdoc - https://gist.github.com/RepComm/c1a2f1d8d8dc52d954eb01ab88866153
interface players {
 name: string;
 avatar: string;
}
interface alignments {
 name: string;
}
interface appearances {
 image: string;
 name: string;
 characters: string;
}
interface armor_classes {
 name: string;
}
interface background {
 name: string;
}
interface characters {
 name: string;
 player: string;
 class: string;
 level: number;
 background: string;
 race: string;
 alignment: string;
 xp: number;
 strength: number;
 dexterity: number;
 constitution: number;
 intelligence: number;
 wisdom: number;
 charisma: number;
 wisdom_passive: number;
 armor_class: string;
 speed: number;
 personality_traits: string;
 ideals: string;
 bonds: string;
 flaws: string;
 age: number;
 height: number;
 weight: number;
 eyes: string;
 skin: string;
 hair: string;
 appearances: string;
 backstory: string;
 treasure: string;
 additional_features_and_traits: string;
 expand?: {
  player: CollectionIdNameMap["_pb_users_auth_"];
  class: CollectionIdNameMap["6p9jdpn1ashwh1k"];
  background: CollectionIdNameMap["v5d83ccwckijday"];
  race: CollectionIdNameMap["irasc0g38drvkxy"];
  alignment: CollectionIdNameMap["kugurr24ahjs4nm"];
  armor_class: CollectionIdNameMap["r6u975oqxkxv66h"];
  appearances: CollectionIdNameMap["y99prlxo6k4folv"];
  treasure: CollectionIdNameMap["7bnpj5je1dn6jfi"];
 }
}
interface __class {
 name: string;
}
interface races {
 name: string;
}
interface treasures {
 name: string;
 image: string;
}
export interface pb_schema_map {
 "players": players;
 "alignments": alignments;
 "appearances": appearances;
 "armor_classes": armor_classes;
 "background": background;
 "characters": characters;
 "class": __class;
 "races": races;
 "treasures": treasures;
}
interface CollectionIdNameMap {
 "_pb_users_auth_": players;
 "kugurr24ahjs4nm": alignments;
 "y99prlxo6k4folv": appearances;
 "r6u975oqxkxv66h": armor_classes;
 "v5d83ccwckijday": background;
 "knyh02k466va30x": characters;
 "6p9jdpn1ashwh1k": __class;
 "irasc0g38drvkxy": races;
 "7bnpj5je1dn6jfi": treasures;
}

@RepComm
Copy link
Author

RepComm commented May 16, 2024

revision 5 - output comment about relation fields for each relation field in type definitions output for more convenience while using the types output by the tool

@RepComm
Copy link
Author

RepComm commented Aug 7, 2024

Revision 6 - fixed bug where 'warn' and 'panic' functions didn't exist, replaced 'panic' with nodejs exit, and 'warn' calls 'log' now
Revision 8 - exports "TypedPocketBase", an interface that extends "Pocketbase" imported from "pocketbase", which you can use like so:

import type { TypedPocketBase } from "./schema";
const pb: TypedPocketBase = new Pocketbase();

as mentioned in pocketbase docs: https://github.com/pocketbase/js-sdk?tab=readme-ov-file#specify-typescript-definitions

@RepComm
Copy link
Author

RepComm commented Aug 7, 2024

Revision 9 - collection types extend RecordModel from PocketBaseImport now, so properties of records will have built-in property type definitions like record.id

@RepComm
Copy link
Author

RepComm commented Aug 8, 2024

Revision 10 - multiple relation now maps to expands field as array instead of singular, which was incorrect.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment