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 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);
});

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:");
		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: 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: Invalid configuration structure:");
		expect(output).toContain("mappings.users.lookupField");
		expect(output).toContain("expected string, received number");
	}, 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.