Seed file for PayloadCMS

23 January 2023by Jack McGregor

Payload is great but sometimes it can be difficult to visualize your app in all its multi-user glory when you have to keep populating everything by hand. Fortunately, we can leverage Payload's awesome constructor and models, along with the new-and-improved FakerJS to create rich linked documents that will help you to hit the ground running!
For this demo, we're going to create Users, Posts & Pages with Media, and some Tags and Categories for our sprinkles. You can obviously use what models you want, you maverick.
So let's start with the configuration. The gist here is that we initialize an instance of Payload using a separate seed script. This script will use the same Collections, database and config as our actual app but will have a start and finish. For this reason, we will declare our seed directory outside of our src directory, so we will need a slightly different tsconfig.seed.js file
// tsconfig.seed.ts
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"strict": false,
"esModuleInterop": true,
"skipLibCheck": true,
"outDir": "./dist",
"rootDir": "./seed", // where our script will live
},
"include": [
"types.d.ts",
"seed"
],
"exclude": [
"src", // we don't want to bring in our src file
"node_modules",
"dist",
"build",
],
"ts-node": {
"transpileOnly": true
}
}
// package.json
{
// ...
"scripts": {
// ...
"seed": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts ts-node --project tsconfig.seed.json ./seed/index.ts",
},
"dependencies": {
// ...
"@aws-sdk/client-s3": "^3.223.0",
"dotenv": "^16.0.3",
"node-fetch": "2",
"payload-s3-upload": "^1.1.2",
},
"devDependencies": {
// ...
"@faker-js/faker": "^7.6.0",
}
}
So we have our tsconfig, our script (note how similar it is to the normal dev script), now we need the entry point file:
// seed/index.ts
import { config } from "dotenv";
import express from "express";
import { MongoClient } from "mongodb";
import payload from "payload";
const env = process.env.NODE_ENV || "development";
config({ path: `.env.${env}` });
const app = express();
payload.init({
secret: `${process.env.PAYLOAD_SECRET}`,
mongoURL: `${process.env.MONGODB_URI}`,
onInit: () => {
payload.logger.info(`Payload Admin URL: ${payload.getAdminURL()}`);
},
express: app,
});
// more code will go below...
If you were to now run yarn seed it wouldn't do much other than initialize the payload config and then shut down. To do actual seeding, I'm going to use a class as I love their abilities to store and share state in a simple object.
For educational purposes I've stripped out the logic for now and just left the method names so you can get a high-level view of the logic we'll be writing. But in case you can't be arsed to read the code, the script will seed the information in the following order:
  1. Media (because we want our other collections to have media objects)
  2. Users (Admins & Editors)
  3. Pages (requires Users and Media)
  4. Categories
  5. Tags
  6. Posts (requires Media, Users, Categories and Tags)
You'll also notice that after the class we do two things:
  1. We wrap the class in a function that initializes, drops previous data, seeds and then shuts down
  2. We call that function in a promise chain. This part isn't strictly necessary but it helps with debugging and keeping it clean
// seed/index.ts
type Role = "admin" | "editor";
type SeedUser = Record<Role, number>;
export class Seed {
private db: ReturnType<MongoClient["db"]>;
private client: MongoClient;
private mongoUri = new URL(`${process.env.MONGODB_URI}`).pathname.substring(
1
);
private users: Record<Role, number>;
constructor(users: SeedUser = { admin: 2, editor: 10 }) {
this.users = users;
}
async init() {
this.client = await this.makeClient();
this.db = this.client.db(this.mongoUri);
}
async makeClient() {
return MongoClient.connect(`${process.env.MONGODB_URI}`);
}
async seedDB() {
try {
// creating users from constructor specifications
console.log("Media....");
await this.uploadImage();
console.log("\nUsers....");
await this.createUsers();
console.log("\nPages....");
await this.createPages();
console.log("\nCategories....");
await this.createCategories();
console.log("\nTags....");
await this.createTags();
console.log("\nPosts....");
await this.createPosts();
} catch (error) {
console.log("seedDB error: ", error);
}
}
async uploadImage() {}
// we will need a super user to manage accounts
async createSuperUser() {}
async createUser(role: Role = "admin") {}
// will call this.createUser multiple times
async createUsers() {}
async createPage() {}
// will call this.createPage multiple times
async createPages() {}
async createPost() {}
// will call this.createPost multiple times
async createPosts() {}
async createCategories() {}
async createTags() {}
async dropDB() {
// drop database
await this.db.dropDatabase();
}
async teardown() {
await this.client.close();
}
}
const seed = async () => {
const seeder = new Seed();
console.log("\nInitialising...");
await seeder.init();
console.log("\nDropping...");
await seeder.dropDB();
console.log("\nSeeding...");
await seeder.seedDB();
console.log("\nShutting down...");
await seeder.teardown();
process.exit();
};
seed()
.then((x) => console.log(x))
.catch((err) => console.log(err));
Hopefully it's not too much and you can see where we're going.

