Last active
December 19, 2024 17:00
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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); |
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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:
as mentioned in pocketbase docs: https://github.com/pocketbase/js-sdk?tab=readme-ov-file#specify-typescript-definitions