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.