Skip to content

Examples & Use Cases

This page showcases live examples of the Payload Collection CLI in action, automatically generated from our E2E test suite. Each section below represents a verified scenario, including the sample data and any custom mapping configuration used.

Success Create

Simple record creation without any custom configuration.

Data (data.jsonl)

jsonline
{"email":"charlie@example.com","password":"password123"}

Test Scenario (scenario.test.ts)

typescript
import { describe, it, expect, beforeEach } from "vitest";
import { runCLI, resetDatabase, getCollectionData } from "../../utils";
import path from "path";
import { fileURLToPath } from "url";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

describe("Success Create", () => {
	beforeEach(() => {
		resetDatabase();
	});

	it("creates a new user successfully from a JSONL file", () => {
		const dataFile = path.resolve(__dirname, "data.jsonl");

		// runCLI returns the string output of the command
		const output = runCLI(`users create ${dataFile}`);

		expect(output).toContain("Operation successful");

		const users = getCollectionData("users");
		expect(users).toHaveLength(1);
		expect(users[0].email).toBe("charlie@example.com");
	}, 60000);
});

Success Update

Data (data.jsonl)

jsonline
{"email":"update-me@example.com","name":"Updated Name"}

Configuration (config.ts)

typescript
export const cliConfig = {
	mappings: {
		users: {
			lookupField: "email",
		},
	},
};

Test Scenario (scenario.test.ts)

typescript
import { describe, it, expect, beforeEach } from "vitest";
import { runCLI, resetDatabase, getCollectionData } from "../../utils";
import path from "path";
import { fileURLToPath } from "url";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

describe("Update Record", () => {
	beforeEach(() => {
		resetDatabase();
	});

	it("updates a specific field of a user successfully using a config file", () => {
		// 1. Create a user first
		const userData = JSON.stringify({
			email: "update-me@example.com",
			name: "Original Name",
			password: "password123",
		});
		runCLI(`users create '${userData}'`);

		// 2. Update the user's name using a config file and data file
		const dataFile = path.resolve(__dirname, "data.jsonl");
		const configFile = path.resolve(__dirname, "config.ts");
		const output = runCLI(`-c ${configFile} users update ${dataFile}`);

		expect(output).toContain("Operation successful");

		const users = getCollectionData("users");
		expect(users).toHaveLength(1);
		expect(users[0].name).toBe("Updated Name");
		expect(users[0].email).toBe("update-me@example.com");
	}, 60000);
});

Success Mapping Upsert

Data (data.jsonl)

jsonline
{"email":"alice@example.com","password":"password123"}
{"email":"bob@example.com","password":"password123"}

Configuration (config.ts)

typescript
export const cliConfig = {
	mappings: {
		users: {
			lookupField: "email",
		},
	},
};

Test Scenario (scenario.test.ts)

typescript
import { describe, it, expect, beforeEach } from "vitest";
import { runCLI, resetDatabase, getCollectionData } from "../../utils";
import path from "path";

describe("Success mapping upsert", () => {
	beforeEach(() => {
		resetDatabase();
	});

	it("upserts users using email as lookup variable provided by strict configuration", () => {
		// 💡 In an `upsert` operation, we must provide a configuration to use `email`
		// for existence checks instead of the default `id`.
		// We achieve this by explicitly passing a file-based configuration to the CLI.
		const dataPath = path.resolve(__dirname, "data.jsonl");
		const configPath = path.resolve(__dirname, "config.ts");

		const output = runCLI(`-c ${configPath} users upsert ${dataPath}`);

		expect(output).toContain("Operation successful");

		const users = getCollectionData("users");
		expect(users).toHaveLength(2);
		const emails = users.map((u: any) => u.email);
		expect(emails).toContain("alice@example.com");
	}, 30000);
});

Success Delete

Test Scenario (scenario.test.ts)

typescript
import { describe, it, expect, beforeEach } from "vitest";
import { runCLI, resetDatabase, getCollectionData } from "../../utils";
import path from "path";