Media

Let's start with our media. Not going to lie, I over complicated this one enormously when I first tried this, then I spent longer than I would care to admit trying to get it to store media on Amazon's S3 buckets. I'm not going to go into details on how to do that in here though. Sorry. The trick is, however, to use S3
// src/collections/Media.ts
import { IncomingUploadType } from "payload/dist/uploads/types";
import { CollectionConfig } from "payload/types";
type s3 = {
bucket: string;
prefix: string;
commandInput: any;
};
interface IncomingUploadTypeS3 extends IncomingUploadType {
s3: s3;
}
interface CollectionConfigS3 extends CollectionConfig {
upload: IncomingUploadTypeS3;
}
const Media: CollectionConfigS3 = {
slug: "media",
admin: {
useAsTitle: "name",
},
fields: [
{
name: "name",
type: "text",
},
{
name: "alt",
type: "text",
},
{
name: "url",
type: "text",
access: {
create: () => false,
read: () => true,
},
admin: {
disabled: true,
},
hooks: {
afterRead: [
({ data: doc }) => {
return `https://<your_bucket>.s3.<your_region>.amazonaws.com/images/blog/${doc.filename}`;
},
],
},
},
],
access: {
read: () => true,
create: () => true,
delete: () => true,
update: () => true,
},
upload: {
staticURL: "/media",
staticDir: "media",
mimeTypes: ["image/*"],
disableLocalStorage: true,
s3: {
bucket: "<your_bucket>",
prefix: "images/blog", // files will be stored in bucket folder images/xyz
commandInput: {
ACL: "public-read",
},
},
adminThumbnail: ({ doc }) =>
`https://<your_bucket>.s3.<your_region>.amazonaws.com/images/blog/${doc.filename}`,
imageSizes: [
{
name: "thumbnail",
width: 400,
height: 300,
position: "centre",
},
{
name: "card",
width: 768,
height: 1024,
position: "centre",
},
{
name: "tablet",
width: 1024,
// By specifying `null` or leaving a height undefined,
// the image will be sized to a certain width,
// but it will retain its original aspect ratio
// and calculate a height automatically.
height: null,
position: "centre",
},
],
},
};
export default Media;
So we have our Media Collection, let's import it into our seed file and hook it up, but before we do this you will need to create a local file for the images you wish to use.
I've done this within seed/media, so will look a bit like this:
- seed/
- media/
- pic-1.jpg
- pic-2.jpg
- etc
// seed/index.ts
export class Seed {
private mediaIds: string[] = []; // create a property; we'll use this later
// ...
async seedDB() {
try {
// creating users from contructor specifications
console.log("Media....");
await this.uploadImage();
// console.log("\nUsers....");
// await this.createUsers();
// etc
} catch (error) {
console.log("seedDB error: ", error);
}
}
async uploadImage() {
let pics: string[] = [];
for (let i = 1; i < 8; i++) {
pics.push(`pic-${i}.jpg`);
}
// loop through local media and use payload.create,
// passing in the Collection name, filepath and data (eg alt)
pics.forEach(async (pic, i) => {
const { id } = await payload.create({
collection: "media",
data: {
alt: `Alternative ${i}`,
},
filePath: path.resolve(__dirname, `./media/${pic}`),
});
this.mediaIds = [...this.mediaIds, id]; // add to the class state
});
}
// ...
}
Now, after running your script, you should see the logs and if everything is set up properly then you will see your images appear in your Amazon S3 Bucket and their reference IDs in your database in the Media collection.
As a bonus, why not try modelling, creating and storing different types of media like gifs, documents, videos and music? It's a good exercise to practice what you've learned.

