Add Nix flake for Zod-PocketBase dev environment and remove doc and
playground
This commit is contained in:
66
src/config.ts
Normal file
66
src/config.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { pascalCase, snakeCase } from "es-toolkit";
|
||||
import { z } from "zod";
|
||||
|
||||
/**
|
||||
* Default config values.
|
||||
*/
|
||||
export const defaultConfig = {
|
||||
ignore: [],
|
||||
nameEnum: (name: string) => snakeCase(name).toUpperCase(),
|
||||
nameEnumField: (collectionName: string, fieldName: string) => `${collectionName}${pascalCase(fieldName)}`,
|
||||
nameEnumSchema: (name: string) => pascalCase(name),
|
||||
nameEnumType: (name: string) => pascalCase(name),
|
||||
nameEnumValues: (name: string) => `${name}Values`,
|
||||
nameRecordSchema: (name: string) => `${pascalCase(name)}Record`,
|
||||
nameRecordType: (name: string) => `${pascalCase(name)}Record`,
|
||||
output: "./zod-pocketbase.ts",
|
||||
};
|
||||
|
||||
/**
|
||||
* Schema for the PocketBase credentials.
|
||||
*/
|
||||
export const Credentials = z.object({
|
||||
adminEmail: z.string().email(),
|
||||
adminPassword: z.string(),
|
||||
url: z.string().url(),
|
||||
});
|
||||
export type Credentials = z.infer<typeof Credentials>;
|
||||
|
||||
/**
|
||||
* Schema for the config file.
|
||||
*/
|
||||
export const Config = z.object({
|
||||
...Credentials.partial().shape,
|
||||
ignore: z.string().array().default(defaultConfig.ignore),
|
||||
nameEnum: z
|
||||
.function(z.tuple([z.string()]), z.string())
|
||||
.optional()
|
||||
.transform((f) => f ?? defaultConfig.nameEnum),
|
||||
nameEnumField: z
|
||||
.function(z.tuple([z.string(), z.string()]), z.string())
|
||||
.optional()
|
||||
.transform((f) => f ?? defaultConfig.nameEnumField),
|
||||
nameEnumSchema: z
|
||||
.function(z.tuple([z.string()]), z.string())
|
||||
.optional()
|
||||
.transform((f) => f ?? defaultConfig.nameEnumSchema),
|
||||
nameEnumType: z
|
||||
.function(z.tuple([z.string()]), z.string())
|
||||
.optional()
|
||||
.transform((f) => f ?? defaultConfig.nameEnumType),
|
||||
nameEnumValues: z
|
||||
.function(z.tuple([z.string()]), z.string())
|
||||
.optional()
|
||||
.transform((f) => f ?? defaultConfig.nameEnumValues),
|
||||
nameRecordSchema: z
|
||||
.function(z.tuple([z.string()]), z.string())
|
||||
.optional()
|
||||
.transform((f) => f ?? defaultConfig.nameRecordSchema),
|
||||
nameRecordType: z
|
||||
.function(z.tuple([z.string()]), z.string())
|
||||
.optional()
|
||||
.transform((f) => f ?? defaultConfig.nameRecordType),
|
||||
output: z.string().default(defaultConfig.output),
|
||||
});
|
||||
export type Config = z.input<typeof Config>;
|
||||
export type ResolvedConfig = z.infer<typeof Config>;
|
||||
120
src/content.ts
Normal file
120
src/content.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import { sortBy } from "es-toolkit";
|
||||
import type { CollectionModel, SchemaField } from "pocketbase";
|
||||
import type { GenerateOpts } from "./server/utils.ts";
|
||||
|
||||
export function stringifyContent(collections: CollectionModel[], opts: GenerateOpts) {
|
||||
function getCollectionSelectFields() {
|
||||
return collections.flatMap((collection) =>
|
||||
collection.schema
|
||||
.filter((field) => field.type === "select")
|
||||
.map((field) => ({ name: opts.nameEnumField(collection.name, field.name), values: (field.options.values ?? []) as string[] })),
|
||||
);
|
||||
}
|
||||
|
||||
function stringifyEnum({ name, values }: SelectField) {
|
||||
const valuesName = opts.nameEnumValues(name);
|
||||
const schemaName = opts.nameEnumSchema(name);
|
||||
const enumName = opts.nameEnum(name);
|
||||
const typeName = opts.nameEnumType(name);
|
||||
return `export const ${valuesName} = [\n\t${values.map((value) => `"${value}"`).join(",\n\t")},\n] as const;\nexport const ${schemaName} = z.enum(${valuesName});\nexport type ${typeName} = z.infer<typeof ${schemaName}>;\nexport const ${enumName} = ${schemaName}.enum;`;
|
||||
}
|
||||
|
||||
function stringifyRecord({ name, schema }: CollectionModel) {
|
||||
const schemaName = opts.nameRecordSchema(name);
|
||||
const typeName = opts.nameRecordType(name);
|
||||
const fields = sortBy(schema, ["name"]).map((field) => stringifyField(field, name));
|
||||
return `export const ${schemaName} = z.object({\n\t...RecordModel.omit({ expand: true }).shape,\n\tcollectionName: z.literal("${name}"),\n\t${fields.join(",\n\t")},\n});\nexport type ${typeName} = z.infer<typeof ${schemaName}>;`;
|
||||
}
|
||||
|
||||
function stringifyField(field: SchemaField, collectionName: string) {
|
||||
let schema: string | undefined;
|
||||
if (field.type === "bool") schema = stringifyBoolField(field);
|
||||
else if (field.type === "date") schema = stringifyDateField(field);
|
||||
else if (field.type === "editor") schema = stringifyEditorField(field);
|
||||
else if (field.type === "email") schema = stringifyEmailField(field);
|
||||
else if (field.type === "file") schema = stringifyFileField(field);
|
||||
else if (field.type === "json") schema = stringifyJsonField(field);
|
||||
else if (field.type === "number") schema = stringifyNumberField(field);
|
||||
else if (field.type === "relation") schema = stringifyRelationField(field);
|
||||
else if (field.type === "select") schema = stringifySelectField(field, collectionName);
|
||||
else if (field.type === "text") schema = stringifyTextField(field);
|
||||
else if (field.type === "url") schema = stringifyUrlField(field);
|
||||
// TODO: manage unknown field type
|
||||
return `${field.name}: ${schema}${field.required ? "" : ".optional()"}`;
|
||||
}
|
||||
|
||||
function stringifyBoolField(_: SchemaField) {
|
||||
return "z.boolean()";
|
||||
}
|
||||
|
||||
function stringifyDateField(_field: SchemaField) {
|
||||
// TODO: implement min and max
|
||||
return "z.string().pipe(z.coerce.date())";
|
||||
}
|
||||
|
||||
function stringifyEditorField(_field: SchemaField) {
|
||||
// TODO: implement convertUrls
|
||||
return "z.string()";
|
||||
}
|
||||
|
||||
function stringifyEmailField(_field: SchemaField) {
|
||||
// TODO: implement exceptDomains and onlyDomains
|
||||
return "z.string().email()";
|
||||
}
|
||||
|
||||
function stringifyFileField({ options: { maxSelect } }: SchemaField) {
|
||||
// TODO: implement maxSize, mimeTypes, protected, thumbs
|
||||
return `z.string()${maxSelect === 1 ? "" : `.array().max(${maxSelect})`}`;
|
||||
}
|
||||
|
||||
function stringifyJsonField(_field: SchemaField) {
|
||||
// TODO: implement maxSize and json schema
|
||||
return "z.any()";
|
||||
}
|
||||
|
||||
function stringifyNumberField({ options: { max, min, noDecimal } }: SchemaField) {
|
||||
return `z.number()${noDecimal ? ".int()" : ""}${min ? `.min(${min})` : ""}${max ? `.max(${max})` : ""}`;
|
||||
}
|
||||
|
||||
function stringifyRelationField({ options, required }: SchemaField) {
|
||||
const { maxSelect, minSelect } = options;
|
||||
// TODO: implement cascadeDelete, displayFields
|
||||
const min = minSelect ? `.min(${minSelect})` : "";
|
||||
const max = maxSelect ? `.max(${maxSelect})` : "";
|
||||
const multiple = maxSelect === 1 ? "" : `.array()${min}${max}`;
|
||||
const isOptional = required || maxSelect !== 1 ? `` : `.transform((id) => id === "" ? undefined : id)`;
|
||||
return `z.string()${isOptional}${multiple}`;
|
||||
}
|
||||
|
||||
function stringifySelectField({ name, options: { maxSelect } }: SchemaField, collectionName: string) {
|
||||
// TODO: implement values
|
||||
return `${opts.nameEnumSchema(opts.nameEnumField(collectionName, name))}${maxSelect === 1 ? "" : `.array().max(${maxSelect})`}`;
|
||||
}
|
||||
|
||||
function stringifyTextField({ options: { max, min } }: SchemaField) {
|
||||
// TODO: implement pattern
|
||||
return `z.string()${min ? `.min(${min})` : ""}${max ? `.max(${max})` : ""}`;
|
||||
}
|
||||
|
||||
function stringifyUrlField(_field: SchemaField) {
|
||||
// TODO: implement exceptDomains and onlyDomains
|
||||
return "z.string().url()";
|
||||
}
|
||||
|
||||
function stringifySchemasEntry({ name }: CollectionModel) {
|
||||
return `["${name}", ${opts.nameRecordSchema(name)}]`;
|
||||
}
|
||||
|
||||
function stringifyService({ name }: CollectionModel) {
|
||||
return `\t\tcollection(idOrName: "${name}"): RecordService<z.input<typeof ${opts.nameRecordSchema(name)}>>;`;
|
||||
}
|
||||
|
||||
return {
|
||||
collectionNames: `[\n\t${collections.map(({ name }) => `"${name}"`).join(",\n\t")},\n]`,
|
||||
enums: getCollectionSelectFields().map(stringifyEnum).join("\n\n"),
|
||||
records: `${collections.map(stringifyRecord).join("\n\n")}\n\nexport const records = new Map<Collection, z.AnyZodObject>([\n\t${collections.map(stringifySchemasEntry).join(",\n\t")},\n]);`,
|
||||
services: collections.map(stringifyService).join("\n"),
|
||||
};
|
||||
}
|
||||
|
||||
export type SelectField = { name: string; values: string[] };
|
||||
29
src/helpers.ts
Normal file
29
src/helpers.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import type { default as Pocketbase, SendOptions } from "pocketbase";
|
||||
import { fullListOptionsFrom, optionsFrom } from "./options.js";
|
||||
import type { AnyZodRecord, RecordFullListOpts, RecordIdRef, RecordRef, RecordSlugRef } from "./types.ts";
|
||||
import { AnyRecordsList, type RecordsList } from "./schemas.ts";
|
||||
|
||||
export function helpersFrom({ fetch, pocketbase }: HelpersFromOpts) {
|
||||
async function getRecord<C extends string, S extends AnyZodRecord>(ref: RecordSlugRef<C>, opts: GetRecordOpts<S>): Promise<S["_output"]>;
|
||||
async function getRecord<C extends string, S extends AnyZodRecord>(ref: RecordIdRef<C>, opts: GetRecordOpts<S>): Promise<S["_output"]>;
|
||||
async function getRecord<C extends string, S extends AnyZodRecord>(ref: RecordRef<C>, opts: GetRecordOpts<S>) {
|
||||
const { schema } = opts;
|
||||
const sdkOpts = { ...optionsFrom(schema), ...(fetch ? { fetch } : {}) };
|
||||
const unsafeRecord = await ("id" in ref
|
||||
? pocketbase.collection(ref.collection).getOne(ref.id, sdkOpts)
|
||||
: pocketbase.collection(ref.collection).getFirstListItem(`slug = "${ref.slug}"`, sdkOpts));
|
||||
return schema.parseAsync(unsafeRecord);
|
||||
}
|
||||
|
||||
async function getRecords<C extends string, S extends AnyZodRecord>(collection: C, opts: GetRecordsOpts<S>): Promise<RecordsList<S>> {
|
||||
const { schema, ...otherOpts } = opts;
|
||||
const sdkOpts = { ...fullListOptionsFrom(schema, otherOpts), ...(fetch ? { fetch } : {}) };
|
||||
const recordsList = await pocketbase.collection(collection).getList(sdkOpts.page, sdkOpts.perPage, sdkOpts);
|
||||
return AnyRecordsList.extend({ items: schema.array() }).parseAsync(recordsList);
|
||||
}
|
||||
|
||||
return { getRecord, getRecords };
|
||||
}
|
||||
export type GetRecordOpts<S extends AnyZodRecord> = { schema: S };
|
||||
export type GetRecordsOpts<S extends AnyZodRecord> = RecordFullListOpts<S> & { schema: S };
|
||||
export type HelpersFromOpts = { fetch?: SendOptions["fetch"]; pocketbase: Pocketbase };
|
||||
6
src/index.ts
Normal file
6
src/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export * from "./config.js";
|
||||
export * from "./helpers.js";
|
||||
export * from "./options.js";
|
||||
export * from "./schemas.js";
|
||||
export * from "./types.ts";
|
||||
export * from "./utils.js";
|
||||
97
src/options.ts
Normal file
97
src/options.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import { z, type AnyZodObject, type ZodTypeAny } from "zod";
|
||||
import type { AnyZodRecord, RecordFullListOpts, RecordListOpts } from "./types.ts";
|
||||
|
||||
/**
|
||||
* Extends the given schema with the given expansion.
|
||||
* @param schema - The original schema
|
||||
* @param shape - The shape of the expansion
|
||||
* @returns A new schema extended with the given expansion
|
||||
*/
|
||||
export function expandFrom<S extends AnyZodRecord>(schema: S) {
|
||||
return expandFromRec(schema)
|
||||
.sort((k1, k2) => (k1 < k2 ? -1 : 1))
|
||||
.join(",");
|
||||
}
|
||||
|
||||
/**
|
||||
* Extends the given schema with the given expansion.
|
||||
* @param schema - The original schema
|
||||
* @param shape - The shape of the expansion
|
||||
* @returns A new schema extended with the given expansion
|
||||
*/
|
||||
export function fieldsFrom<S extends AnyZodRecord>(schema: S) {
|
||||
return fieldsFromRec(schema)
|
||||
.sort((k1, k2) => (k1 < k2 ? -1 : 1))
|
||||
.join(",");
|
||||
}
|
||||
|
||||
/**
|
||||
* Extends the given schema with the given expansion.
|
||||
* @param schema - The original schema
|
||||
* @param shape - The shape of the expansion
|
||||
* @returns A new schema extended with the given expansion
|
||||
*/
|
||||
export function optionsFrom<S extends AnyZodRecord>(schema: S) {
|
||||
return { expand: expandFrom(schema), fields: fieldsFrom(schema) };
|
||||
}
|
||||
|
||||
/**
|
||||
* Extends the given schema with the given expansion.
|
||||
* @param schema - The original schema
|
||||
* @param shape - The shape of the expansion
|
||||
* @returns A new schema extended with the given expansion
|
||||
*/
|
||||
export function listOptionsFrom<S extends AnyZodRecord>(schema: S, opts: RecordListOpts<S>) {
|
||||
const { page = 1, perPage = 30, ...rest } = opts;
|
||||
return { ...optionsFrom(schema), page, perPage, ...rest };
|
||||
}
|
||||
|
||||
/**
|
||||
* Extends the given schema with the given expansion.
|
||||
* @param schema - The original schema
|
||||
* @param shape - The shape of the expansion
|
||||
* @returns A new schema extended with the given expansion
|
||||
*/
|
||||
export function fullListOptionsFrom<S extends AnyZodRecord>(schema: S, opts: RecordFullListOpts<S>) {
|
||||
const { page = 1, perPage = 200, skipTotal = true, ...rest } = opts;
|
||||
return listOptionsFrom(schema, { page, perPage, skipTotal, ...rest });
|
||||
}
|
||||
|
||||
function expandFromRec<S extends ZodTypeAny>(schema: S, prefix = "") {
|
||||
let expands: string[] = [];
|
||||
const shape = getObjectSchemaDescendant(schema)?.shape;
|
||||
if (!shape || !("expand" in shape)) return [];
|
||||
for (const [key, value] of Object.entries(getObjectSchemaDescendant(shape.expand)!.shape)) {
|
||||
expands = [...expands, `${prefix}${key}`];
|
||||
if (hasObjectSchemaDescendant(value)) expands = [...expands, ...expandFromRec(getObjectSchemaDescendant(value)!, `${prefix}${key}.`)];
|
||||
}
|
||||
return expands;
|
||||
}
|
||||
|
||||
function fieldsFromRec<S extends z.ZodTypeAny>(schema: S, prefix = "") {
|
||||
let fields: string[] = [];
|
||||
const shape = getObjectSchemaDescendant(schema)?.shape;
|
||||
if (!shape) return [];
|
||||
for (const [key, value] of Object.entries(shape)) {
|
||||
fields = [
|
||||
...fields,
|
||||
...(hasObjectSchemaDescendant(value) ? fieldsFromRec(getObjectSchemaDescendant(value)!, `${prefix}${key}.`) : [`${prefix}${key}`]),
|
||||
];
|
||||
}
|
||||
return fields.sort((k1, k2) => (k1 < k2 ? -1 : 1));
|
||||
}
|
||||
|
||||
function hasObjectSchemaDescendant(value: unknown): value is z.ZodTypeAny {
|
||||
if (value instanceof z.ZodEffects) return hasObjectSchemaDescendant(value.innerType());
|
||||
if (value instanceof z.ZodArray) return hasObjectSchemaDescendant(value.element);
|
||||
if (value instanceof z.ZodOptional) return hasObjectSchemaDescendant(value.unwrap());
|
||||
return value instanceof z.ZodObject;
|
||||
}
|
||||
|
||||
function getObjectSchemaDescendant<S extends z.ZodTypeAny>(schema: S): AnyZodObject | undefined {
|
||||
if (schema instanceof z.ZodEffects) return getObjectSchemaDescendant(schema.innerType());
|
||||
if (schema instanceof z.ZodArray) return getObjectSchemaDescendant(schema.element);
|
||||
if (schema instanceof z.ZodOptional) return getObjectSchemaDescendant(schema.unwrap());
|
||||
if (schema instanceof z.ZodObject) return schema;
|
||||
return;
|
||||
}
|
||||
71
src/schemas.ts
Normal file
71
src/schemas.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { type AnyZodObject, type objectUtil, z, type ZodEffects, type ZodObject, ZodOptional, type ZodRawShape } from "zod";
|
||||
import type { AnyZodRecord, HasRequiredKeys, ZodRecordKeys } from "./types.ts";
|
||||
|
||||
/**
|
||||
* Records list schema.
|
||||
*/
|
||||
export const AnyRecordsList = z.object({
|
||||
items: z.any().array(),
|
||||
page: z.number().int().min(1),
|
||||
perPage: z.number().int().min(1),
|
||||
totalItems: z.number().int().min(-1),
|
||||
totalPages: z.number().int().min(-1),
|
||||
});
|
||||
|
||||
/**
|
||||
* Extends the given schema with the given expansion.
|
||||
* @param schema - The original schema
|
||||
* @param shape - The shape of the expansion
|
||||
* @returns A new schema extended with the given expansion
|
||||
*/
|
||||
export function expand<S extends AnyZodObject, E extends ZodRawShape>(schema: S, shape: E) {
|
||||
const isExpandOptional = Object.entries(shape).every(([, value]) => value instanceof z.ZodOptional);
|
||||
return z
|
||||
.object({ ...schema.shape, expand: isExpandOptional ? z.object(shape).optional() : z.object(shape) })
|
||||
.transform(({ expand, ...rest }) => ({ ...rest, ...(expand ?? {}) })) as ZodObjectExpand<S, E>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Picks the given keys from the given schema.
|
||||
* @param schema - The original schema
|
||||
* @param keys - The keys to keep
|
||||
* @returns A new schema with only the given keys
|
||||
*/
|
||||
export function pick<S extends AnyZodObject, K extends ZodRecordKeys<S>[]>(schema: S, keys: K) {
|
||||
return schema.pick(Object.fromEntries(keys.map((key) => [key, true]))) as ZodObjectPick<S, K>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Picks the given keys from the given schema and extends it with the given expansion.
|
||||
* @param schema - The original schema
|
||||
* @param keys - The keys to keep
|
||||
* @param shape - The shape of the expansion
|
||||
* @returns A new schema with only the given keys
|
||||
*/
|
||||
export function select<S extends AnyZodObject, K extends ZodRecordKeys<S>[], E extends ZodRawShape>(
|
||||
schema: S,
|
||||
keys: K,
|
||||
shape: E,
|
||||
): ZodObjectExpand<ZodObjectPick<S, K>, E>;
|
||||
export function select<S extends AnyZodObject, K extends ZodRecordKeys<S>[]>(schema: S, keys: K): ZodObjectPick<S, K>;
|
||||
export function select<S extends AnyZodObject, K extends ZodRecordKeys<S>[], E extends ZodRawShape | undefined>(
|
||||
schema: S,
|
||||
keys: K,
|
||||
shape?: E,
|
||||
) {
|
||||
return shape ? expand(pick(schema, keys), shape) : pick(schema, keys);
|
||||
}
|
||||
|
||||
export type ZodObjectExpand<S extends AnyZodObject, E extends ZodRawShape> =
|
||||
S extends ZodObject<infer T, infer U, infer C>
|
||||
? ZodEffects<
|
||||
ZodObject<objectUtil.extendShape<T, { expand: HasRequiredKeys<E> extends true ? ZodObject<E> : ZodOptional<ZodObject<E>> }>, U, C>,
|
||||
ZodObject<objectUtil.extendShape<T, E>, U, C>["_output"]
|
||||
>
|
||||
: never;
|
||||
|
||||
export type ZodObjectPick<S extends AnyZodObject, K extends ZodRecordKeys<S>[]> =
|
||||
S extends ZodObject<infer T, infer U, infer C> ? ZodObject<Pick<T, K[number]>, U, C> : never;
|
||||
|
||||
export type AnyRecordsList = z.infer<typeof AnyRecordsList>;
|
||||
export type RecordsList<S extends AnyZodRecord> = Omit<AnyRecordsList, "items"> & { items: S["_output"][] };
|
||||
13
src/sdk.ts
Normal file
13
src/sdk.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import Pocketbase from "pocketbase";
|
||||
import type { Credentials } from "./config.ts";
|
||||
|
||||
let adminPocketbase: Pocketbase;
|
||||
|
||||
export async function getPocketbase({ adminEmail, adminPassword, url }: Credentials) {
|
||||
if (!adminPocketbase) {
|
||||
const pocketbase = new Pocketbase(url);
|
||||
await pocketbase.admins.authWithPassword(adminEmail, adminPassword);
|
||||
adminPocketbase = pocketbase;
|
||||
}
|
||||
return adminPocketbase;
|
||||
}
|
||||
91
src/server/cli.ts
Normal file
91
src/server/cli.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { Config, type ResolvedConfig } from "../config.ts";
|
||||
import pkg from "../../package.json" with { type: "json" };
|
||||
import { loadConfig } from "c12";
|
||||
import { defineCommand, runMain } from "citty";
|
||||
import { cancel, group, intro, log, outro, confirm, text, spinner, multiselect, isCancel } from "@clack/prompts";
|
||||
import { fetchCollections } from "../utils.ts";
|
||||
import { generate } from "./utils.ts";
|
||||
import { existsSync } from "node:fs";
|
||||
|
||||
async function getConfig() {
|
||||
const { config } = await loadConfig({ name: "zod-pocketbase", rcFile: false, dotenv: true });
|
||||
const { ZOD_POCKETBASE_ADMIN_EMAIL: adminEmail, ZOD_POCKETBASE_ADMIN_PASSWORD: adminPassword, ZOD_POCKETBASE_URL: url } = process.env;
|
||||
const result = Config.safeParse({ ...config, adminEmail, adminPassword, url });
|
||||
if (!result.success) {
|
||||
log.error("Invalid fields in your config file.");
|
||||
onCancel();
|
||||
}
|
||||
return result.data!;
|
||||
}
|
||||
|
||||
function onCancel() {
|
||||
cancel("Operation cancelled.");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
async function selectCollections(config: ResolvedConfig) {
|
||||
const credentialPrompts = {
|
||||
url: () => text({ message: "What is the url of your pocketbase instance?", initialValue: config.url ?? "" }),
|
||||
adminEmail: () => text({ message: "What is your admin's email?", initialValue: config.adminEmail ?? "" }),
|
||||
adminPassword: () => text({ message: "What is your admin's password?", initialValue: config.adminPassword ?? "" }),
|
||||
};
|
||||
const credentials = await group(credentialPrompts, { onCancel });
|
||||
const s = spinner();
|
||||
s.start("Fetching collections...");
|
||||
try {
|
||||
const allCollections = await fetchCollections(credentials);
|
||||
s.stop("Successfully fetched collections.");
|
||||
const collectionNames = await multiselect({
|
||||
message: "What collections do you want to generate schemas for?",
|
||||
options: allCollections.map(({ name: value }) => ({ value })),
|
||||
initialValues: allCollections.filter(({ name }) => !config.ignore.includes(name)).map(({ name }) => name),
|
||||
});
|
||||
if (isCancel(collectionNames)) onCancel();
|
||||
return allCollections.filter(({ name }) => (collectionNames as string[]).includes(name));
|
||||
} catch {
|
||||
s.stop("Failed to fetch collections.Please check your credentials and try again.");
|
||||
return selectCollections(config);
|
||||
}
|
||||
}
|
||||
|
||||
async function setGeneratedFilePath(config: ResolvedConfig) {
|
||||
const output = await text({
|
||||
message: "What is the generated file path?",
|
||||
initialValue: config.output,
|
||||
validate: (value) => {
|
||||
if (!value) return "Please enter a path.";
|
||||
if (value[0] !== ".") return "Please enter a relative path.";
|
||||
return;
|
||||
},
|
||||
});
|
||||
if (isCancel(output)) onCancel();
|
||||
|
||||
if (existsSync(output as string)) {
|
||||
const confirmed = await confirm({ message: "The file already exists, would you like to overwrite it?" });
|
||||
if (isCancel(confirmed)) onCancel();
|
||||
if (!confirmed) return setGeneratedFilePath(config);
|
||||
}
|
||||
|
||||
return output as string;
|
||||
}
|
||||
|
||||
const main = defineCommand({
|
||||
meta: { name: "zod-pocketbase", version: pkg.version, description: "Generate Zod schemas for your pocketbase instance." },
|
||||
run: async () => {
|
||||
intro(`ZOD POCKETBASE`);
|
||||
const config = await getConfig();
|
||||
const collections = await selectCollections(config);
|
||||
const output = await setGeneratedFilePath(config);
|
||||
|
||||
const s = spinner();
|
||||
s.start("Generating your schemas...");
|
||||
await generate(collections, { ...config, output });
|
||||
s.stop("Schemas successfully generated.");
|
||||
|
||||
outro("Operation completed.");
|
||||
},
|
||||
});
|
||||
|
||||
runMain(main);
|
||||
1
src/server/index.ts
Normal file
1
src/server/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./utils.js";
|
||||
19
src/server/utils.ts
Normal file
19
src/server/utils.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import type { CollectionModel } from "pocketbase";
|
||||
import { stringifyContent } from "../content.js";
|
||||
import type { ResolvedConfig } from "../config.ts";
|
||||
|
||||
export async function generate(collections: CollectionModel[], opts: GenerateOpts) {
|
||||
const stub = readFileSync(resolve(dirname(fileURLToPath(import.meta.url)), "../../assets/stubs/index.ts"), "utf-8");
|
||||
const { collectionNames, enums, records, services } = stringifyContent(collections, opts);
|
||||
const content = stub
|
||||
.replace("@@_COLLECTION_NAMES_@@", collectionNames)
|
||||
.replace("@@_ENUMS_@@", enums)
|
||||
.replace("@@_RECORDS_@@", records)
|
||||
.replace("@@_SERVICES_@@", services);
|
||||
mkdirSync(dirname(opts.output), { recursive: true });
|
||||
writeFileSync(opts.output, content);
|
||||
}
|
||||
export type GenerateOpts = Omit<ResolvedConfig, "adminEmail" | "adminPassword" | "ignore" | "url">;
|
||||
31
src/types.ts
Normal file
31
src/types.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import type { AnyZodObject, ZodEffects, ZodOptional, ZodRawShape, ZodTypeAny } from "zod";
|
||||
|
||||
export type AnyZodRecord = AnyZodObject | ZodEffects<AnyZodObject>;
|
||||
|
||||
export type RecordFullListOpts<S extends AnyZodRecord> = RecordListOpts<S> & { batch?: number };
|
||||
|
||||
export type RecordListOpts<S extends AnyZodRecord> = {
|
||||
filter?: string;
|
||||
page?: number;
|
||||
perPage?: number;
|
||||
skipTotal?: boolean;
|
||||
sort?: ZodRecordSort<S>;
|
||||
};
|
||||
|
||||
export type RecordIdRef<C extends string> = { collection: C; id: string };
|
||||
export type RecordSlugRef<C extends string> = { collection: C; slug: string };
|
||||
export type RecordRef<C extends string> = RecordIdRef<C> | RecordSlugRef<C>;
|
||||
|
||||
export type ZodRecordKeys<S extends AnyZodRecord> = Extract<keyof S["_input"], string>;
|
||||
|
||||
export type ZodRecordMainKeys<S extends AnyZodRecord> = Exclude<ZodRecordKeys<S>, "expand">;
|
||||
|
||||
export type ZodRecordSort<S extends AnyZodRecord> = `${"+" | "-"}${ZodRecordMainKeys<S>}` | "@random";
|
||||
|
||||
type RequiredKeysOf<S extends ZodRawShape> = Exclude<
|
||||
{
|
||||
[Key in keyof S]: S[Key] extends ZodOptional<ZodTypeAny> ? never : Key;
|
||||
}[keyof S],
|
||||
undefined
|
||||
>;
|
||||
export type HasRequiredKeys<S extends ZodRawShape> = RequiredKeysOf<S> extends never ? false : true;
|
||||
10
src/utils.ts
Normal file
10
src/utils.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { sortBy } from "es-toolkit";
|
||||
import type { CollectionModel } from "pocketbase";
|
||||
import { getPocketbase } from "./sdk.js";
|
||||
import type { Credentials } from "./config.ts";
|
||||
|
||||
export async function fetchCollections(credentials: Credentials): Promise<CollectionModel[]> {
|
||||
const pocketbase = await getPocketbase(credentials);
|
||||
const collections = await pocketbase.collections.getFullList();
|
||||
return sortBy(collections, ["name"]);
|
||||
}
|
||||
Reference in New Issue
Block a user