describe("Success delete", () => {
	beforeEach(() => {
		resetDatabase();
	});

	it("deletes a user successfully", () => {
		// 1. Create a user first
		const userData = JSON.stringify({
			email: "delete-me@example.com",
			name: "Delete Me",
			password: "password123",
		});
		runCLI(`users create '${userData}'`);

		expect(getCollectionData("users")).toHaveLength(1);

		// 2. Delete the user
		const deleteData = JSON.stringify({ email: "delete-me@example.com" });
		const output = runCLI(
			`-j '{"mappings":{"users":{"lookupField":"email"}}}' users delete '${deleteData}'`,
		);

		expect(output).toContain("Operation successful");
		expect(getCollectionData("users")).toHaveLength(0);
	}, 60000);
});

Success Patch

This scenario verifies that a JSON Patch file can be used to add, replace, and remove documents.

Test Scenario (scenario.test.ts)

typescript
import { describe, it, expect, beforeEach } from "vitest";
import { runCLI, resetDatabase, getCollectionData } from "../../utils";
import path from "path";
import { fileURLToPath } from "url";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

describe("Success Patch", () => {
	beforeEach(() => {
		resetDatabase();
	});

	it("applies a JSON Patch successfully to a collection", () => {
		// First, create some initial data
		runCLI(`users create '{"email": "patch-old@example.com", "password": "password123"}'`);
		runCLI(`users create '{"email": "patch-delete@example.com", "password": "password123"}'`);

		const dataFile = path.resolve(__dirname, "data.json");
		const output = runCLI(`users patch ${dataFile}`);

		expect(output).toContain("Operation successful");

		const users = getCollectionData("users");
		// Initial 2, added 1, removed 1 = 2 total
		expect(users).toHaveLength(2);

		const emails = users.map((u: any) => u.email);
		expect(emails).toContain("patch-new@example.com");
		expect(emails).toContain("patch-updated@example.com");
		expect(emails).not.toContain("patch-old@example.com");
		expect(emails).not.toContain("patch-delete@example.com");
	}, 60000);
});

Success Sync

This scenario verifies that a collection can be synchronized with a JSONL file.

  • New users are created.
  • Existing users (by email) are updated.
  • Users not in the file are deleted.

Data (data.jsonl)

jsonline
{"email": "sync-keep-updated@example.com", "name": "Updated Name", "password": "password123"}
{"email": "sync-new@example.com", "name": "New User", "password": "password123"}

Configuration (config.ts)

typescript
export const cliConfig = {
	mappings: {
		users: {
			lookupField: "email",
		},
	},
};

Test Scenario (scenario.test.ts)

typescript
import { describe, it, expect, beforeEach } from "vitest";
import { runCLI, resetDatabase, getCollectionData } from "../../utils";
import path from "path";
import { fileURLToPath } from "url";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

describe("Success Sync", () => {
	beforeEach(() => {
		resetDatabase();
	});

	it("synchronizes a collection with a JSONL file successfully", () => {
		// First, create some order data
		runCLI(`users create '{"email": "sync-keep-updated@example.com", "name": "Old Name", "password": "password123"}'`);
		runCLI(`users create '{"email": "sync-delete@example.com", "name": "Delete Me", "password": "password123"}'`);

		const dataFile = path.resolve(__dirname, "data.jsonl");
		const configFile = path.resolve(__dirname, "config.ts");
		
		// Run sync with config to use email as lookup
		const output = runCLI(`-c ${configFile} users sync ${dataFile}`);

		expect(output).toContain("Operation successful");

		const users = getCollectionData("users");
		// Initial 2, 1 updated, 1 added, 1 deleted = 2 total
		expect(users).toHaveLength(2);

		const emails = users.map((u: any) => u.email);
		expect(emails).toContain("sync-keep-updated@example.com");
		expect(emails).toContain("sync-new@example.com");
		expect(emails).not.toContain("sync-delete@example.com");

		const updatedUser = users.find((u: any) => u.email === "sync-keep-updated@example.com");
		expect(updatedUser.name).toBe("Updated Name");
	}, 60000);
});

Success Package Json Config

Data (data.jsonl)

jsonline
{"email":"pkg-json@example.com","password":"password123"}

