@payload-cc/payload-collection-cli 1.0.9 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -13,14 +13,23 @@ pnpm add @payload-cc/payload-collection-cli
13
13
 
14
14
  ## Quick Start
15
15
 
16
- You can immediately start using the commands without any configuration!
16
+ You can immediately start using the commands without any configuration for basic insertions/updates!
17
17
 
18
+ **Command Syntax**:
19
+ ```bash
20
+ npx @payload-cc/payload-collection-cli [-c config-file] <collection-slug> <operation> <file or string>
21
+ ```
22
+
23
+ **Examples**:
18
24
  ```bash
19
25
  # Bulk create/upsert from jsonlines
20
26
  npx @payload-cc/payload-collection-cli posts upsert data.jsonl
21
27
 
22
28
  # Simple JSON update
23
29
  npx @payload-cc/payload-collection-cli users update '{"email": "user@example.com", "name": "New Name"}'
30
+
31
+ # With explicit mapping configuration
32
+ npx @payload-cc/payload-collection-cli -c ./my-map.config.ts users upsert data.jsonl
24
33
  ```
25
34
 
26
35
  ## Available Actions
@@ -29,10 +38,18 @@ npx @payload-cc/payload-collection-cli users update '{"email": "user@example.com
29
38
  - `delete`: Delete records.
30
39
  - `upsert`: Update if existing, create if not.
31
40
 
41
+ ## Specifications & FAQ
42
+ For detailed behavior specifications (such as how identifier lookups work strictly during upserts, updates, and deletes), please refer to the [Specifications & FAQ](docs/references/specs_detail.md).
43
+
32
44
  ## Configuration (Optional)
33
45
 
34
46
  ### Relation Mappings (`payload-collection-cli.config.ts`)
35
- If you want the CLI to magically resolve relation IDs (e.g. searching a User by email to relate to a Post), create a `payload-collection-cli.config.ts` in your Payload project root:
47
+ By default, Payload relations require you to provide target document IDs (e.g., ObjectIDs or numeric IDs). The CLI can magically resolve these relations by searching for human-readable fields instead.
48
+
49
+ **Example Scenario**:
50
+ Assume your `posts` collection has a relationship field named `author` that references the `users` collection. Instead of manually finding and hard-coding the user's database ID, you want to simply provide their email address.
51
+
52
+ Create a `payload-collection-cli.config.ts` in your Payload project root (or pass it via `-c`) to define the `lookupField` for the `users` collection:
36
53
 
37
54
  ```typescript
38
55
  export const cliConfig = {
@@ -48,7 +65,7 @@ export const cliConfig = {
48
65
  }
49
66
  ```
50
67
 
51
- Now, when you supply an `author: "user@example.com"` property to a `posts` collection insertion, the CLI will look up the user by email in the exact database rather than forcing you to hard-code Payload ObjectId strings!
68
+ Now, when you supply an `author: "user@example.com"` property to a `posts` collection insertion, the CLI will intercept this relationship, look up the `users` collection by the `email` field, and automatically replace the email string with the actual database ID before inserting!
52
69
 
53
70
  ## Development & CI/CD
54
71
 
