-
-
Save RepComm/c1a2f1d8d8dc52d954eb01ab88866153 to your computer and use it in GitHub Desktop.
#!/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); |
TODO - map "expand" props for relation field types
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;
}
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
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
Revision 9 - collection types extend RecordModel from PocketBaseImport now, so properties of records will have built-in property type definitions like record.id
Revision 10 - multiple relation now maps to expands field as array instead of singular, which was incorrect.
Revision 11 - updated to handle PocketBase v0.23.8 pb_schema.json
Revision 12 - updated to handle relation maxSelect!=1 field type as string[] instead of string
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