Test Scenario (scenario.test.ts)

typescript
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import {
	runCLI,
	resetDatabase,
	getCollectionData,
	payloadAppDir,
} from "../../utils";
import path from "path";
import fs from "fs";

describe("Success package.json config", () => {
	const pkgPath = path.join(payloadAppDir, "package.json");
	let originalPkg: string;

	beforeEach(() => {
		resetDatabase();
		if (!originalPkg && fs.existsSync(pkgPath)) {
			originalPkg = fs.readFileSync(pkgPath, "utf-8");
		}
	});

	afterEach(() => {
		if (originalPkg) {
			fs.writeFileSync(pkgPath, originalPkg);
		}
	});

	it("reads config from package.json defaults", () => {
		// 1. Prepare data
		const dataPath = path.resolve(__dirname, "data.jsonl");

		// 2. Inject payload-collection-cli config into package.json
		const pkg = JSON.parse(originalPkg);
		pkg["payload-collection-cli"] = {
			configJson: JSON.stringify({
				mappings: {
					users: { lookupField: "email" },
				},
			}),
		};
		fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2));

		// 3. Run CLI WITHOUT -c or -j flags
		const output = runCLI(`users upsert ${dataPath}`);

		// Cleanup package.json immediately
		fs.writeFileSync(pkgPath, originalPkg);

		expect(output).toContain("Operation successful");

		const users = getCollectionData("users");
		expect(users).toHaveLength(1);
		expect(users[0].email).toBe("pkg-json@example.com");
	}, 60000);
});

Success Inline Config String

Data (data.jsonl)

jsonline
{"email":"inline1@example.com","password":"pwd"}
{"email":"inline2@example.com","password":"pwd"}

Test Scenario (scenario.test.ts)

typescript
import { describe, it, expect, beforeEach } from "vitest";
import { runCLI, resetDatabase, getCollectionData } from "../../utils";
import path from "path";

describe("Success mapping upsert via inline string config", () => {
	beforeEach(() => {
		resetDatabase();
	});

	it("upserts users using an inline JSON string for lookupField", () => {
		// 💡 In an `upsert` operation, we must provide a configuration to use `email`
		// for existence checks instead of the default `id`.
		// We achieve this by directly supplying an inline JSON string to the CLI.
		const dataPath = path.resolve(__dirname, "data.jsonl");

		// Pass config directly as an inline JSON string
		const output = runCLI(
			`-j '{"mappings":{"users":{"lookupField":"email"}}}' users upsert ${dataPath}`,
		);

		expect(output).toContain("Operation successful");

		const users = getCollectionData("users");
		expect(users).toHaveLength(2);
		const emails = users.map((u: any) => u.email);
		expect(emails).toContain("inline1@example.com");
	}, 30000);
});

Success Config With Import

Data (data.jsonl)

jsonline
{"title":"Imported Default Post","author":"alice@example.com"}

Configuration (config.ts)

typescript
// 💡 This config imports DEFAULT_CATEGORY_NAME from a separate constants file
// in the Payload app. This proves that jiti correctly resolves imports
// relative to the config file's own location—NOT the CLI's location.
import { DEFAULT_CATEGORY_NAME } from "../../_payload/src/constants";

export const cliConfig = {
	mappings: {
		users: { lookupField: "email" },
		categories: {
			lookupField: "name",
			onNotFound: "create", // Auto-create category if it doesn't exist
		},
		posts: {
			defaults: {
				category: DEFAULT_CATEGORY_NAME, // Injected from shared constants!
			},
		},
	},
};

Test Scenario (scenario.test.ts)

typescript
import { describe, it, expect, beforeEach } from "vitest";
import { runCLI, resetDatabase, getCollectionData } from "../../utils";
import path from "path";

