-
-
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); |
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 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:
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