import { PocketBaseFieldType, APIRuleType } from './index.cjs'; export { AutodateField, AutodateFieldOptions, BoolField, ByteSize, CollectionConfig, DateField, DateFieldOptions, EditorField, EmailField, EnumFromArray, FIELD_METADATA_KEY, FieldMetadata, FileField, FileFieldOptions, FilesField, FilesFieldOptions, GeoPointField, JSONField, JSONFieldOptions, NumberField, NumberFieldOptions, PermissionSchema, PermissionTemplate, PermissionTemplateConfig, PermissionTemplates, RelationConfig, RelationField, RelationsConfig, RelationsField, RuleExpression, SelectField, SelectFieldOptions, TextField, TextFieldOptions, URLField, ViewCollectionConfig, ViewPermissionSchema, baseSchema, dedentSql, defineCollection, defineView, extractFieldMetadata, extractRelationMetadata, resolveTemplate, sql, validateViewQuery } from './index.cjs'; import { z } from 'zod'; /** * Shared types for migration tool */ interface FieldDefinition { name: string; id: string; zodType?: z.ZodTypeAny; type: PocketBaseFieldType; required: boolean; unique?: boolean; options?: Record; relation?: { collection: string; cascadeDelete?: boolean; maxSelect?: number; minSelect?: number; displayFields?: string[] | null; }; } interface CollectionSchema { name: string; type: "base" | "auth" | "view"; /** * Pre-generated collection ID for use in migrations * Format: pb_ followed by 15 alphanumeric lowercase characters * Special case: "_pb_users_auth_" for users collection * This ID is generated during migration creation to avoid runtime lookups */ id?: string; /** * SQL SELECT statement backing a view collection (type: "view" only) * PocketBase derives the collection's fields from this query, so `fields` * is not emitted into migrations for view collections */ viewQuery?: string; fields: FieldDefinition[]; indexes?: string[]; rules?: { listRule?: string | null; viewRule?: string | null; createRule?: string | null; updateRule?: string | null; deleteRule?: string | null; manageRule?: string | null; }; permissions?: { listRule?: string | null; viewRule?: string | null; createRule?: string | null; updateRule?: string | null; deleteRule?: string | null; manageRule?: string | null; }; } interface SchemaDefinition { collections: Map; } interface SchemaSnapshot { version: string; timestamp: string; collections: Map; } interface FieldChange { property: string; oldValue: any; newValue: any; } interface FieldModification { fieldName: string; currentDefinition: any; newDefinition: FieldDefinition; changes: FieldChange[]; } interface RuleUpdate { ruleType: "listRule" | "viewRule" | "createRule" | "updateRule" | "deleteRule" | "manageRule"; oldValue: string | null; newValue: string | null; } /** * Permission change tracking for migrations */ interface PermissionChange { ruleType: APIRuleType; oldValue: string | null; newValue: string | null; } /** * View query change tracking for view collections * Applied in place so the collection ID stays stable */ interface ViewQueryUpdate { oldValue: string | null; newValue: string; } interface CollectionModification { collection: string; fieldsToAdd: FieldDefinition[]; fieldsToRemove: any[]; fieldsToModify: FieldModification[]; indexesToAdd: string[]; indexesToRemove: string[]; rulesToUpdate: RuleUpdate[]; permissionsToUpdate: PermissionChange[]; /** * Set when a view collection's SQL query changed (view collections only) */ viewQueryUpdate?: ViewQueryUpdate; } interface SchemaDiff { collectionsToCreate: CollectionSchema[]; collectionsToDelete: any[]; collectionsToModify: CollectionModification[]; /** * Map of existing collection names to their IDs from the previous snapshot * Used to resolve relation field references to existing collections */ existingCollectionIds?: Map; } /** * Represents a single collection operation for file splitting * Each operation will generate a separate migration file */ interface CollectionOperation { /** * Type of operation being performed */ type: "create" | "modify" | "delete"; /** * Collection being operated on * For create/modify: CollectionSchema * For delete: collection name as string */ collection: CollectionSchema | string; /** * Modifications to apply (only for 'modify' operations) */ modifications?: CollectionModification; /** * Timestamp for this operation's migration file */ timestamp: string; } /** * Configuration options for schema discovery and parsing */ interface SchemaAnalyzerConfig { /** * Directory containing schema files (source or compiled) * Can be absolute or relative to workspaceRoot */ schemaDir: string; /** * Workspace root directory for resolving relative paths * Defaults to process.cwd() */ workspaceRoot?: string; /** * File patterns to exclude from schema discovery * Defaults to ['base.ts', 'index.ts', 'permissions.ts', 'permission-templates.ts'] */ excludePatterns?: string[]; /** * File extensions to include in schema discovery * Defaults to ['.ts', '.js'] */ includeExtensions?: string[]; /** * Whether to use compiled JavaScript files instead of TypeScript source * When true, looks for .js files; when false, looks for .ts files * Defaults to true (use compiled files for dynamic import) */ useCompiledFiles?: boolean; /** * Custom path transformation function for converting source paths to import paths * Useful for monorepo setups where source and dist directories differ * If not provided, uses the schemaDir directly */ pathTransformer?: (sourcePath: string) => string; } /** * Converts a Zod schema to a CollectionSchema interface * * @param collectionName - The name of the collection * @param zodSchema - The Zod object schema * @returns CollectionSchema definition */ declare function convertZodSchemaToCollectionSchema(collectionName: string, zodSchema: z.ZodObject): CollectionSchema; /** * Discovers schema files in the specified directory * Filters based on configuration patterns * * @param config - Schema analyzer configuration * @returns Array of schema file paths (without extension) */ declare function discoverSchemaFiles(config: SchemaAnalyzerConfig): string[]; /** * Schema Analyzer - Parses Zod schemas and extracts field definitions * * This module provides a standalone, configurable schema analyzer that can be used * by consumer projects to parse Zod schemas and convert them to PocketBase collection schemas. */ /** * Parses schema files and returns a SchemaDefinition * Main entry point for the Schema Analyzer * * A file contributes a collection iff one of its exports is a Zod object * whose description carries collection metadata (what defineCollection()/ * defineView() produce). Files without such an export are skipped with a * warning; a file with more than one, or two files declaring the same * collection name, are errors. * * @param config - Schema analyzer configuration * @returns Complete SchemaDefinition with all collections */ declare function parseSchemaFiles(config: SchemaAnalyzerConfig): Promise; interface AppliedMigration { /** Filename as PocketBase recorded it, e.g. "1712345678_created_Posts.js" */ file: string; /** Unix seconds from `_migrations.applied`, when the column has a value */ applied?: number; } interface AppliedMigrationsSource { /** Where the list came from — a database path, or a caller-supplied label */ origin: string; /** Every row, in application order */ entries: AppliedMigration[]; /** * Entries PocketBase's own Go core migrations contributed (`*.go`). They * never correspond to a file in a pb_migrations directory, so they are * kept separate from `entries` rather than reported as missing. */ coreEntries: AppliedMigration[]; } /** * Raised when an applied-migrations list was requested but could not be read. * Callers that treat the list as optional should catch this and fall back to * "assume everything on disk is applied". */ declare class AppliedMigrationsError extends Error { readonly source?: string; readonly originalError?: Error; constructor(message: string, source?: string, originalError?: Error); } /** * Builds a source from an explicit list of filenames (paths are reduced to * their basename, so a caller can pass either). */ declare function appliedMigrationsFromList(files: string[], origin?: string): AppliedMigrationsSource; /** * The pb_data directory PocketBase uses alongside a pb_migrations directory. * Both live under the PocketBase working directory, so pb_data is a sibling. */ declare function defaultDataDirectory(migrationsPath: string): string; /** * Reads `_migrations` out of a PocketBase SQLite database. * * @param dataPathOrFile - A pb_data directory or a data.db file * @throws AppliedMigrationsError when the runtime, file, or table is unusable */ declare function readAppliedMigrations(dataPathOrFile: string): AppliedMigrationsSource; /** * Best-effort read: returns null instead of throwing when there is simply no * database to read (the common case for a checkout that has never run * PocketBase). Genuine failures — an unreadable file, a missing table, a * runtime without node:sqlite — still throw, because silently assuming * "everything is applied" is the drift this module exists to catch. */ declare function readAppliedMigrationsIfPresent(dataPathOrFile: string): AppliedMigrationsSource | null; /** * Replay planning — which migration files reconstruct the current state * * Without knowledge of PocketBase's `_migrations` table, replay assumes every * file on disk has been applied. That is right for the common case and wrong * in exactly the situations that matter: a migration written but not yet run, * or a file removed from disk after it ran. Both produce a reconstructed state * the database was never in, and therefore a wrong diff. * * Given an applied-migrations list (see `applied-migrations.ts`), this module * turns a pb_migrations directory into a plan: * * - `filesToReplay` — the applied prefix, starting at the newest *applied* * snapshot, which is the checkpoint replay should start from * - `pending` — on disk, never applied * - `missing` — applied, no longer on disk * - `outOfOrder` — pending files whose timestamp sits behind an already * applied one, so they will be applied out of authoring order */ /** A migration file discovered on disk */ interface DiscoveredMigration { /** Absolute path */ path: string; /** Basename, which is what `_migrations.file` stores */ name: string; timestamp: number; /** PocketBase's own `*_collections_snapshot.js` files */ isSnapshot: boolean; } interface MigrationPlan { /** The snapshot the replay starts from, when the directory has one */ snapshotFile: string | null; /** Absolute paths to execute, in timestamp order */ filesToReplay: string[]; /** Basenames present on disk but absent from `_migrations` */ pending: string[]; /** Basenames recorded in `_migrations` with no file on disk */ missing: string[]; /** * Basenames from `pending` whose timestamp precedes an already-applied * migration. PocketBase will still run them, but after migrations authored * later — so the state they produce depends on apply order. */ outOfOrder: string[]; /** False when no applied list was supplied: every file is assumed applied */ appliedKnown: boolean; /** Where the applied list came from, when there was one */ appliedOrigin?: string; /** Number of JS migrations recorded as applied */ appliedCount: number; /** True when disk and `_migrations` agree exactly */ inSync: boolean; } interface PlanOptions { /** * The applied-migrations list. A plain array of filenames is accepted as a * shorthand. Omit (or pass null) to assume every file on disk is applied. */ applied?: AppliedMigrationsSource | string[] | null; } /** * Lists every timestamped `.js` migration in a directory, in timestamp order. * Returns an empty list when the directory does not exist. */ declare function discoverMigrations(migrationsPath: string): DiscoveredMigration[]; /** * Builds the replay plan for a migrations directory. * * Without an applied list this reproduces the directory's default replay * window — newest snapshot plus everything after it — so the plan is a * drop-in for the previous file selection. */ declare function planMigrationReplay(migrationsPath: string, options?: PlanOptions): MigrationPlan; /** * Runtime field constructors matching the PocketBase JSVM globals * * In the PocketBase JSVM, migrations construct fields via `new Field({...})` * or typed constructors like `new TextField({...})`. Instances are plain * mutable objects; migrations freely assign properties after construction * (e.g. `field.max = 500`). * * The constructors deliberately add no default properties beyond `type`: * the resulting state must match what the migration file literally declares, * the same data the literal declares, materialized as a real object. */ /** * Generic field constructor: `new Field({type: "number", ...})`. * PocketBase's own generated migrations use this form with an explicit type. */ declare class Field { [key: string]: any; constructor(data?: Record); } /** * FieldsList — runtime equivalent of a PocketBase collection's `fields` * * Mirrors the methods the JSVM exposes on `collection.fields`: * add / addAt / removeById / removeByName / getById / getByName, plus * read-side iteration. Field entries are live objects — mutations through * getByName() are visible when the collection is saved, exactly like the * generator's `const f = collection.fields.getByName("x"); f.max = 500;` * output expects. */ declare class FieldsList { private items; constructor(fields?: any[]); get length(): number; /** * Appends fields; a field that matches an existing entry replaces it in * place (position preserved), matching PocketBase upsert semantics. */ add(...fields: any[]): void; /** * Inserts fields at a position. A field matching an already-present entry is * moved: the old entry is removed first, then the field is inserted at * the requested position. */ addAt(position: number, ...fields: any[]): void; /** * Materializes an incoming field and locates the entry it replaces. * * PocketBase's rule (documented on `FieldsList.Add` in types.d.ts): match by * id, "or by their name if the new field doesn't have an explicit id", and * autogenerate a missing id from the name. Matching by id alone turns the * idiomatic `fields.add(new TextField({name: "title", max: 500}))` — no id, * meant to rewrite `title` — into a second field also called `title`, which * PocketBase would reject on save but the engine used to accept, corrupting * the reconstructed state. * * On a name match the *existing* id is kept rather than replaced with the * derived one. For a collection PocketBase authored the two are identical * (its auto-ids already are ``); they diverge only when * the stored field carries a hand-assigned id, and there preserving it keeps * a later `removeById`/`getById` in the same migration working. */ private resolveIncoming; removeById(id: string): void; removeByName(name: string): void; /** * Null rather than undefined for a miss: these two return a Go `Field` * interface, and goja surfaces a nil interface to JavaScript as `null`. A * migration written as `if (collection.fields.getByName("x") === null)` has * to take the same branch here as it does in production. */ getById(id: string): Field | null; getByName(name: string): Field | null; at(index: number): Field | undefined; find(predicate: (field: Field, index: number) => boolean): Field | undefined; filter(predicate: (field: Field, index: number) => boolean): Field[]; map(mapper: (field: Field, index: number) => T): T[]; forEach(callback: (field: Field, index: number) => void): void; [Symbol.iterator](): Iterator; /** Replaces the whole list (used by unmarshal({fields: [...]}, collection)) */ replaceAll(fields: any[]): void; /** * Plain objects, in order — copied deeply, so option arrays like a select * field's `values` are not shared with the live list. A shallow spread let a * snapshot handed to the diff engine alias engine state, where an in-place * sort or push downstream would silently rewrite the collection it came from. */ serialize(): Record[]; private indexOfId; } /** * Runtime Collection class matching the PocketBase JSVM global * * `new Collection({...})` in a migration produces one of these. All data * properties are copied verbatim (auth options, templates, etc. survive a * snapshot import round-trip); `fields` becomes a live FieldsList and * `indexes` a real array so `push`/`findIndex`/`splice` work natively. * * No rule properties are defaulted: the serialized collection must contain * exactly what the migration declared, so engine output stays byte-parity * with static analysis of the same literals. */ declare class Collection { [key: string]: any; id: string; name: string; type: string; system: boolean; fields: FieldsList; indexes: string[]; constructor(data?: RawCollection); /** PocketBase-shaped plain object (what a snapshot array entry looks like) */ serialize(): RawCollection; } /** * Record simulation — an in-memory row store per collection * * Schema reconstruction never needs this: a migration that seeds or rewrites * data does not change the shape of a collection. It matters for the other * job the engine can do — running a hand-written data migration before it * touches production and seeing what it actually does. * * Enabled with `records: "simulate"`. Left off, `Record` and the data-layer * `app.*` methods stay the inert stubs they have always been, so schema-only * replay is unchanged. * * The store lives on `CollectionStore`, which means it inherits the runner's * transaction semantics for free: a migration that throws halfway through a * data rewrite leaves neither schema nor records behind. */ /** * The `Record` global a migration sees. Mirrors `core.Record`'s JSVM surface: * `get`/`set` plus the typed getters, `load`, `publicExport`, `originalCopy`. * * Arbitrary property access (`record.title`) is deliberately *not* supported — * `core.Record` is a Go struct and does not support it either, so a migration * that relies on it would fail in PocketBase. */ declare class RecordModel { private data; private original; private readonly owner; /** Set by setPassword(); PocketBase stores a hash, the simulation the value */ private password?; constructor(collection: Collection, data?: Record); get id(): string; set id(value: string); collection(): Collection; /** PocketBase names a collection's table after the collection */ tableName(): string; get(key: string): unknown; set(key: string, value: unknown): void; getString(key: string): string; getBool(key: string): boolean; getInt(key: string): number; getFloat(key: string): number; getDateTime(key: string): string; getStringSlice(key: string): string[]; /** Bulk assignment, as `record.load({...})` does in the JSVM */ load(data: Record): void; setPassword(password: string): void; validatePassword(password: string): boolean; /** The record as it was last read from (or written to) the store */ originalCopy(): RecordModel; /** * Deep, independent copy. `owner` rebinds the copy to another Collection * instance (what a transaction clone needs); everything the getters cannot * reach — the pre-modification `original` and the password `setPassword()` * stored — is carried over, so a copy behaves like the record it came from. */ clone(owner?: Collection): RecordModel; /** Plain object, the shape `publicExport()` returns in the JSVM */ publicExport(): Record; /** Every stored value, including system columns */ export(): Record; /** Called by the store once a save has committed */ markPersisted(): void; /** Fields the record carries that the collection does not declare */ undeclaredFields(): string[]; } /** * Rows, keyed by collection id then record id. Insertion order is preserved * so unsorted queries come back the way they went in. */ declare class RecordStore { private byCollection; save(record: RecordModel): RecordModel; delete(record: RecordModel): boolean; deleteById(collectionId: string, recordId: string): boolean; getById(collectionId: string, recordId: string): RecordModel | undefined; list(collectionId: string): RecordModel[]; count(collectionId: string): number; /** Drops every row of a collection (what deleting the collection does) */ dropCollection(collectionId: string): void; /** Every collection id that currently holds at least one record */ collectionIds(): string[]; /** * Deep copy. Records are rebound to the collections in `collections` so a * cloned record's `collection()` points at the cloned schema, not the one * the transaction started from. */ clone(resolveCollection: (id: string) => Collection | undefined): RecordStore; /** Commit: adopt another store's rows */ replaceWith(other: RecordStore): void; private rowsFor; } /** * CollectionStore — the in-memory database state migrations execute against * * Keyed by collection id (migrations reference collections by id, name, or * the `_pb_users_auth_` alias; renames must not orphan entries). Cloning * serializes to plain objects and rebuilds, giving transaction semantics: * the runner clones, applies a migration to the clone, and commits with * replaceWith() only on success. */ declare class CollectionStore { private byId; /** * Rows, when record simulation is enabled. Always present so clone/commit * carry data through the same transaction as the schema; empty and * effectively free when only schema is being replayed. */ readonly records: RecordStore; list(): Collection[]; getById(id: string): Collection | undefined; getByNameOrId(nameOrId: string): Collection | undefined; upsert(collection: Collection): void; removeById(id: string): boolean; /** Deep, independent copy (rebuilds Collection/FieldsList instances) */ clone(): CollectionStore; /** Commit: adopt another store's state */ replaceWith(other: CollectionStore): void; serialize(): RawCollection[]; /** Convert to the internal schema model used by diff/compare */ toSnapshot(): SchemaSnapshot; } /** * Execution engine types * * The engine executes PocketBase JS migration files in a sandboxed context * that emulates the PocketBase JSVM (goja) API surface, instead of statically * parsing them. These types describe its configuration and results. */ /** * A PocketBase-shaped plain collection object, as found in migration files * and snapshot arrays (the JSON shape PocketBase itself serializes). */ type RawCollection = Record; /** * How the engine treats PocketBase APIs it does not simulate * ($os, $dbx, Record, data-layer app methods, ...): * - "lenient": record a warning and return an inert no-op value * - "strict": throw immediately */ type EngineStrictness = "strict" | "lenient"; /** * Whether record CRUD and `$dbx`/`app.db()` queries execute against an * in-memory row store: * - "stub" (default): `Record` and the data-layer `app.*` methods stay inert, * which is all schema reconstruction needs * - "simulate": rows are tracked per collection, so a hand-written data * migration can be executed and inspected before it runs for real */ type EngineRecordMode = "stub" | "simulate"; interface EngineWarning { /** Migration file the warning originated from, when known */ file?: string; kind: "unsupported-api" | "console" | "noop"; /** The API that was stubbed, e.g. "$os.getenv" or "app.findRecordById" */ api?: string; message: string; } interface EngineOptions { /** Defaults to "lenient" */ strictness?: EngineStrictness; /** Defaults to "stub" */ records?: EngineRecordMode; /** Invoked for every warning as it happens (warnings are also collected) */ onWarning?: (warning: EngineWarning) => void; /** Per-file evaluation/execution timeout in milliseconds. Defaults to 5000. */ timeoutMs?: number; } /** Which of a migration's two closures was executed */ type MigrationDirection = "up" | "down"; interface MigrationExecutionResult { file?: string; /** Which closure ran. Defaults to "up" for state reconstruction. */ direction: MigrationDirection; /** * False when the file registered no closure for this direction — no * migrate() call at all, or a migrate(up) with no down. */ applied: boolean; warnings: EngineWarning[]; } interface ReplayResult { /** Final state converted to the internal schema model */ snapshot: SchemaSnapshot; /** The raw store, for further execution or inspection */ store: CollectionStore; warnings: EngineWarning[]; filesExecuted: string[]; /** * How the file list was chosen — including anything on disk that is not * applied, and anything applied that is no longer on disk. Null when the * caller supplied the file list directly. */ plan: MigrationPlan | null; } /** * Snapshot loading * * The "current database state" is reconstructed by executing the generated * migration files in a simulated PocketBase JSVM (see migration/engine/). * There is no snapshot JSON file. */ /** * Configuration for snapshot operations */ interface SnapshotConfig { /** * Path to the migrations directory to replay */ migrationsPath?: string; /** * Options forwarded to the execution engine, which reconstructs state by * executing migration files in a simulated PocketBase JSVM. */ engineOptions?: EngineOptions; /** * Migrations PocketBase has actually applied, read from its `_migrations` * table (see `readAppliedMigrations`). When supplied, replay starts from * the newest applied snapshot and stops at the applied set, so a migration * written but not yet run does not leak into the reconstructed state. */ appliedMigrations?: AppliedMigrationsSource | string[] | null; } /** * Finds the most recent snapshot file in the migrations directory * Identifies snapshot files by naming pattern (e.g., *_collections_snapshot.js) * * @param migrationsPath - Path to pb_migrations directory * @returns Path to most recent snapshot file or null if none exist */ declare function findLatestSnapshot(migrationsPath: string): string | null; /** * Reconstructs the current database state: executes the snapshot migration and * every migration after it in a simulated PocketBase JSVM. * * A migration that cannot be executed fails hard — continuing past it would * silently reconstruct the wrong state and cause the generator to emit * incorrect diffs. * * @param config - Snapshot configuration (must include migrationsPath) * @returns SchemaSnapshot representing the current state, or null when there is * nothing to replay (an empty database) */ declare function loadSnapshotWithMigrations(config?: SnapshotConfig): SchemaSnapshot | null; /** * Configuration options for the diff engine */ interface DiffEngineConfig { /** * Custom system collections to exclude from diff * These collections will not be created or deleted */ systemCollections?: string[]; /** * Custom system fields to exclude from user collection diffs * These fields will not be included in fieldsToAdd for the users collection */ usersSystemFields?: string[]; } interface FilterOptions { patterns?: string[]; skipDestructive?: boolean; } declare function filterDiff(diff: SchemaDiff, options: FilterOptions): SchemaDiff; /** * Categorizes changes by severity * Returns object with destructive and non-destructive changes * * @param diff - Schema diff to categorize * @param config - Optional configuration * @returns Object with categorized changes */ declare function categorizeChangesBySeverity(diff: SchemaDiff, _config?: DiffEngineConfig): { destructive: string[]; nonDestructive: string[]; }; /** * Diff Engine component * Compares current schema with previous snapshot and identifies changes * * This module provides a standalone, configurable diff engine that can be used * by consumer projects to compare schema definitions and detect changes. */ /** * Main comparison function * Compares current schema with previous snapshot and returns complete diff * * @param currentSchema - Current schema definition * @param previousSnapshot - Previous schema snapshot (null for first run) * @param config - Optional configuration * @returns Complete SchemaDiff with all detected changes */ declare function compare(currentSchema: SchemaDefinition, previousSnapshot: SchemaSnapshot | null, config?: DiffEngineConfig): SchemaDiff; /** * Validation and warning utilities for migration tool * Detects destructive changes and provides warnings */ /** * Types of destructive changes */ declare enum DestructiveChangeType { COLLECTION_DELETION = "collection_deletion", FIELD_DELETION = "field_deletion", FIELD_TYPE_CHANGE = "field_type_change", FIELD_REQUIRED_CHANGE = "field_required_change" } /** * Represents a destructive change with details */ interface DestructiveChange { type: DestructiveChangeType; description: string; collection: string; field?: string; details?: { oldValue?: any; newValue?: any; }; severity: "high" | "medium" | "low"; warning: string; } /** * Detects all destructive changes in a schema diff * * @param diff - Schema diff to analyze * @returns Array of all destructive changes */ declare function detectDestructiveChanges(diff: SchemaDiff): DestructiveChange[]; /** * Checks if a diff contains any destructive changes * * @param diff - Schema diff to check * @returns True if there are destructive changes */ declare function hasDestructiveChanges(diff: SchemaDiff): boolean; /** * Formats destructive changes for display * Groups changes by severity and provides clear warnings * * @param changes - Array of destructive changes * @returns Formatted string for display */ declare function formatDestructiveChanges(changes: DestructiveChange[]): string; /** * Generates a summary of destructive changes * * @param changes - Array of destructive changes * @returns Summary object with counts by severity */ declare function summarizeDestructiveChanges(changes: DestructiveChange[]): { total: number; high: number; medium: number; low: number; }; /** * Checks if force flag is required for the given changes * Force is required if there are any high or medium severity changes * * @param changes - Array of destructive changes * @returns True if force flag should be required */ declare function requiresForceFlag(changes: DestructiveChange[]): boolean; /** * Configuration options for the migration generator */ interface MigrationGeneratorConfig { /** * Directory to write migration files */ migrationDir: string; /** * Workspace root for resolving relative paths * Defaults to process.cwd() */ workspaceRoot?: string; /** * Custom timestamp generator function * Defaults to Unix timestamp in seconds */ timestampGenerator?: () => string; /** * Custom migration file template * Use {{UP_CODE}} and {{DOWN_CODE}} placeholders */ template?: string; /** * Whether to include type reference comment * Defaults to true */ includeTypeReference?: boolean; /** * Path to types.d.ts file for reference comment * Defaults to '../pb_data/types.d.ts' */ typesPath?: string; /** * Whether to force generation even if duplicate migration exists * Defaults to false */ force?: boolean; } /** * Migration Generator component * Creates PocketBase migration files based on detected differences * * This module provides a standalone, configurable migration generator that can be used * by consumer projects to generate PocketBase-compatible migration files. */ /** * A migration file that has been generated but not yet written to disk */ interface PlannedMigration { /** Filename the migration will be written as */ filename: string; /** Complete migration file content */ content: string; /** The collection operation this file was generated from */ operation: CollectionOperation; } /** * Generates migration file contents from a schema diff without writing them * * Splitting planning from writing lets callers inspect or verify a migration * before it lands in the migrations directory — see the engine's * `verifyMigrationSources`, which the CLI's `generate --verify` runs over the * plan and aborts on. * * @param diff - Schema diff containing all changes * @param config - Migration generator configuration * @returns One planned migration per collection operation, in dependency order */ declare function planMigrations(diff: SchemaDiff, config: MigrationGeneratorConfig | string): PlannedMigration[]; /** * Main generation function * Generates migration files from schema diff (one file per collection operation) * * @param diff - Schema diff containing all changes * @param config - Migration generator configuration * @returns Array of paths to the generated migration files */ declare function generate(diff: SchemaDiff, config: MigrationGeneratorConfig | string): string[]; /** * Writes planned migrations to disk, in order * * @param planned - Migrations produced by planMigrations() * @param migrationDir - Absolute path to the migrations directory * @returns Array of paths to the written files */ declare function writePlannedMigrations(planned: PlannedMigration[], migrationDir: string): string[]; /** * goja-compatibility lint * * The engine executes migrations with Node's JavaScript, a superset of the * goja dialect PocketBase actually runs. That gap is silent: a migration can * replay perfectly here, pass verification, and still fail the moment * `pocketbase migrate up` reaches it — because it referenced a Node global, * used class fields, or awaited a promise in a runtime with no event loop. * * This pass closes the gap statically. It parses the file with acorn and * reports: * * - `unknown-global` — a free identifier that is neither declared in the file * nor part of the PocketBase JSVM surface (the sandbox globals) or the * ECMAScript library goja implements. `require`, `process`, `Buffer`, * `fetch`, `setTimeout` all land here. * - `unsupported-syntax` — constructs goja's parser rejects: class fields, * private members, static blocks, BigInt literals, `import.meta`. * - `module-syntax` — `import`/`export`; migrations are scripts. * - `async` — `async`/`await`/`Promise`. goja runs migrations synchronously, * so an awaited promise never settles. * - `unsupported-api` — folded in from execution warnings: a call that * resolved to an inert stub did nothing here and will do something (or * throw) in production. */ type GojaLintRule = "syntax" | "unsupported-syntax" | "module-syntax" | "async" | "unknown-global" | "unsupported-api"; type GojaLintSeverity = "error" | "warning"; interface GojaLintFinding { rule: GojaLintRule; severity: GojaLintSeverity; message: string; file?: string; line?: number; column?: number; } interface GojaLintResult { file: string; findings: GojaLintFinding[]; /** No error-severity findings */ ok: boolean; } interface GojaLintOptions { /** Path or name used in messages */ file?: string; /** * Extra globals to treat as available — for a PocketBase build that * registers its own JSVM bindings. */ allowedGlobals?: string[]; /** * Engine warnings from executing the same file. `unsupported-api` entries * become findings, which is how a stubbed call gets surfaced prominently * instead of being buried in replay output. */ warnings?: EngineWarning[]; } declare function lintMigrationSource(source: string, options?: GojaLintOptions): GojaLintResult; declare function lintMigrationFile(filePath: string, options?: GojaLintOptions): GojaLintResult; declare function formatGojaLintFinding(finding: GojaLintFinding): string; /** * Replayer — folds an ordered list of migration files into a final state * * Mirrors what PocketBase does on `migrate up`: execute each unapplied file * in timestamp order. State reconstruction starts from an empty store; a * native snapshot migration (app.importCollections) is just the first file. * * Which files that is comes from `planMigrationReplay`: by default the newest * snapshot plus everything after it, or — when an applied-migrations list is * supplied — only the files PocketBase has actually run, starting from the * newest applied snapshot. */ declare function replayMigrations(files: string[], options?: EngineOptions & { initialStore?: CollectionStore; plan?: MigrationPlan; }): ReplayResult; /** * Replays a pb_migrations directory: the snapshot the state starts from, * then every migration after it, in timestamp order. * * Pass `applied` (from `readAppliedMigrations`) to replay only what PocketBase * has actually run — otherwise every file on disk is assumed applied. * * Returns null when there is nothing to replay (an empty database). */ declare function replayMigrationsDirectory(migrationsPath: string, options?: EngineOptions & PlanOptions): ReplayResult | null; /** * Runner — executes one migration file against a CollectionStore * * Evaluation happens in a vm context built from the sandbox globals; the * file's migrate(up, down) call registers its closures. The requested * direction then runs transactionally: the store is cloned, the closure is * applied to the clone, and the clone replaces the store only on success. * The closure is invoked from inside the context (not host code) so the vm * timeout also bounds infinite loops within migration bodies. * * State reconstruction only ever runs `up`, which structurally rules out * replaying rollback statements as forward operations. `down` is executed on request (`executeMigrationDownSource`), * for rollback verification — see `verify.ts`. Downs run in reverse * registration order, mirroring how PocketBase rolls back. */ declare function executeMigrationFile(filePath: string, store: CollectionStore, options?: EngineOptions): MigrationExecutionResult; /** * Structural comparison of two engine states * * Down-migration verification asks one question — "is the state after * up() + down() the state we started from?" — and needs an answer precise * enough to name what drifted. This compares the raw PocketBase-shaped * collections of two stores directly, rather than routing through the diff * engine, because a rollback that leaves the schema semantically equal but * structurally different (a restored field carrying a different id, an index * re-added in a different form) is exactly the kind of drift verification * exists to catch. * * Normalization is limited to differences PocketBase itself does not * distinguish: * * - An option a migration never declared and one set to its Go zero value * (`""`, `0`, `false`, `[]`, `null`) express the same constraint, so an * absent key equals a zero-valued one. Two *declared* values are always * compared (`min: 0` vs `min: 5` is a real difference). * - API rules are exempt from that rule: `null` (superuser only) and `""` * (public) are different permissions, so only absent ≡ `null` holds. * - Index order is not meaningful; index lists are compared as sets. * - Field order is not compared unless `strictFieldOrder` is set. */ interface StateDifference { kind: "collection-added" | "collection-removed" | "collection-property" | "field-added" | "field-removed" | "field-property" | "field-order" | "indexes"; /** Collection name (baseline name when the collection exists on both sides) */ collection: string; field?: string; property?: string; expected?: unknown; actual?: unknown; /** Human-readable one-liner, suitable for CLI output */ message: string; } interface StateCompareOptions { /** * Collection and field properties to skip. * Defaults to PocketBase's own bookkeeping timestamps. */ ignoreKeys?: string[]; /** Also compare the position of each field within its collection */ strictFieldOrder?: boolean; /** Labels used in messages; defaults to "baseline" / "actual" */ labels?: { expected: string; actual: string; }; } /** * Down-migration verification * * A generated `down()` is never exercised by state reconstruction — the * replayer only runs `up()` — so a rollback that does not actually roll back * stays invisible until someone runs `pocketbase migrate down` in anger. * This module closes that gap: for each migration it executes `up()` against * a baseline, then `down()` against the result, and asserts the state came * back to the baseline. * * `down()` runs from a fresh evaluation of the file, the way PocketBase runs * it — a separate invocation that cannot observe anything `up()` left in the * file's module scope. * * Failures are returned, not thrown: a verification pass reports on every * migration it was given, and the caller decides whether an unreversible * migration is fatal. */ interface MigrationSourceRef { source: string; /** Path or name used in messages */ file?: string; } interface MigrationRoundTripResult { file: string; /** The file registered an up() closure and it committed */ upApplied: boolean; /** The file registered a down() closure and it committed */ downApplied: boolean; /** up() changed the state but the file has no down() to undo it */ missingDown: boolean; /** up() followed by down() returned to the baseline */ reversible: boolean; /** What the rollback failed to restore (empty when reversible) */ differences: StateDifference[]; warnings: EngineWarning[]; /** Set when a phase failed to execute at all */ error?: { phase: MigrationDirection | "evaluate"; message: string; }; /** State after up() — what the next migration in a sequence starts from */ storeAfterUp: CollectionStore; } interface MigrationVerificationReport { results: MigrationRoundTripResult[]; /** Every migration executed and reversed cleanly */ ok: boolean; /** State after every up() — the state the migrations leave behind */ store: CollectionStore; /** Only the results that failed, in input order */ failures: MigrationRoundTripResult[]; } interface VerifyOptions extends EngineOptions { /** State the first migration is applied to. Defaults to an empty store. */ initialStore?: CollectionStore; /** Forwarded to the state comparison */ compare?: StateCompareOptions; } /** * Verifies a sequence of migrations the way they will be applied: each one * is round-tripped against the state its predecessors leave behind, and its * up() is then committed before moving on. */ declare function verifyMigrationSources(migrations: MigrationSourceRef[], options?: VerifyOptions): MigrationVerificationReport; /** * Custom error classes for migration tool * Provides specific error types for better error handling and user feedback */ /** * Base error class for all migration-related errors */ declare class MigrationError extends Error { constructor(message: string); } /** * Error thrown when schema parsing fails * Used when Zod schemas cannot be parsed or are invalid */ declare class SchemaParsingError extends MigrationError { readonly filePath?: string; readonly originalError?: Error; constructor(message: string, filePath?: string, originalError?: Error); /** * Creates a formatted error message with file path and original error details */ getDetailedMessage(): string; } /** * Error thrown when snapshot operations fail * Used for snapshot file read/write/parse errors */ declare class SnapshotError extends MigrationError { readonly snapshotPath?: string; readonly operation?: "read" | "write" | "parse" | "validate"; readonly originalError?: Error; constructor(message: string, snapshotPath?: string, operation?: "read" | "write" | "parse" | "validate", originalError?: Error); /** * Creates a formatted error message with snapshot path and operation details */ getDetailedMessage(): string; } /** * Error thrown when the execution engine fails to run a migration file * Carries the file path and the phase that failed so the user can pinpoint * (and fix or exclude) the offending migration */ declare class MigrationExecutionError extends MigrationError { readonly filePath?: string; readonly phase?: "evaluate" | "up" | "down"; readonly originalError?: Error; constructor(message: string, filePath?: string, phase?: "evaluate" | "up" | "down", originalError?: Error); /** * Creates a formatted error message with file path and phase details */ getDetailedMessage(): string; } /** * Error thrown when migration file generation fails * Used when migration files cannot be created or written */ declare class MigrationGenerationError extends MigrationError { readonly migrationPath?: string; readonly originalError?: Error; constructor(message: string, migrationPath?: string, originalError?: Error); /** * Creates a formatted error message with migration path and original error details */ getDetailedMessage(): string; } /** * Error thrown when file system operations fail * Used for directory creation, file permissions, disk space issues */ declare class FileSystemError extends MigrationError { readonly path?: string; readonly operation?: "read" | "write" | "create" | "delete" | "access"; readonly code?: string; readonly originalError?: Error; constructor(message: string, path?: string, operation?: "read" | "write" | "create" | "delete" | "access", code?: string, originalError?: Error); /** * Creates a formatted error message with path, operation, and error code details */ getDetailedMessage(): string; } /** * Error thrown when configuration is invalid * Used for configuration file parsing, validation, and path resolution errors */ declare class ConfigurationError extends MigrationError { readonly configPath?: string; readonly invalidFields?: string[]; readonly originalError?: Error; constructor(message: string, configPath?: string, invalidFields?: string[], originalError?: Error); /** * Creates a formatted error message with configuration details */ getDetailedMessage(): string; } /** * Error thrown when CLI command usage is incorrect * Used for invalid arguments, missing required options, etc. */ declare class CLIUsageError extends MigrationError { readonly command?: string; readonly suggestion?: string; constructor(message: string, command?: string, suggestion?: string); /** * Creates a formatted error message with usage suggestions */ getDetailedMessage(): string; } /** * Generate command implementation * Generates migrations from schema changes */ /** * Executes the generate command * * @param filters - Optional filters for collection/field names * @param options - Command options */ declare function executeGenerate(filters: string[], options: any): Promise; /** * Status command implementation * Shows current migration status without generating files */ /** * Executes the status command * * @param options - Command options */ declare function executeStatus(options: any): Promise; /** * Configuration loader for migration tool * Handles loading and merging configuration from various sources */ /** * Migration tool configuration */ interface MigrationConfig { schema: { directory: string; exclude: string[]; }; migrations: { directory: string; format: string; /** * Whether `generate` executes each new migration's up() and down() in the * simulation before writing it, and refuses to write one that does not * roll back cleanly. Off by default. */ verify: boolean; /** * PocketBase's data directory (or a data.db file), used to read the * `_migrations` table so replay can stop at what has actually been * applied. Empty means "the pb_data directory next to the migrations * directory". */ dataDirectory: string; }; diff: { warnOnDelete: boolean; requireForceForDestructive: boolean; }; typeGen: { outPath: string; }; } /** * Loads and merges configuration from all sources * Priority: CLI args > Environment variables > Config file > Defaults */ declare function loadConfig(options?: any): Promise; export { APIRuleType, type AppliedMigration, AppliedMigrationsError, type AppliedMigrationsSource, CLIUsageError, type CollectionModification, type CollectionOperation, type CollectionSchema, CollectionStore, ConfigurationError, type DestructiveChange, DestructiveChangeType, type DiffEngineConfig, type DiscoveredMigration, type EngineOptions, type EngineRecordMode, type EngineStrictness, type EngineWarning, type FieldChange, type FieldDefinition, type FieldModification, FileSystemError, type FilterOptions, type GojaLintFinding, type GojaLintOptions, type GojaLintResult, type GojaLintRule, type GojaLintSeverity, type MigrationConfig, type MigrationDirection, MigrationError, MigrationExecutionError, type MigrationExecutionResult, MigrationGenerationError, type MigrationGeneratorConfig, type MigrationPlan, type MigrationRoundTripResult, type MigrationSourceRef, type MigrationVerificationReport, type PermissionChange, type PlanOptions, type PlannedMigration, PocketBaseFieldType, RecordModel, type ReplayResult, type RuleUpdate, type SchemaAnalyzerConfig, type SchemaDefinition, type SchemaDiff, SchemaParsingError, type SchemaSnapshot, type SnapshotConfig, SnapshotError, type ViewQueryUpdate, appliedMigrationsFromList, categorizeChangesBySeverity, compare, convertZodSchemaToCollectionSchema, defaultDataDirectory, detectDestructiveChanges, discoverMigrations, discoverSchemaFiles, executeMigrationFile, filterDiff, findLatestSnapshot, formatDestructiveChanges, formatGojaLintFinding, generate, executeGenerate as generateMigration, executeStatus as getMigrationStatus, hasDestructiveChanges, lintMigrationFile, lintMigrationSource, loadConfig, loadSnapshotWithMigrations, parseSchemaFiles, planMigrationReplay, planMigrations, readAppliedMigrations, readAppliedMigrationsIfPresent, replayMigrations, replayMigrationsDirectory, requiresForceFlag, summarizeDestructiveChanges, verifyMigrationSources, writePlannedMigrations };