describe("Config with cross-file import", () => {
	beforeEach(() => {
		resetDatabase();
	});

	it("resolves defaults from an imported constant in a separate project file", () => {
		// 💡 This test verifies that the CLI config file can use standard TypeScript imports
		// to reference constants defined elsewhere in the Payload app.
		// The config imports DEFAULT_CATEGORY_NAME ('uncategorized') from src/constants.ts,
		// uses it as the default value for the post's category field.
		// Combined with onNotFound:'create', the category is auto-created from that imported value.

		// 1. Preparation: Upsert the author
		// 1. Preparation: Upsert the author using local fixture
		const userDataPath = path.resolve(__dirname, "users.jsonl");
		const userConfig = JSON.stringify({
			mappings: { users: { lookupField: "email" } },
		});
		runCLI(`-j '${userConfig}' users upsert ${userDataPath}`);

		// 2. Main Execution: Upsert a Post without specifying category
		//    The config's defaults.category is imported from src/constants.ts => 'uncategorized'
		const dataPath = path.resolve(__dirname, "data.jsonl");
		const configPath = path.resolve(__dirname, "config.ts");

		const output = runCLI(`-c ${configPath} posts create ${dataPath}`);
		expect(output).toContain("Operation successful");

		// 3. Verify the auto-created category name matches the imported constant
		const categories = getCollectionData("categories");
		expect(categories).toHaveLength(1);
		expect(categories[0].name).toBe("uncategorized"); // From imported DEFAULT_CATEGORY_NAME

		const posts = getCollectionData("posts");
		expect(posts).toHaveLength(1);
		expect(posts[0].category.id).toBe(categories[0].id);
	}, 60000);
});

Relation Posts Author

Data (data.jsonl)

jsonline
{"title":"My first E2E Post","author":"alice@example.com","category":"general"}

Configuration (config.ts)

typescript
export const cliConfig = {
	mappings: {
		users: { lookupField: "email" },
		categories: { lookupField: "name", onNotFound: "create" },
	},
};

Test Scenario (scenario.test.ts)

typescript
import { describe, it, expect, beforeEach } from "vitest";
import { runCLI, resetDatabase, getCollectionData } from "../../utils";
import path from "path";

describe("Relation auto-resolution", () => {
	beforeEach(() => {
		resetDatabase();
	});

	it("safely binds relational fields through pure email string specifications", () => {
		// 💡 Since resolving relationship fields inherently requires the target document's `id`,
		// the CLI config auto-resolves explicit relationships (e.g. mapping `author` email directly to `Users.id`).

		// 1. Preparation: Upsert the author
		// 1. Preparation: Upsert the author using local fixture
		const userDataPath = path.resolve(__dirname, "users.jsonl");
		const userConfig = JSON.stringify({
			mappings: { users: { lookupField: "email" } },
		});
		runCLI(`-j '${userConfig}' users upsert ${userDataPath}`);

		// 2. Main Execution: Upsert the Post referencing the author by their email string
		const dataPath = path.resolve(__dirname, "data.jsonl");
		const configPath = path.resolve(__dirname, "config.ts");

		const output = runCLI(`-c ${configPath} posts create ${dataPath}`);
		expect(output).toContain("Operation successful");

		const users = getCollectionData("users");
		const posts = getCollectionData("posts");
		const alice = users.find((u: any) => u.email === "alice@example.com");

		expect(posts).toHaveLength(1);
		// Assure Payload successfully bridged the relationship automatically converting string -> ID!
		expect(posts[0].author.id).toBe(alice.id);
	}, 60000);
});

Relation Auto Create Default

Data (data.jsonl)

jsonline
{"title":"My E2E Post","author":"alice@example.com"}

Configuration (config.ts)

typescript
export const cliConfig = {
	mappings: {
		users: { lookupField: "email" },
		categories: {
			lookupField: "name",
			onNotFound: "create", // Fallback: if category doesn't exist, create it via CLI dynamically
		},
		posts: {
			defaults: {
				category: "default", // Fallback: if data lacks 'category', inject 'default'
			},
		},
	},
};

Test Scenario (scenario.test.ts)

typescript
import { describe, it, expect, beforeEach } from "vitest";
import { runCLI, resetDatabase, getCollectionData } from "../../utils";
import path from "path";

