diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index ead5928..4827452 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -16,6 +16,10 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v4 + + - name: Setup + run: npm ci + run: npm run build - name: Run in docker run: | diff --git a/dist/index.js b/dist/index.js deleted file mode 100755 index 53de119..0000000 --- a/dist/index.js +++ /dev/null @@ -1,385 +0,0 @@ -#!/usr/bin/env node - -// src/cli.ts -import dotenv from "dotenv"; - -// src/schema.ts -import FormData from "form-data"; -import fetch from "cross-fetch"; -import { promises as fs } from "fs"; -import { open } from "sqlite"; -import sqlite3 from "sqlite3"; -async function fromDatabase(dbPath) { - const db = await open({ - driver: sqlite3.Database, - filename: dbPath - }); - const result = await db.all("SELECT * FROM _collections"); - return result.map((collection) => ({ - ...collection, - fields: JSON.parse(collection.fields ?? collection.schema ?? "{}") - })); -} -async function fromJSON(path) { - const schemaStr = await fs.readFile(path, { encoding: "utf8" }); - return JSON.parse(schemaStr); -} -async function fromURLWithToken(url, token = "") { - let collections = []; - try { - const result = await fetch(`${url}/api/collections?perPage=200`, { - headers: { - Authorization: token - } - }).then((res) => { - if (!res.ok) - throw res; - return res.json(); - }); - collections = result.items; - } catch (error) { - console.error(error); - process.exit(1); - } - return collections; -} -async function fromURLWithPassword(url, email = "", password = "") { - const formData = new FormData(); - formData.append("identity", email); - formData.append("password", password); - let collections = []; - try { - const { token } = await fetch( - `${url}/api/collections/_superusers/auth-with-password`, - { - body: formData, - method: "post" - } - ).then((res) => { - if (!res.ok) - throw res; - return res.json(); - }); - collections = await fromURLWithToken(url, token); - } catch (error) { - console.error(error); - process.exit(1); - } - return collections; -} - -// src/constants.ts -var EXPORT_COMMENT = `/** -* This file was @generated using pocketbase-typegen -*/`; -var IMPORTS = `import type PocketBase from 'pocketbase' -import type { RecordService } from 'pocketbase'`; -var RECORD_TYPE_COMMENT = `// Record types for each collection`; -var RESPONSE_TYPE_COMMENT = `// Response types include system fields and match responses from the PocketBase API`; -var ALL_RECORD_RESPONSE_COMMENT = `// Types containing all Records and Responses, useful for creating typing helper functions`; -var TYPED_POCKETBASE_COMMENT = `// Type for usage with type asserted PocketBase instance -// https://github.com/pocketbase/js-sdk#specify-typescript-definitions`; -var EXPAND_GENERIC_NAME = "expand"; -var DATE_STRING_TYPE_NAME = `IsoDateString`; -var RECORD_ID_STRING_NAME = `RecordIdString`; -var HTML_STRING_NAME = `HTMLString`; -var ALIAS_TYPE_DEFINITIONS = `// Alias types for improved usability -export type ${DATE_STRING_TYPE_NAME} = string -export type ${RECORD_ID_STRING_NAME} = string -export type ${HTML_STRING_NAME} = string`; -var BASE_SYSTEM_FIELDS_DEFINITION = `// System fields -export type BaseSystemFields = { - id: ${RECORD_ID_STRING_NAME} - collectionId: string - collectionName: Collections -} & ExpandType`; -var AUTH_SYSTEM_FIELDS_DEFINITION = `export type AuthSystemFields = { - email: string - emailVisibility: boolean - username: string - verified: boolean -} & BaseSystemFields`; -var EXPAND_TYPE_DEFINITION = `type ExpandType = unknown extends T - ? T extends unknown - ? { expand?: unknown } - : { expand: T } - : { expand: T }`; - -// src/utils.ts -import { promises as fs2 } from "fs"; -function toPascalCase(str) { - if (/^[\p{L}\d]+$/iu.test(str)) { - return str.charAt(0).toUpperCase() + str.slice(1); - } - return str.replace( - /([\p{L}\d])([\p{L}\d]*)/giu, - (g0, g1, g2) => g1.toUpperCase() + g2.toLowerCase() - ).replace(/[^\p{L}\d]/giu, ""); -} -function sanitizeFieldName(name) { - return !isNaN(parseFloat(name.charAt(0))) ? `"${name}"` : name; -} -async function saveFile(outPath, typeString) { - await fs2.writeFile(outPath, typeString, "utf8"); - console.log(`Created typescript definitions at ${outPath}`); -} -function getSystemFields(type) { - switch (type) { - case "auth": - return "AuthSystemFields"; - default: - return "BaseSystemFields"; - } -} -function getOptionEnumName(recordName, fieldName) { - return `${toPascalCase(recordName)}${toPascalCase(fieldName)}Options`; -} -function getOptionValues(field) { - const values = field.values; - if (!values) - return []; - return values.filter((val, i) => values.indexOf(val) === i); -} - -// src/collections.ts -function createCollectionEnum(collectionNames) { - const collections = collectionNames.map((name) => ` ${toPascalCase(name)} = "${name}",`).join("\n"); - const typeString = `export enum Collections { -${collections} -}`; - return typeString; -} -function createCollectionRecords(collectionNames) { - const nameRecordMap = collectionNames.map((name) => ` ${name}: ${toPascalCase(name)}Record`).join("\n"); - return `export type CollectionRecords = { -${nameRecordMap} -}`; -} -function createCollectionResponses(collectionNames) { - const nameRecordMap = collectionNames.map((name) => ` ${name}: ${toPascalCase(name)}Response`).join("\n"); - return `export type CollectionResponses = { -${nameRecordMap} -}`; -} -function createTypedPocketbase(collectionNames) { - const nameRecordMap = collectionNames.map( - (name) => ` collection(idOrName: '${name}'): RecordService<${toPascalCase( - name - )}Response>` - ).join("\n"); - return `export type TypedPocketBase = PocketBase & { -${nameRecordMap} -}`; -} - -// src/generics.ts -function fieldNameToGeneric(name) { - return `T${name}`; -} -function getGenericArgList(schema) { - const jsonFields = schema.filter((field) => field.type === "json").map((field) => fieldNameToGeneric(field.name)).sort(); - return jsonFields; -} -function getGenericArgStringForRecord(schema) { - const argList = getGenericArgList(schema); - if (argList.length === 0) - return ""; - return `<${argList.map((name) => `${name}`).join(", ")}>`; -} -function getGenericArgStringWithDefault(schema, opts) { - const argList = getGenericArgList(schema); - if (opts.includeExpand) { - argList.push(fieldNameToGeneric(EXPAND_GENERIC_NAME)); - } - if (argList.length === 0) - return ""; - return `<${argList.map((name) => `${name} = unknown`).join(", ")}>`; -} - -// src/fields.ts -var pbSchemaTypescriptMap = { - bool: "boolean", - date: DATE_STRING_TYPE_NAME, - autodate: DATE_STRING_TYPE_NAME, - editor: HTML_STRING_NAME, - email: "string", - text: "string", - url: "string", - password: "string", - number: "number", - file: (fieldSchema) => fieldSchema.maxSelect && fieldSchema.maxSelect > 1 ? "string[]" : "string", - json: (fieldSchema) => `null | ${fieldNameToGeneric(fieldSchema.name)}`, - relation: (fieldSchema) => fieldSchema.maxSelect && fieldSchema.maxSelect === 1 ? RECORD_ID_STRING_NAME : `${RECORD_ID_STRING_NAME}[]`, - select: (fieldSchema, collectionName) => { - const valueType = fieldSchema.values ? getOptionEnumName(collectionName, fieldSchema.name) : "string"; - return fieldSchema.maxSelect && fieldSchema.maxSelect > 1 ? `${valueType}[]` : valueType; - }, - user: (fieldSchema) => fieldSchema.maxSelect && fieldSchema.maxSelect > 1 ? `${RECORD_ID_STRING_NAME}[]` : RECORD_ID_STRING_NAME -}; -function createTypeField(collectionName, fieldSchema) { - let typeStringOrFunc; - if (!(fieldSchema.type in pbSchemaTypescriptMap)) { - console.log(`WARNING: unknown type "${fieldSchema.type}" found in schema`); - typeStringOrFunc = "unknown"; - } else { - typeStringOrFunc = pbSchemaTypescriptMap[fieldSchema.type]; - } - const typeString = typeof typeStringOrFunc === "function" ? typeStringOrFunc(fieldSchema, collectionName) : typeStringOrFunc; - const fieldName = sanitizeFieldName(fieldSchema.name); - const required = fieldSchema.required ? "" : "?"; - return ` ${fieldName}${required}: ${typeString}`; -} -function createSelectOptions(recordName, fields) { - const selectFields = fields.filter((field) => field.type === "select"); - const typestring = selectFields.map( - (field) => `export enum ${getOptionEnumName(recordName, field.name)} { -${getOptionValues(field).map((val) => ` "${getSelectOptionEnumName(val)}" = "${val}",`).join("\n")} -} -` - ).join("\n"); - return typestring; -} -function getSelectOptionEnumName(val) { - if (!isNaN(Number(val))) { - return `E${val}`; - } else { - return val; - } -} - -// src/lib.ts -function generate(results, options2) { - const collectionNames = []; - const recordTypes = []; - const responseTypes = [RESPONSE_TYPE_COMMENT]; - results.sort((a, b) => a.name <= b.name ? -1 : 1).forEach((row) => { - if (row.name) - collectionNames.push(row.name); - if (row.fields) { - recordTypes.push(createRecordType(row.name, row.fields)); - responseTypes.push(createResponseType(row)); - } - }); - const sortedCollectionNames = collectionNames; - const fileParts = [ - EXPORT_COMMENT, - options2.sdk && IMPORTS, - createCollectionEnum(sortedCollectionNames), - ALIAS_TYPE_DEFINITIONS, - EXPAND_TYPE_DEFINITION, - BASE_SYSTEM_FIELDS_DEFINITION, - AUTH_SYSTEM_FIELDS_DEFINITION, - RECORD_TYPE_COMMENT, - ...recordTypes, - responseTypes.join("\n"), - ALL_RECORD_RESPONSE_COMMENT, - createCollectionRecords(sortedCollectionNames), - createCollectionResponses(sortedCollectionNames), - options2.sdk && TYPED_POCKETBASE_COMMENT, - options2.sdk && createTypedPocketbase(sortedCollectionNames) - ]; - return fileParts.filter(Boolean).join("\n\n") + "\n"; -} -function createRecordType(name, schema) { - const selectOptionEnums = createSelectOptions(name, schema); - const typeName = toPascalCase(name); - const genericArgs = getGenericArgStringWithDefault(schema, { - includeExpand: false - }); - const fields = schema.map((fieldSchema) => createTypeField(name, fieldSchema)).sort().join("\n"); - return `${selectOptionEnums}export type ${typeName}Record${genericArgs} = ${fields ? `{ -${fields} -}` : "never"}`; -} -function createResponseType(collectionSchemaEntry) { - const { name, fields, type } = collectionSchemaEntry; - const pascaleName = toPascalCase(name); - const genericArgsWithDefaults = getGenericArgStringWithDefault(fields, { - includeExpand: true - }); - const genericArgsForRecord = getGenericArgStringForRecord(fields); - const systemFields = getSystemFields(type); - const expandArgString = ``; - return `export type ${pascaleName}Response${genericArgsWithDefaults} = Required<${pascaleName}Record${genericArgsForRecord}> & ${systemFields}${expandArgString}`; -} - -// src/cli.ts -async function main(options2) { - let schema; - if (options2.db) { - schema = await fromDatabase(options2.db); - } else if (options2.json) { - schema = await fromJSON(options2.json); - } else if (options2.url) { - if (options2.token) { - schema = await fromURLWithToken(options2.url, options2.token); - } else { - schema = await fromURLWithPassword( - options2.url, - options2.email, - options2.password - ); - } - } else if (options2.env) { - const path = typeof options2.env === "string" ? options2.env : ".env"; - dotenv.config({ path }); - if (!process.env.PB_TYPEGEN_URL || !process.env.PB_TYPEGEN_EMAIL || !process.env.PB_TYPEGEN_PASSWORD) { - return console.error( - "Missing environment variables. Check options: pocketbase-typegen --help" - ); - } - schema = await fromURLWithPassword( - process.env.PB_TYPEGEN_URL, - process.env.PB_TYPEGEN_EMAIL, - process.env.PB_TYPEGEN_PASSWORD - ); - } else { - return console.error( - "Missing schema path. Check options: pocketbase-typegen --help" - ); - } - const typeString = generate(schema, { - sdk: options2.sdk ?? true - }); - await saveFile(options2.out, typeString); - return typeString; -} - -// src/index.ts -import { program } from "commander"; - -// package.json -var version = "1.3.0"; - -// src/index.ts -program.name("Pocketbase Typegen").version(version).description( - "CLI to create typescript typings for your pocketbase.io records" -).option("-d, --db ", "path to the pocketbase SQLite database").option( - "-j, --json ", - "path to JSON schema exported from pocketbase admin UI" -).option( - "-u, --url ", - "URL to your hosted pocketbase instance. When using this options you must also provide email and password options or auth token option." -).option( - "--email ", - "email for a pocketbase superuser. Use this with the --url option" -).option( - "-p, --password ", - "password for a pocketbase superuser. Use this with the --url option" -).option( - "-t, --token ", - "auth token for a pocketbase superuser. Use this with the --url option" -).option( - "-o, --out ", - "path to save the typescript output file", - "pocketbase-types.ts" -).option( - "--no-sdk", - "remove the pocketbase package dependency. A typed version of the SDK will not be generated." -).option( - "--env [path]", - "flag to use environment variables for configuration. Add PB_TYPEGEN_URL, PB_TYPEGEN_EMAIL, PB_TYPEGEN_PASSWORD to your .env file. Optionally provide a path to your .env file" -); -program.parse(process.argv); -var options = program.opts(); -main(options); diff --git a/package.json b/package.json index 71d2e83..039eeaf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pocketbase-typegen", - "version": "1.3.0", + "version": "1.3.1", "description": "Generate pocketbase record types from your database", "main": "dist/index.js", "bin": {