Users

Every app needs users, both administrative (provided by Payload) and business-use (created by us). Here's a simple User Collection; yours will probably look much different
const isAdminFieldLevel: FieldAccess<{ id: string }, unknown, IUser> = ({
req: { user },
}) => {
return Boolean(user?.roles?.includes("admin"));
};
const Users: CollectionConfig = {
slug: "users",
auth: {
depth: 1,
useAPIKey: true,
},
admin: {
useAsTitle: "email",
},
access: {
read: () => true,
},
labels: {
plural: "Users",
singular: "user",
},
fields: [
// Email added by default
{
name: "roles",
saveToJWT: true,
type: "select",
hasMany: true,
defaultValue: ["editor"],
access: {
read: () => true,
create: isAdminFieldLevel,
update: sAdminFieldLevel,
},
options: [
{
label: "Admin",
value: "admin",
},
{
label: "Editor",
value: "editor",
},
],
},
{
type: "row",
fields: [
{
name: "firstName",
label: "First Name",
type: "text",
required: true,
},
{
name: "lastName",
label: "Last Name",
type: "text",
required: true,
},
],
},
],
};
export default Users;
Once we have a User, we'll create our SuperAdmin user first; this just makes it easier for us to go into the app to check our progress
export class Seed {
private superUserId = "";
async createSuperUser() {
const { id } = await payload.create<User>({
collection: "users",
data: {
firstName: "Admin",
lastName: "User",
email: "admin@test.com",
password: "Password1!",
roles: ["admin"],
abilities: [],
},
});
this.superUserId = id.toString();
return id.toString();
}
}
You can probably see where this is going, but its where we first get to use Faker!
When we run the script, we use the default user values (2 admin and 10 editors) to create 2 admins & 10 editors with fake names, emails, passwords (that we can actually use to log in as them), but also a Super Admin who's details stay the same and you can grant any executive privileges to should you choose to.
export class Seed {
private users: Record<Role, number>; // property filled during initialization
private superUserId = "";
private adminIds: string[] = [];
private editorIds: string[] = [];
async seedDB() {
try {
// creating users from contructor specifications
console.log("Media....");
await this.uploadImage();
console.log("\nUsers....");
await this.createUsers();
} catch (error) {
console.log("seedDB error: ", error);
}
}
async createSuperUser() {
// User is generated by yarn generate:types
const { id } = await payload.create<User>({
collection: "users",
data: {
firstName: "Admin",
lastName: "User",
email: "admin@test.com",
password: "Password1!",
roles: ["admin"],
abilities: [],
},
});
this.superUserId = id.toString();
return id.toString();
}
async createUser(role: Role = "admin") {
const email = faker.internet.email();
const firstName = faker.name.firstName();
const lastName = faker.name.lastName();
const { id } = await payload.create<User>({
collection: "users",
data: {
firstName,
lastName,
email,
password: "password",
roles: [role === "admin" ? "editor" : "admin"],
},
});
return id.toString();
}
async createUsers() {
// create Super User & add to users
const superUser = await this.createSuperUser();
this.adminIds = [...this.adminIds, superUser];
// create users
await Promise.all(
Object.entries(this.users).map(
// @ts-ignore
async ([role, quantity]: [Role, number], i) => {
// make Users
for (let i = 0; i < quantity; i++) {
// returns ID of new user
const user = await this.createUser(role);
if (role === "admin") {
this.adminIds = [...this.adminIds, user];
}
if (role === "editor") {
this.editorIds = [...this.editorIds, user];
}
}
}
)
);
}
}
Pages
We now have Media and Users, so we can start making content. We'll start with the simpler Collection, Pages.
See the simple Page Collection below:
import { CollectionConfig } from "payload/types";
const Pages: CollectionConfig = {
slug: "pages",
admin: {
defaultColumns: ["title"],
useAsTitle: "title",
},
auth: false, // auth: true disables seeding of collection
access: {
read: () => true,
create: () => true,
update: () => true,
delete: () => true,
},
fields: [
{
name: "title",
type: "text",
},
{
name: "publishedDate",
type: "date",
},
{
name: "content",
type: "richText",
admin: {
upload: {
collections: {
media: {
fields: [
// any fields that you would like to save
// on an upload element in the `media` collection
{
name: "alt",
type: "text",
},
],
},
},
},
},
},
{
name: "author",
type: "relationship",
relationTo: "users",
access: {
read: () => true,
create: () => true,
},
},
{
name: "backgroundImage",
label: "backgroundImage",
type: "upload",
relationTo: "media",
access: {
read: () => true,
create: () => true,
},
},
],
};
export default Pages;
Pretty simple. The Media field will save the image itself in our bucket but the ID in Mongo and reference it in this collection.
However, since we want to generate Rich Text to mimic the Rich Text Editor with which I'm writing this post, we need to replicate its structure.
We could create another method within the class to handle this but we might as well utilize inheritance.
In this example, I've created two more classes:
  • SeedHelper
    • Generic helpers eg. shuffle
    • Will be inherited by our Seed class
    • Will in turn inherit...
  • RichTextHelper
    • Will create structures like headers, paragraphs, lists etc