describe("Relation auto-create and defaults", () => {
	beforeEach(() => {
		resetDatabase();
	});

	it("injects default relationship values and auto-creates them if they do not exist", () => {
		// 💡 Tests two advanced config features:
		// 1. `defaults`: Post lacks a `category`, so CLI injects "default"
		// 2. `onNotFound: 'create'`: The "default" category doesn't exist in DB, so CLI creates it on-the-fly!

		// 1. Preparation: Upsert the author
		// 1. Preparation: Upsert the author using local fixture
		const userDataPath = path.resolve(__dirname, "users.jsonl");
		const userConfig = JSON.stringify({
			mappings: { users: { lookupField: "email" } },
		});
		runCLI(`-j '${userConfig}' users upsert ${userDataPath}`);

		// 2. Main Execution: Upsert the Post (lacking category!)
		const dataPath = path.resolve(__dirname, "data.jsonl");
		const configPath = path.resolve(__dirname, "config.ts");

		const output = runCLI(`-c ${configPath} posts create ${dataPath}`);
		expect(output).toContain("Operation successful");

		const categories = getCollectionData("categories");
		expect(categories).toHaveLength(1);
		expect(categories[0].name).toBe("default");

		const posts = getCollectionData("posts");
		expect(posts).toHaveLength(1);

		// Assure Payload successfully bridged the missing relationship by auto-creating it
		expect(posts[0].category.id).toBe(categories[0].id);
	}, 60000);
});

Patch Partial Defaults Fix

Configuration (config.ts)

typescript

			export const cliConfig = {
				mappings: {
					categories: {
						lookupField: 'name',
						defaults: {
							displayName: 'DEFAULT_DISPLAY_NAME'
						}
					}
				}
			}

Test Scenario (scenario.test.ts)

typescript
import { describe, it, expect, beforeEach } from "vitest";
import { runCLI, resetDatabase, getCollectionData } from "../../utils";
import path from "path";
import { fileURLToPath } from "url";
import fs from "fs";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

describe("Patch Partial Defaults Fix", () => {
	beforeEach(() => {
		resetDatabase();
	});

	it("does not apply defaults to partial field updates in patch", () => {
		// 1. Create initial data
		runCLI(`categories create '{"name": "Original Name", "displayName": "Original Display"}'`);
		
		// 2. Define a config with a default for displayName
		const configPath = path.resolve(__dirname, "config.ts");
		const configContent = `
			export const cliConfig = {
				mappings: {
					categories: {
						lookupField: 'name',
						defaults: {
							displayName: 'DEFAULT_DISPLAY_NAME'
						}
					}
				}
			}
		`;
		fs.writeFileSync(configPath, configContent);

		// 3. Patch only the 'name' field
		const patchPath = path.resolve(__dirname, "patch.json");
		const patchContent = [
			{ op: "replace", path: "/[name=Original Name]/name", value: "Updated Name" }
		];
		fs.writeFileSync(patchPath, JSON.stringify(patchContent));

		const output = runCLI(`-c ${configPath} categories patch ${patchPath}`);
		expect(output).toContain("Operation successful");

		// 4. Verify that displayName was NOT changed to the default
		const categories = getCollectionData("categories");
		const category = categories.find((c: any) => c.name === "Updated Name");
		
		expect(category).toBeDefined();
		expect(category.name).toBe("Updated Name");
		// This is the CRITICAL check: it should remain "Original Display"
		expect(category.displayName).toBe("Original Display");
		expect(category.displayName).not.toBe("DEFAULT_DISPLAY_NAME");
	}, 60000);
});

Patch Test Relation Resolution

Configuration (config.ts)

typescript

			export const cliConfig = {
				mappings: {
					users: { lookupField: 'email' },
					categories: { lookupField: 'name' }
				}
			}

Test Scenario (scenario.test.ts)

typescript
import { describe, it, expect, beforeEach } from "vitest";
import { runCLI, resetDatabase, getCollectionData } from "../../utils";
import path from "path";
import { fileURLToPath } from "url";
import fs from "fs";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

