pocketbase-typegen
Version:
Generate pocketbase record types from your database
312 lines (297 loc) • 11.2 kB
JavaScript
// 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
};