// seed/RichTextHelper.ts
import { faker } from "@faker-js/faker";
type Header = "h1" | "h2" | "h3" | "h4" | "h5" | "h6";
type List = "ol" | "ul";
export class RichTextHelper {
header() {
const size = this.headerSize();
const text = faker.lorem.sentence();
return {
type: size,
children: [
{
text,
},
],
};
}
headerSize(): Header {
const header = Math.ceil(Math.random() * 6);
switch (header) {
case 1:
return "h1";
case 2:
return "h2";
case 3:
return "h3";
case 4:
return "h4";
case 5:
return "h5";
default:
return "h6";
}
}
list(type: List = "ul", items = 5) {
let listItems: any = [];
for (let i = 0; i < items; i++) {
const item = this.listItem();
listItems = [...listItems, item];
}
return {
type: type,
children: listItems,
};
}
listItem() {
const text = faker.lorem.sentence();
return {
type: "li",
children: [
{
text,
},
],
};
}
media(id: string) {
return {
children: [
{
text: "",
},
],
type: "upload",
value: {
id,
},
relationTo: "media",
};
}
paragraph(sentences = 3) {
const text = faker.lorem.sentences(sentences);
const isBold = Math.random() < 0.1;
const isItalic = Math.random() < 0.1;
const isUnderline = Math.random() < 0.1;
const isCode = Math.random() < 0.1;
return {
type: "p",
children: [
{
text,
bold: isBold,
italic: isItalic,
underline: isUnderline,
code: isCode,
},
],
};
}
}
// Seedhelper.ts
import { RichTextHelper } from "./RichTextHelper";
export class SeedHelper extends RichTextHelper {
constructor() {
super();
}
shuffle(arr: any[]) {
return arr
.map((value) => ({ value, sort: Math.random() }))
.sort((a, b) => a.sort - b.sort)
.map(({ value }) => value);
}
makeRichText(
{ paragraphs, imageIds }: { paragraphs: number; imageIds: string[] } = {
paragraphs: 3,
imageIds: [],
}
) {
const sentences = Math.ceil(Math.random() * 5);
const headers = Math.ceil(Math.random() * 3);
const lists = Math.ceil(Math.random() * 3);
const images = Math.ceil(Math.random() * 3);
let content: any[] = [];
// make paragraphs
for (let i = 0; i < paragraphs; i++) {
const p = this.paragraph(sentences);
content = [...content, p];
}
// make headers
for (let i = 0; i < headers; i++) {
const header = this.header();
content = [...content, header];
}
// make lists
for (let i = 0; i < lists; i++) {
const listType = Math.random() < 0.5 ? "ul" : "ol";
const list = this.list(listType);
content = [...content, list];
}
// add images
const shuffledImages = this.shuffle(imageIds).slice(images);
const mappedImages = shuffledImages.map((imgId) => this.media(imgId));
content = [...content, ...mappedImages];
// shuffle everything
const sorted = this.shuffle(content);
return sorted;
}
}
In a nutshell, we randomly generate arrays of Rich Text blocks in order (headers then lists then paragraphs etc) and then shuffle the array at the end using another helper method defined in the helper class.
Once you have your helpers, you can create the main methods. Each Page needs and author which we grab randomly from `this.users`
Also important to note that the way the methods are configured you can consistently create the same pages every time (eg About, Contact, Clients, whatever), as well as random ones. This is useful if you have both static pages and dynamic pages.
export class Seed extends SeedHelper {
// ...
private pageIds: string[] = [];
async seedDB() {
try {
// creating users from contructor specifications
console.log("Media....");
await this.uploadImage();
console.log("\nUsers....");
await this.createUsers();
console.log("\nPages....");
await this.createPages();
// ...
} catch (error) {
console.log("seedDB error: ", error);
}
}
async createPage(
userId: string,
title = faker.lorem.sentence()
): Promise<string> {
const paragraphs = Math.ceil(Math.random() * 10);
const randImage = this.shuffle([...this.mediaIds])[0];
const page = await payload.create<any>({
collection: "pages",
data: {
author: userId,
title,
content: this.makeRichText({ paragraphs, imageIds: this.mediaIds }),
publishedDate: new Date(Date.now()),
backgroundImage: randImage,
},
});
return page.id;
}
async createPages(num = 5) {
const admins = this.adminIds.length;
const editors = this.editorIds.length;
await this.createPage(this.superUserId, "about");
await this.createPage(this.superUserId, "contact");
for (let i = 0; i < num; i++) {
const randId: Role = Math.random() > 0.3 ? "editor" : "admin";
const randUserId: string =
randId === "admin"
? this.adminIds[Math.ceil(Math.random() * admins) - 1]
: this.editorIds[Math.ceil(Math.random() * editors) - 1];
const id = await this.createPage(randUserId);
this.pageIds = [...this.pageIds, id];
}
}
}
Excellent! Running your script now should drop then populate Media, Users and Pages with relations to each other.