describe("Patch Test Relation Resolution", () => {
	beforeEach(() => {
		resetDatabase();
	});

	it("resolves relations in the 'test' operation of a patch for consistency", () => {
		// 1. Setup collections
		// Create a category
		runCLI(`categories create '{"name": "News", "displayName": "News Category"}'`);
		// Create a user
		runCLI(`users create '{"email": "author@example.com", "name": "Author", "password": "password123"}'`);
		
		// 2. Create a post referencing them
		// We'll use sync or create with slugs if relations are resolved
		// Let's use simple create and then patch to set relations if needed,
		// but we want to test the 'test' op itself.
		
		// Map slugs to IDs is what resolveRelations does.
		// Let's create the post directly with slugs (which should work due to my fixes or existing logic)
		const configPath = path.resolve(__dirname, "config.ts");
		const configContent = `
			export const cliConfig = {
				mappings: {
					users: { lookupField: 'email' },
					categories: { lookupField: 'name' }
				}
			}
		`;
		fs.writeFileSync(configPath, configContent);

		runCLI(`-c ${configPath} posts create '{"title": "Post 1", "author": "author@example.com", "category": "News"}'`);

		// 3. Perform a patch with a 'test' operation using slugs
		const patchPath = path.resolve(__dirname, "patch.json");
		const patchContent = [
			{ op: "test", path: "/[title=Post 1]/author", value: "author@example.com" },
			{ op: "test", path: "/[title=Post 1]/category", value: "News" },
			{ op: "replace", path: "/[title=Post 1]/title", value: "Verified Post" }
		];
		fs.writeFileSync(patchPath, JSON.stringify(patchContent));

		const output = runCLI(`-c ${configPath} posts patch ${patchPath}`);
		expect(output).toContain("Operation successful");

		const posts = getCollectionData("posts");
		expect(posts[0].title).toBe("Verified Post");
	}, 60000);
});

Nested Relation Resolution

Configuration (config.ts)

typescript

			export const cliConfig = {
				mappings: {
					users: { lookupField: 'email' },
					categories: { lookupField: 'name' }
				}
			}

Test Scenario (scenario.test.ts)

typescript
import { describe, it, expect, beforeEach } from "vitest";
import { runCLI, resetDatabase, getCollectionData } from "../../utils";
import path from "path";
import { fileURLToPath } from "url";
import fs from "fs";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

describe("Nested Relation Resolution", () => {
	beforeEach(() => {
		resetDatabase();
	});

	it("resolves relations inside groups, arrays, and via flattened paths", () => {
		// 1. Setup collections
		runCLI(`users create '{"email": "reviewer@example.com", "name": "Reviewer", "password": "password123"}'`);
		runCLI(`users create '{"email": "author1@example.com", "name": "Author 1", "password": "password123"}'`);
		runCLI(`users create '{"email": "author2@example.com", "name": "Author 2", "password": "password123"}'`);
		
		const configPath = path.resolve(__dirname, "config.ts");
		const configContent = `
			export const cliConfig = {
				mappings: {
					users: { lookupField: 'email' },
					categories: { lookupField: 'name' }
				}
			}
		`;
		fs.writeFileSync(configPath, configContent);

		// 2. Test recursive resolution in 'create' (Group and Array)
		const createData = {
			name: "Tech",
			displayName: "Technology",
			metadata: {
				reviewer: "reviewer@example.com"
			},
			authors: [
				{ user: "author1@example.com" },
				{ user: "author2@example.com" }
			]
		};
		const createOutput = runCLI(`-c ${configPath} categories create '${JSON.stringify(createData)}'`);
		if (!createOutput.includes("Operation successful")) {
			throw new Error(`Create failed: ${createOutput}`);
		}

		const categories = getCollectionData("categories");
		const tech = categories.find((c: any) => c.name === "Tech");
		expect(tech.metadata.reviewer).toBeDefined();
		expect(typeof tech.metadata.reviewer).not.toBe("string"); // Should be populated object or ID
		const reviewerId = typeof tech.metadata.reviewer === 'object' ? tech.metadata.reviewer.id : tech.metadata.reviewer;

		expect(tech.authors).toHaveLength(2);
		const author1Id = typeof tech.authors[0].user === 'object' ? tech.authors[0].user.id : tech.authors[0].user;

		// 3. Test flattened path resolution in 'patch' using slashes
		// (Identifier-based paths support both slashes and dots for sub-paths)
		const patchPath = path.resolve(__dirname, "patch.json");
		const patchContent = [
			{ op: "replace", path: "/[name=Tech]/metadata/reviewer", value: "author1@example.com" }
		];
		fs.writeFileSync(patchPath, JSON.stringify(patchContent));

		runCLI(`-c ${configPath} categories patch ${patchPath}`);
		
		const updatedTech = getCollectionData("categories").find((c: any) => c.name === "Tech");
		const updatedReviewerId = typeof updatedTech.metadata.reviewer === 'object' ? updatedTech.metadata.reviewer.id : updatedTech.metadata.reviewer;
		expect(updatedReviewerId).toBe(author1Id);
	}, 60000);
});

