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.