UNPKG

pocketbase-typegen

Version:

Generate pocketbase record types from your database

312 lines (297 loc) 11.2 kB
// src/utils.ts 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; } 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); } function containsGeoPoint(collections) { return collections.some( (collection) => collection.fields.some((field) => field.type === "geoPoint") ); } // src/collections.ts function createCollectionEnum(collectionNames) { const collections = collectionNames.map((name) => ` ${toPascalCase(name)}: "${name}",`).join("\n"); return `export const Collections = { ${collections} } as const export type Collections = typeof Collections[keyof typeof Collections]`; } 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} }`; } // 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_TYPE = `// Type for usage with type asserted PocketBase instance // https://github.com/pocketbase/js-sdk#specify-typescript-definitions export type TypedPocketBase = { collection<T extends keyof CollectionResponses>( idOrName: T ): RecordService<CollectionResponses[T]> } & PocketBase`; var EXPAND_GENERIC_NAME = "expand"; var DATE_STRING_TYPE_NAME = `IsoDateString`; var AUTODATE_STRING_TYPE_NAME = `IsoAutoDateString`; var RECORD_ID_STRING_NAME = `RecordIdString`; var FILE_NAME_STRING_NAME = `FileNameString`; var HTML_STRING_NAME = `HTMLString`; var GEOPOINT_TYPE_NAME = `GeoPoint`; var ALIAS_TYPE_DEFINITIONS = `// Alias types for improved usability export type ${DATE_STRING_TYPE_NAME} = string export type ${AUTODATE_STRING_TYPE_NAME} = string & { readonly autodate: unique symbol } export type ${RECORD_ID_STRING_NAME} = string export type ${FILE_NAME_STRING_NAME} = string & { readonly filename: unique symbol } export type ${HTML_STRING_NAME} = string`; var BASE_SYSTEM_FIELDS_DEFINITION = `// System fields export type BaseSystemFields<T = unknown> = { id: ${RECORD_ID_STRING_NAME} collectionId: string collectionName: Collections } & ExpandType<T>`; var AUTH_SYSTEM_FIELDS_DEFINITION = `export type AuthSystemFields<T = unknown> = { email: string emailVisibility: boolean username: string verified: boolean } & BaseSystemFields<T>`; var EXPAND_TYPE_DEFINITION = `type ExpandType<T> = unknown extends T ? T extends unknown ? { expand?: unknown } : { expand: T } : { expand: T }`; var GEOPOINT_TYPE_DEFINITION = `export type ${GEOPOINT_TYPE_NAME} = { lon: number lat: number }`; var UTILITY_TYPES = `// Utility types for create/update operations type ProcessCreateAndUpdateFields<T> = Omit<{ // Omit AutoDate fields [K in keyof T as Extract<T[K], IsoAutoDateString> extends never ? K : never]: // Convert FileNameString to File T[K] extends infer U ? U extends (FileNameString | FileNameString[]) ? U extends any[] ? File[] : File : U : never }, 'id'> // Create type for Auth collections export type CreateAuth<T> = { id?: ${RECORD_ID_STRING_NAME} email: string emailVisibility?: boolean password: string passwordConfirm: string verified?: boolean } & ProcessCreateAndUpdateFields<T> // Create type for Base collections export type CreateBase<T> = { id?: RecordIdString } & ProcessCreateAndUpdateFields<T> // Update type for Auth collections export type UpdateAuth<T> = Partial< Omit<ProcessCreateAndUpdateFields<T>, keyof AuthSystemFields> > & { email?: string emailVisibility?: boolean oldPassword?: string password?: string passwordConfirm?: string verified?: boolean } // Update type for Base collections export type UpdateBase<T> = Partial< Omit<ProcessCreateAndUpdateFields<T>, keyof BaseSystemFields> > // Get the correct create type for any collection export type Create<T extends keyof CollectionResponses> = CollectionResponses[T] extends AuthSystemFields ? CreateAuth<CollectionRecords[T]> : CreateBase<CollectionRecords[T]> // Get the correct update type for any collection export type Update<T extends keyof CollectionResponses> = CollectionResponses[T] extends AuthSystemFields ? UpdateAuth<CollectionRecords[T]> : UpdateBase<CollectionRecords[T]>`; // 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 = { // Basic fields bool: "boolean", date: DATE_STRING_TYPE_NAME, autodate: AUTODATE_STRING_TYPE_NAME, editor: HTML_STRING_NAME, email: "string", geoPoint: GEOPOINT_TYPE_NAME, text: "string", url: "string", password: "string", number: "number", // Dependent on schema file: (fieldSchema) => fieldSchema.maxSelect && fieldSchema.maxSelect > 1 ? `${FILE_NAME_STRING_NAME}[]` : `${FILE_NAME_STRING_NAME}`, 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; }, // DEPRECATED: PocketBase v0.8 does not have a dedicated user relation 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.type === "autodate" && !fieldSchema.onCreate || fieldSchema.type !== "autodate" && !fieldSchema.required ? "?" : ""; return ` ${fieldName}${required}: ${typeString}`; } function createSelectOptions(recordName, fields) { const selectFields = fields.filter((field) => field.type === "select"); const typestring = selectFields.map((field) => { const name = getOptionEnumName(recordName, field.name); const values = getOptionValues(field); const entries = values.map( (val) => ` "${getSelectOptionEnumName(val)}": "${val}",` ).join("\n"); return `export const ${name} = { ${entries} } as const export type ${name} = typeof ${name}[keyof typeof ${name}] `; }).join("\n"); return typestring; } function getSelectOptionEnumName(val) { if (!isNaN(Number(val))) { return `E${val}`; } else { return val; } } // src/lib.ts function generate(results, options) { const collectionNames = []; const recordTypes = []; const responseTypes = [RESPONSE_TYPE_COMMENT]; results.sort((a, b) => a.name.localeCompare(b.name)).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 includeGeoPoint = containsGeoPoint(results); const fileParts = [ EXPORT_COMMENT, options.sdk && IMPORTS, createCollectionEnum(sortedCollectionNames), ALIAS_TYPE_DEFINITIONS, includeGeoPoint && GEOPOINT_TYPE_DEFINITION, 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), UTILITY_TYPES, options.sdk && TYPED_POCKETBASE_TYPE ]; 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 = `<T${EXPAND_GENERIC_NAME}>`; return `export type ${pascaleName}Response${genericArgsWithDefaults} = Required<${pascaleName}Record${genericArgsForRecord}> & ${systemFields}${expandArgString}`; } // src/index.ts function generateFromSchema(collections, options) { return generate(collections, { sdk: options?.sdk ?? true }); } export { generateFromSchema };