Auto Create With Defaults

Configuration (config.ts)

typescript

			export const cliConfig = {
				mappings: {
					categories: {
						lookupField: 'name',
						onNotFound: 'create',
						defaults: {
							displayName: 'AUTO_CREATED_DISPLAY_NAME'
						}
					}
				}
			}

Test Scenario (scenario.test.ts)

typescript
import { describe, it, expect, beforeEach } from "vitest";
import { runCLI, resetDatabase, getCollectionData } from "../../utils";
import path from "path";
import { fileURLToPath } from "url";
import fs from "fs";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

describe("Auto Create with Defaults", () => {
	beforeEach(() => {
		resetDatabase();
	});

	it("applies defaults to documents auto-created via onNotFound: 'create'", () => {
		// 1. Setup config with defaults and onNotFound: 'create' for 'categories'
		const configPath = path.resolve(__dirname, "config.ts");
		const configContent = `
			export const cliConfig = {
				mappings: {
					categories: {
						lookupField: 'name',
						onNotFound: 'create',
						defaults: {
							displayName: 'AUTO_CREATED_DISPLAY_NAME'
						}
					}
				}
			}
		`;
		fs.writeFileSync(configPath, configContent);

		// 2. Create a post with a category that doesn't exist
		// This should trigger auto-creation of the category
		runCLI(`-c ${configPath} posts create '{"title": "New Post", "author": "admin@example.com", "category": "Brand New Category"}'`);

		// 3. Verify that the auto-created category has its default displayName
		const categories = getCollectionData("categories");
		const category = categories.find((c: any) => c.name === "Brand New Category");

		expect(category).toBeDefined();
		expect(category.name).toBe("Brand New Category");
		// This is the CRITICAL check for auto-create hierarchy support
		expect(category.displayName).toBe("AUTO_CREATED_DISPLAY_NAME");
	}, 60000);
});

Error Validation

Test Scenario (scenario.test.ts)

typescript
import { describe, it, expect, beforeEach } from "vitest";
import { runCLI, resetDatabase, getCollectionData } from "../../utils";
import path from "path";

describe("Error validation", () => {
	beforeEach(() => {
		resetDatabase();
	});

	it("fails when a required field is missing in create", () => {
		// 1. Attempt to create a post missing the required 'title'
		const postData = JSON.stringify({
			author: "dev@example.com",
			category: "general",
		});
		const output = runCLI(`posts create '${postData}'`);

		// 2. Verify the output contains the validation error
		expect(output).toContain("error: fatal:");
		expect(output.toLowerCase()).toContain("error");
		expect(output.toLowerCase()).toContain("title");
		expect(output.toLowerCase()).toContain("author");
		expect(output.toLowerCase()).toContain("category");

		expect(getCollectionData("posts")).toHaveLength(0);
	}, 60000);

	it("fails when a unique constraint is violated in create", () => {
		// 1. Create a category
		const output1 = runCLI(
			`categories create '{"name": "unique-cat", "displayName": "Unique"}'`,
		);
		expect(output1).toContain("Operation successful");

		// 2. Attempt to create another category with the same name
		const output = runCLI(
			`categories create '{"name": "unique-cat", "displayName": "Duplicate"}'`,
		);

		// 3. Verify the output contains the unique constraint error
		expect(output.toLowerCase()).toContain("error");
		expect(output.toLowerCase()).toContain("name");

		expect(getCollectionData("categories")).toHaveLength(1);
	}, 60000);
});