Categories and Tags

Since these are very simple I'm going to write them up together. Basically, we want to include these for our Post documents.
In my example, a Category would be more general, like Development, Testing or Deployment, whereas a Tag would be more specific, like AWS, Payload, Docker, Typescript or Jest. Again, just an example. Go crazy.
// src/collections/Categories.ts
import { CollectionConfig } from "payload/types";
const Categories: CollectionConfig = {
slug: "categories",
admin: {
useAsTitle: "name",
},
access: {
read: () => true,
create: () => true,
update: () => true,
delete: () => true,
},
fields: [
{
name: "name",
type: "text",
},
],
timestamps: false,
};
export default Categories;
// src/collections/Tags.ts
import { CollectionConfig } from "payload/types";
const Tags: CollectionConfig = {
slug: "tags",
admin: {
useAsTitle: "name",
},
access: {
read: () => true,
create: () => true,
update: () => true,
delete: () => true,
},
fields: [
{
name: "name",
type: "text",
},
],
timestamps: false,
};
export default Tags;
Cool. Now for the seeding method. I chose to create the same tags each time but you're under no obligation to do that. Fill your boots with Faker data!
export class Seed extends SeedHelper {
// ...
private categoryIds: string[] = [];
private tagIds: string[] = [];
async seedDB() {
try {
// creating users from contructor specifications
console.log("Media....");
await this.uploadImage();
console.log("\nUsers....");
await this.createUsers();
console.log("\nPages....");
await this.createPages();
console.log("\nCategories....");
await this.createCategories();
console.log("\nTags....");
await this.createTags();
// ...
} catch (error) {
console.log("seedDB error: ", error);
}
}
async createCategories() {
// Create Categories
const categories = await Promise.all([
payload.create<Category>({
collection: "categories",
data: {
name: "news",
},
}),
payload.create<Category>({
collection: "categories",
data: {
name: "feature",
},
}),
payload.create<Category>({
collection: "categories",
data: {
name: "tutorial",
},
}),
]);
this.categoryIds = categories.map((c) => c.id);
}
async createTags() {
// Create Categories
const tags = await Promise.all([
payload.create<Tag>({
collection: "tags",
data: {
name: "funny",
},
}),
payload.create<Tag>({
collection: "tags",
data: {
name: "comic",
},
}),
payload.create<Tag>({
collection: "tags",
data: {
name: "marvel",
},
}),
]);
this.tagIds = tags.map((c) => c.id);
}
}
Run your script to check it works. If it does, awesome. If not then put on your debugger hat. This post doesn't have comments. Sorry