package/dist/bin.js CHANGED
@@ -42,12 +42,20 @@ async function resolveRelations(payload, collectionSlug, data, config) {
42
42
  const collection = payload.collections[collectionSlug];
43
43
  if (!collection) throw new Error(`Collection "${collectionSlug}" not found.`);
44
44
  const resolved = { ...data };
45
+ const mappingConfig = config.mappings[collectionSlug];
46
+ if (mappingConfig?.defaults) {
47
+ for (const [k, v] of Object.entries(mappingConfig.defaults)) {
48
+ if (resolved[k] === void 0 || resolved[k] === null) {
49
+ resolved[k] = v;
50
+ }
51
+ }
52
+ }
45
53
  for (const field of collection.config.fields) {
46
- if (field.type === "relationship" && data[field.name]) {
54
+ if (field.type === "relationship" && resolved[field.name]) {
47
55
  const relationTo = Array.isArray(field.relationTo) ? field.relationTo[0] : field.relationTo;
48
56
  const mapping = config.mappings[relationTo];
49
57
  if (!mapping) continue;
50
- const rawValue = data[field.name];
58
+ const rawValue = resolved[field.name];
51
59
  const isArray = Array.isArray(rawValue);
52
60
  const values = isArray ? rawValue : [rawValue];
53
61
  const resolvedIds = [];
@@ -60,6 +68,12 @@ async function resolveRelations(payload, collectionSlug, data, config) {
60
68
  });
61
69
  if (found.docs.length > 0) {
62
70
  resolvedIds.push(found.docs[0].id);
71
+ } else if (mapping.onNotFound === "create") {
72
+ const created = await payload.create({
73
+ collection: relationTo,
74
+ data: { [mapping.lookupField]: val }
75
+ });
76
+ resolvedIds.push(created.id);
63
77
  } else if (mapping.onNotFound === "error") {
64
78
  throw new Error(`Relation not found: ${relationTo} (${mapping.lookupField}=${val})`);
65
79
  } else {
@@ -84,24 +98,34 @@ async function processSingle(payload, collection, action, data, config) {
84
98
  case "create":
85
99
  return await payload.create({ collection, data: resolved });
86
100
  case "upsert":
87
- const existing = await payload.find({
101
+ if (data[lookupField] === void 0) {
102
+ throw new Error(`[upsert] Missing lookup field '${lookupField}' in data. Cannot perform upsert.`);
103
+ }
104
+ const existingUpsert = await payload.find({
88
105
  collection,
89
106
  where: { [lookupField]: { equals: data[lookupField] } }
90
107
  });
91
- if (existing.docs.length > 0) {
92
- return await payload.update({ collection, id: existing.docs[0].id, data: resolved });
108
+ if (existingUpsert.docs.length > 0) {
109
+ return await payload.update({ collection, id: existingUpsert.docs[0].id, data: resolved });
93
110
  }
94
111
  return await payload.create({ collection, data: resolved });
95
112
  case "update":
113
+ if (data[lookupField] === void 0) {
114
+ throw new Error(`[update] Missing lookup field '${lookupField}' in data. Cannot perform update.`);
115
+ }
96
116
  return await payload.update({
97
117
  collection,
98
118
  where: { [lookupField]: { equals: data[lookupField] } },
99
119
  data: resolved
100
120
  });
101
121
  case "delete":
122
+ const delVal = typeof data === "object" ? data[lookupField] : data;
123
+ if (delVal === void 0) {
124
+ throw new Error(`[delete] Missing lookup field '${lookupField}' in action. Cannot perform delete.`);
125
+ }
102
126
  return await payload.delete({
103
127
  collection,
104
- where: { [lookupField]: { equals: typeof data === "object" ? data[lookupField] : data } }
128
+ where: { [lookupField]: { equals: delVal } }
105
129
  });
106
130
  default:
107
131
  throw new Error(`Unsupported action: ${action}`);
@@ -122,10 +146,38 @@ async function execute(payload, collection, action, input, config) {
122
146
  // src/bin.ts
123
147
  var jiti = (0, import_jiti.createJiti)(importMetaUrl);
124
148
  async function run() {
125
- const [collection, action, input] = process.argv.slice(2);
149
+ const args = process.argv.slice(2);
150
+ let configOptIdx = args.indexOf("-c");
151
+ if (configOptIdx === -1) configOptIdx = args.indexOf("--config");
152
+ let cliConfig = { mappings: {} };
153
+ if (configOptIdx !== -1) {
154
+ if (args.length <= configOptIdx + 1) {
155
+ console.error("\u274C Error: Missing option after --config parameter.");
156
+ process.exit(1);
157
+ }
158
+ const val = args[configOptIdx + 1];
159
+ args.splice(configOptIdx, 2);
160
+ if (val.trim().startsWith("{")) {
161
+ try {
162
+ cliConfig = JSON.parse(val);
163
+ } catch (err) {
164
+ console.error("\u274C Error: Failed to parse inline JSON config:", err);
165
+ process.exit(1);
166
+ }
167
+ } else {
168
+ const customConfigPath = import_path2.default.resolve(process.cwd(), val);
169
+ if (!require("fs").existsSync(customConfigPath)) {
170
+ console.error(`\u274C Error: Config file not found at ${customConfigPath}`);
171
+ process.exit(1);
172
+ }
173
+ const imported = await jiti.import(customConfigPath);
174
+ cliConfig = imported.cliConfig || imported.default || cliConfig;
175
+ }
176
+ }
177
+ const [collection, action, input] = args;
126
178
  const root = process.cwd();
127
179
  if (!collection || !action || !input) {
128
- console.log("Usage: payload-collection-cli <collection> <action> <json|file.jsonl>");
180
+ console.log("Usage: payload-collection-cli [-c path] <collection> <action> <json|file.jsonl>");
129
181
  process.exit(1);
130
182
  }
131
183
  const configPath = [
@@ -134,12 +186,6 @@ async function run() {
134
186
  ].find((p) => require("fs").existsSync(p));
135
187
  if (!configPath) throw new Error("payload.config.ts not found.");
136
188
  const { default: payloadConfig } = await jiti.import(configPath);
137
- let cliConfig = { mappings: {} };
138
- const cliCfgPath = import_path2.default.resolve(root, "payload-collection-cli.config.ts");
139
- if (require("fs").existsSync(cliCfgPath)) {
140
- const imported = await jiti.import(cliCfgPath);
141
- cliConfig = imported.cliConfig || imported.default || cliConfig;
142
- }
143
189
  const payload = await (0, import_payload.getPayload)({ config: payloadConfig });
144
190
  try {
145
191
  const result = await execute(payload, collection, action, input, cliConfig);
package/dist/bin.mjs CHANGED
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  __require,
4
4
  execute
5
- } from "./chunk-3HKWP2RE.mjs";
5
+ } from "./chunk-RIXWYITK.mjs";
6
6
 
7
7
  // src/bin.ts
8
8
  import { getPayload } from "payload";
@@ -10,10 +10,38 @@ import { createJiti } from "jiti";
10
10
  import path from "path";
11
11
  var jiti = createJiti(import.meta.url);
12
12
  async function run() {
13
- const [collection, action, input] = process.argv.slice(2);
13
+ const args = process.argv.slice(2);
14
+ let configOptIdx = args.indexOf("-c");
15
+ if (configOptIdx === -1) configOptIdx = args.indexOf("--config");
16
+ let cliConfig = { mappings: {} };
17
+ if (configOptIdx !== -1) {
18
+ if (args.length <= configOptIdx + 1) {
19
+ console.error("\u274C Error: Missing option after --config parameter.");
20
+ process.exit(1);
21
+ }
22
+ const val = args[configOptIdx + 1];
23
+ args.splice(configOptIdx, 2);
24
+ if (val.trim().startsWith("{")) {
25
+ try {
26
+ cliConfig = JSON.parse(val);
27
+ } catch (err) {
28
+ console.error("\u274C Error: Failed to parse inline JSON config:", err);
29
+ process.exit(1);
30
+ }
31
+ } else {
32
+ const customConfigPath = path.resolve(process.cwd(), val);
33
+ if (!__require("fs").existsSync(customConfigPath)) {
34
+ console.error(`\u274C Error: Config file not found at ${customConfigPath}`);
35
+ process.exit(1);
36
+ }
37
+ const imported = await jiti.import(customConfigPath);
38
+ cliConfig = imported.cliConfig || imported.default || cliConfig;
39
+ }
40
+ }
41
+ const [collection, action, input] = args;
14
42
  const root = process.cwd();
15
43
  if (!collection || !action || !input) {
16
- console.log("Usage: payload-collection-cli <collection> <action> <json|file.jsonl>");
44
+ console.log("Usage: payload-collection-cli [-c path] <collection> <action> <json|file.jsonl>");
17
45
  process.exit(1);
18
46
  }
19
47
  const configPath = [
@@ -22,12 +50,6 @@ async function run() {
22
50
  ].find((p) => __require("fs").existsSync(p));
23
51
  if (!configPath) throw new Error("payload.config.ts not found.");
24
52
  const { default: payloadConfig } = await jiti.import(configPath);
25
- let cliConfig = { mappings: {} };
26
- const cliCfgPath = path.resolve(root, "payload-collection-cli.config.ts");
27
- if (__require("fs").existsSync(cliCfgPath)) {
28
- const imported = await jiti.import(cliCfgPath);
29
- cliConfig = imported.cliConfig || imported.default || cliConfig;
30
- }
31
53
  const payload = await getPayload({ config: payloadConfig });
32
54
  try {
33
55
  const result = await execute(payload, collection, action, input, cliConfig);
@@ -10,12 +10,20 @@ async function resolveRelations(payload, collectionSlug, data, config) {
10
10
  const collection = payload.collections[collectionSlug];
11
11
  if (!collection) throw new Error(`Collection "${collectionSlug}" not found.`);
12
12
  const resolved = { ...data };
13
+ const mappingConfig = config.mappings[collectionSlug];
14
+ if (mappingConfig?.defaults) {
15
+ for (const [k, v] of Object.entries(mappingConfig.defaults)) {
16
+ if (resolved[k] === void 0 || resolved[k] === null) {
17
+ resolved[k] = v;
18
+ }
19
+ }
20
+ }
13
21
  for (const field of collection.config.fields) {
14
- if (field.type === "relationship" && data[field.name]) {
22
+ if (field.type === "relationship" && resolved[field.name]) {
15
23
  const relationTo = Array.isArray(field.relationTo) ? field.relationTo[0] : field.relationTo;
16
24
  const mapping = config.mappings[relationTo];
17
25
  if (!mapping) continue;
18
- const rawValue = data[field.name];
26
+ const rawValue = resolved[field.name];
19
27
  const isArray = Array.isArray(rawValue);
20
28
  const values = isArray ? rawValue : [rawValue];
21
29
  const resolvedIds = [];
@@ -28,6 +36,12 @@ async function resolveRelations(payload, collectionSlug, data, config) {
28
36
  });
29
37
  if (found.docs.length > 0) {
30
38
  resolvedIds.push(found.docs[0].id);
39
+ } else if (mapping.onNotFound === "create") {
40
+ const created = await payload.create({
41
+ collection: relationTo,
42
+ data: { [mapping.lookupField]: val }
43
+ });
44
+ resolvedIds.push(created.id);
31
45
  } else if (mapping.onNotFound === "error") {
32
46
  throw new Error(`Relation not found: ${relationTo} (${mapping.lookupField}=${val})`);
33
47
  } else {
@@ -55,24 +69,34 @@ async function processSingle(payload, collection, action, data, config) {
55
69
  case "create":
56
70
  return await payload.create({ collection, data: resolved });
57
71
  case "upsert":
58
- const existing = await payload.find({
72
+ if (data[lookupField] === void 0) {
73
+ throw new Error(`[upsert] Missing lookup field '${lookupField}' in data. Cannot perform upsert.`);
74
+ }
75
+ const existingUpsert = await payload.find({
59
76
  collection,
60
77
  where: { [lookupField]: { equals: data[lookupField] } }
61
78
  });
62
- if (existing.docs.length > 0) {
63
- return await payload.update({ collection, id: existing.docs[0].id, data: resolved });
79
+ if (existingUpsert.docs.length > 0) {
80
+ return await payload.update({ collection, id: existingUpsert.docs[0].id, data: resolved });
64
81
  }
65
82
  return await payload.create({ collection, data: resolved });
66
83
  case "update":
84
+ if (data[lookupField] === void 0) {
85
+ throw new Error(`[update] Missing lookup field '${lookupField}' in data. Cannot perform update.`);
86
+ }
67
87
  return await payload.update({
68
88
  collection,
69
89
  where: { [lookupField]: { equals: data[lookupField] } },
70
90
  data: resolved
71
91
  });
72
92
  case "delete":
93
+ const delVal = typeof data === "object" ? data[lookupField] : data;
94
+ if (delVal === void 0) {
95
+ throw new Error(`[delete] Missing lookup field '${lookupField}' in action. Cannot perform delete.`);
96
+ }
73
97
  return await payload.delete({
74
98
  collection,
75
- where: { [lookupField]: { equals: typeof data === "object" ? data[lookupField] : data } }
99
+ where: { [lookupField]: { equals: delVal } }
76
100
  });
77
101
  default:
78
102
  throw new Error(`Unsupported action: ${action}`);
package/dist/index.d.mts CHANGED
@@ -4,6 +4,7 @@ import { Payload } from 'payload';
4
4
  interface MappingConfig {
5
5
  lookupField: string;
6
6
  onNotFound?: 'error' | 'ignore' | 'create';
7
+ defaults?: Record<string, any>;
7
8
  }
8
9
  interface CLIConfig {
9
10
  mappings: Record<string, MappingConfig>;
package/dist/index.d.ts CHANGED
@@ -4,6 +4,7 @@ import { Payload } from 'payload';
4
4
  interface MappingConfig {
5
5
  lookupField: string;
6
6
  onNotFound?: 'error' | 'ignore' | 'create';
7
+ defaults?: Record<string, any>;
7
8
  }
8
9
  interface CLIConfig {
9
10
  mappings: Record<string, MappingConfig>;
package/dist/index.js CHANGED
@@ -40,12 +40,20 @@ async function resolveRelations(payload, collectionSlug, data, config) {
40
40
  const collection = payload.collections[collectionSlug];
41
41
  if (!collection) throw new Error(`Collection "${collectionSlug}" not found.`);
42
42
  const resolved = { ...data };
43
+ const mappingConfig = config.mappings[collectionSlug];
44
+ if (mappingConfig?.defaults) {
45
+ for (const [k, v] of Object.entries(mappingConfig.defaults)) {
46
+ if (resolved[k] === void 0 || resolved[k] === null) {
47
+ resolved[k] = v;
48
+ }
49
+ }
50
+ }
43
51
  for (const field of collection.config.fields) {
44
- if (field.type === "relationship" && data[field.name]) {
52
+ if (field.type === "relationship" && resolved[field.name]) {
45
53
  const relationTo = Array.isArray(field.relationTo) ? field.relationTo[0] : field.relationTo;
46
54
  const mapping = config.mappings[relationTo];
47
55
  if (!mapping) continue;
48
- const rawValue = data[field.name];
56
+ const rawValue = resolved[field.name];
49
57
  const isArray = Array.isArray(rawValue);
50
58
  const values = isArray ? rawValue : [rawValue];
51
59
  const resolvedIds = [];
@@ -58,6 +66,12 @@ async function resolveRelations(payload, collectionSlug, data, config) {
58
66
  });
59
67
  if (found.docs.length > 0) {
60
68
  resolvedIds.push(found.docs[0].id);
69
+ } else if (mapping.onNotFound === "create") {
70
+ const created = await payload.create({
71
+ collection: relationTo,
72
+ data: { [mapping.lookupField]: val }
73
+ });
74
+ resolvedIds.push(created.id);
61
75
  } else if (mapping.onNotFound === "error") {
62
76
  throw new Error(`Relation not found: ${relationTo} (${mapping.lookupField}=${val})`);
63
77
  } else {
@@ -85,24 +99,34 @@ async function processSingle(payload, collection, action, data, config) {
85
99
  case "create":
86
100
  return await payload.create({ collection, data: resolved });
87
101
  case "upsert":
88
- const existing = await payload.find({
102
+ if (data[lookupField] === void 0) {
103
+ throw new Error(`[upsert] Missing lookup field '${lookupField}' in data. Cannot perform upsert.`);
104
+ }
105
+ const existingUpsert = await payload.find({
89
106
  collection,
90
107
  where: { [lookupField]: { equals: data[lookupField] } }
91
108
  });
92
- if (existing.docs.length > 0) {
93
- return await payload.update({ collection, id: existing.docs[0].id, data: resolved });
109
+ if (existingUpsert.docs.length > 0) {
110
+ return await payload.update({ collection, id: existingUpsert.docs[0].id, data: resolved });
94
111
  }
95
112
  return await payload.create({ collection, data: resolved });
96
113
  case "update":
114
+ if (data[lookupField] === void 0) {
115
+ throw new Error(`[update] Missing lookup field '${lookupField}' in data. Cannot perform update.`);
116
+ }
97
117
  return await payload.update({
98
118
  collection,
99
119
  where: { [lookupField]: { equals: data[lookupField] } },
100
120
  data: resolved
101
121
  });
102
122
  case "delete":
123
+ const delVal = typeof data === "object" ? data[lookupField] : data;
124
+ if (delVal === void 0) {
125
+ throw new Error(`[delete] Missing lookup field '${lookupField}' in action. Cannot perform delete.`);
126
+ }
103
127
  return await payload.delete({
104
128
  collection,
105
- where: { [lookupField]: { equals: typeof data === "object" ? data[lookupField] : data } }
129
+ where: { [lookupField]: { equals: delVal } }
106
130
  });
107
131
  default:
108
132
  throw new Error(`Unsupported action: ${action}`);
package/dist/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  execute,
3
3
  resolveRelations
4
- } from "./chunk-3HKWP2RE.mjs";
4
+ } from "./chunk-RIXWYITK.mjs";
5
5
  export {
6
6
  execute,
7
7
  resolveRelations
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@payload-cc/payload-collection-cli",
3
- "version": "1.0.9",
3
+ "version": "1.2.0",
4
4
  "description": "Functional CLI for Payload 3.0 collection management",
5
5
  "repository": {
6
6
  "type": "git",
@@ -22,7 +22,8 @@
22
22
  "scripts": {
23
23
  "build": "tsup src/index.ts src/bin.ts --format cjs,esm --dts --clean --shims && chmod +x dist/bin.js",
24
24
  "release": "pnpm build && pnpm publish --access public",
25
- "test": "vitest run"
25
+ "test": "vitest run tests/*",
26
+ "test:e2e": "vitest run e2e-tests/scenarios/ --no-file-parallelism"
26
27
  },
27
28
  "keywords": [
28
29
  "payload",
@@ -40,9 +41,7 @@
40
41
  },
41
42
  "devDependencies": {
42
43
  "@types/node": "^25.5.0",
43
- "next": "^16.2.0",
44
44
  "payload": "^3.79.1",
45
- "react": "^19.2.4",
46
45
  "tsup": "^8.5.1",
47
46
  "typescript": "^5.9.3",
48
47
  "vitest": "^4.1.0"