Error Missing Id

Data (data.jsonl)

jsonline
{"email":"charlie@example.com","password":"password123"}
{"email":"dave@example.com","password":"password123"}

Test Scenario (scenario.test.ts)

typescript
import { describe, it, expect, beforeEach } from "vitest";
import { runCLI, resetDatabase, getCollectionData } from "../../utils";
import path from "path";

describe("Error missing ID strictly enforced", () => {
	beforeEach(() => {
		resetDatabase();
	});

	it("throws an Error explicitly when no lookupField is configured and no id exists", () => {
		// 💡 Conversely, attempting an `upsert` of data lacking `id` without providing
		// a configuration mapping will strictly trigger the default `missing lookup field` error.
		const dataPath = path.resolve(__dirname, "data.jsonl");

		const output = runCLI(`users upsert ${dataPath}`);

		expect(output).toContain("lookup field 'id' in data");

		const users = getCollectionData("users");
		expect(users).toHaveLength(0); // DB remains untouched
	}, 30000);
});

Error Invalid Config

Data (data.jsonl)

jsonline
{"email":"error-config@example.com"}

Test Scenario (scenario.test.ts)

typescript
import { describe, it, expect, beforeEach } from "vitest";
import { runCLI, resetDatabase } from "../../utils";
import path from "path";

describe("Error invalid config", () => {
	beforeEach(() => {
		resetDatabase();
	});

	it("fails when an invalid onNotFound value is provided via -j", () => {
		const dataPath = path.resolve(__dirname, "data.jsonl");

		// 'invalid-action' is not a valid enum value for onNotFound
		const output = runCLI(
			`-j '{"mappings":{"users":{"onNotFound":"invalid-action"}}}' users upsert ${dataPath}`,
		);

		expect(output).toContain("error: [CONFIG_INVALID] Invalid configuration structure:");
		expect(output).toContain("mappings.users.onNotFound");
		expect(output).toContain("error");
		expect(output).toContain("ignore");
		expect(output).toContain("create");
	}, 60000);

	it("fails when lookupField is not a string", () => {
		const dataPath = path.resolve(__dirname, "data.jsonl");

		const output = runCLI(
			`-j '{"mappings":{"users":{"lookupField": 123}}}' users upsert ${dataPath}`,
		);

		expect(output).toContain("error: [CONFIG_INVALID] Invalid configuration structure:");
		expect(output).toContain("mappings.users.lookupField");
		expect(output).toContain("expected string, received number");
	}, 60000);
});

Error Stderr

Test Scenario (scenario.test.ts)

typescript
import { describe, it, expect, beforeEach } from "vitest";
import { runCLIFull, resetDatabase } from "../../utils";
import path from "path";

describe("Error stderr reporting", () => {
	beforeEach(() => {
		resetDatabase();
	});

	it("reports COLLECTION_NOT_FOUND to stderr with non-zero exit code", () => {
		const { stderr, stdout, status } = runCLIFull("non-existent-collection create '{}'");

		expect(status).toBe(1);
		// Error should be in stderr
		expect(stderr).toContain("error: [COLLECTION_NOT_FOUND]");
		expect(stdout).not.toContain("error: [COLLECTION_NOT_FOUND]");
	}, 60000);

	it("reports CONFIG_NOT_FOUND to stderr with non-zero exit code", () => {
		const { stderr, stdout, status } = runCLIFull("-c non-existent.config.ts users create '{}'");

		expect(status).toBe(1);
		expect(stderr).toContain("error: [CONFIG_NOT_FOUND]");
		expect(stderr).toContain("tip:");
		expect(stderr).toContain("refer to:");
		
		// Error should not be in stdout
		expect(stdout).not.toContain("error: [CONFIG_NOT_FOUND]");
	}, 60000);
});

TIP

All these examples are extracted directly from the e2e-tests/scenarios directory and are automatically verified on every CI run to ensure they remain perfectly functional with the latest Payload CMS version.