Posts

Finally onto the main event: creating a Post that utilizes our Media, Users, Categories and Tags!
Here's our Collection:
// src/collections/Posts.ts
import { CollectionConfig } from "payload/types";
import { PostHooks } from "../hooks/posts";
import { ContentBlock } from "../blocks/Content";
import { CodeBlock } from "../blocks/Code";
const Posts: CollectionConfig = {
slug: "posts",
admin: {
defaultColumns: ["title", "author", "category", "tags", "status"],
useAsTitle: "title",
},
auth: false, // auth: true disables seeding of collection
access: {
read: () => true,
create: () => true,
update: () => true,
delete: () => true,
},
fields: [
{
name: "title",
type: "text",
},
{
name: "author",
type: "relationship",
relationTo: "users",
access: {
read: () => true,
create: () => true,
},
},
{
name: "backgroundImage",
label: "backgroundImage",
type: "upload",
relationTo: "media",
access: {
read: () => true,
create: () => true,
},
},
{
name: "publishedDate",
type: "date",
},
{
name: "category",
type: "relationship",
relationTo: "categories",
},
{
name: "tags",
type: "relationship",
relationTo: "tags",
hasMany: true,
},
{
name: "content",
type: "richText",
admin: {
upload: {
collections: {
media: {
fields: [
// any fields that you would like to save
// on an upload element in the `media` collection
{
name: "alt",
type: "text",
},
],
},
},
},
},
},
{
name: "status",
type: "select",
options: [
{
value: "draft",
label: "Draft",
},
{
value: "published",
label: "Published",
},
],
defaultValue: "draft",
admin: {
position: "sidebar",
},
},
],
hooks: {
beforeChange: [PostHooks.setCreatedBy],
},
};
export default Posts;
And our method:
export class Seed extends SeedHelper {
// ...
private postIds: string[] = [];
async seedDB() {
try {
// creating users from contructor specifications
console.log("Media....");
await this.uploadImage();
console.log("\nUsers....");
await this.createUsers();
console.log("\nPages....");
await this.createPages();
console.log("\nCategories....");
await this.createCategories();
console.log("\nTags....");
await this.createTags();
console.log("\nPosts....");
await this.createPosts();
} catch (error) {
console.log("seedDB error: ", error);
}
}
async createPost(userId: string): Promise<string> {
// 0 - 10 paragraphs
const paragraphs = Math.ceil(Math.random() * 10);
// random category
const category = Math.ceil(Math.random() * this.categoryIds.length);
const shuffledTags = this.shuffle(this.tagIds);
// random tags (shuffle array then take first x items)
const tags = shuffledTags.slice(
1,
Math.ceil(Math.random() * shuffledTags.length + 1)
);
const randomImage = this.shuffle(this.mediaIds)[0];
const post = await payload.create<any>({
collection: "posts",
data: {
author: userId,
title: faker.lorem.sentence(),
content: this.makeRichText({ paragraphs, imageIds: this.mediaIds }),
publishedDate: new Date(Date.now()),
category: this.categoryIds[category],
tags,
status: Math.random() > 0.5 ? "draft" : "published",
backgroundImage: randomImage,
},
});
return post.id;
}
async createPosts(num = 100) {
const admins = this.adminIds.length;
const editors = this.editorIds.length;
for (let i = 0; i < num; i++) {
const randId: Role = Math.random() > 0.3 ? "editor" : "admin";
const randUserId: string =
randId === "admin"
? this.adminIds[Math.ceil(Math.random() * admins) - 1]
: this.editorIds[Math.ceil(Math.random() * editors) - 1];
const id = await this.createPost(randUserId);
this.postIds = [...this.postIds, id];
}
}
}
And there you have it!
A very rough seed file with 5 related collections, Media in S3, Rich Text maker and Faker and a smidge of inheritance. No doubt some people will look at this with horror but who cares - its just a rough guide to help you with the general direction. Hope you enjoyed it!
Playwright ID scraper31 January 2023Rendering languages in code blocks17 January 2023Creating dynamic test IDs for Jest and Playwright17 January 2023