Skip to content

Instantly share code, notes, and snippets.

@RepComm
Last active December 19, 2024 17:00
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
#!/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, relationIsMultiple = false) {
switch (field_type) {
case "text":
return "string";
case "number":
return "number";
case "relation":
if (relationIsMultiple) {
return "string[]";
} else {
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.fields) {
if (field.type === "relation") {
fieldNameToRelationIdMap.set(field.name, field.collectionId);
output += ` /**relation id, use .expand property*/\n`;
}
let relationIsMultiple = false;
if (!field.maxSelect || field.maxSelect !== 1) {
relationIsMultiple = true;
//output += "//DEBUG: maxSelect: " + field.maxSelect + "\n";
fieldNameToRelationExpandIsArray.set(field.name, true);
}
const ft = fieldToPropType(field.type, relationIsMultiple);
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.

@RepComm
Copy link
Author

RepComm commented Dec 16, 2024

Revision 11 - updated to handle PocketBase v0.23.8 pb_schema.json

@RepComm
Copy link
Author

RepComm commented Dec 19, 2024

Revision 12 - updated to handle relation maxSelect!=1 field type as string[] instead of string

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