@payload-cc/payload-collection-cli 1.0.8 → 1.1.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
@@ -84,24 +84,34 @@ async function processSingle(payload, collection, action, data, config) {
84
84
  case "create":
85
85
  return await payload.create({ collection, data: resolved });
86
86
  case "upsert":
87
- const existing = await payload.find({
87
+ if (data[lookupField] === void 0) {
88
+ throw new Error(`[upsert] Missing lookup field '${lookupField}' in data. Cannot perform upsert.`);
89
+ }
90
+ const existingUpsert = await payload.find({
88
91
  collection,
89
92
  where: { [lookupField]: { equals: data[lookupField] } }
90
93
  });
91
- if (existing.docs.length > 0) {
92
- return await payload.update({ collection, id: existing.docs[0].id, data: resolved });
94
+ if (existingUpsert.docs.length > 0) {
95
+ return await payload.update({ collection, id: existingUpsert.docs[0].id, data: resolved });
93
96
  }
94
97
  return await payload.create({ collection, data: resolved });
95
98
  case "update":
99
+ if (data[lookupField] === void 0) {
100
+ throw new Error(`[update] Missing lookup field '${lookupField}' in data. Cannot perform update.`);
101
+ }
96
102
  return await payload.update({
97
103
  collection,
98
104
  where: { [lookupField]: { equals: data[lookupField] } },
99
105
  data: resolved
100
106
  });
101
107
  case "delete":
108
+ const delVal = typeof data === "object" ? data[lookupField] : data;
109
+ if (delVal === void 0) {
110
+ throw new Error(`[delete] Missing lookup field '${lookupField}' in action. Cannot perform delete.`);
111
+ }
102
112
  return await payload.delete({
103
113
  collection,
104
- where: { [lookupField]: { equals: typeof data === "object" ? data[lookupField] : data } }
114
+ where: { [lookupField]: { equals: delVal } }
105
115
  });
106
116
  default:
107
117
  throw new Error(`Unsupported action: ${action}`);
@@ -122,10 +132,38 @@ async function execute(payload, collection, action, input, config) {
122
132
  // src/bin.ts
123
133
  var jiti = (0, import_jiti.createJiti)(importMetaUrl);
124
134
  async function run() {
125
- const [collection, action, input] = process.argv.slice(2);
135
+ const args = process.argv.slice(2);
136
+ let configOptIdx = args.indexOf("-c");
137
+ if (configOptIdx === -1) configOptIdx = args.indexOf("--config");
138
+ let cliConfig = { mappings: {} };
139
+ if (configOptIdx !== -1) {
140
+ if (args.length <= configOptIdx + 1) {
141
+ console.error("\u274C Error: Missing option after --config parameter.");
142
+ process.exit(1);
143
+ }
144
+ const val = args[configOptIdx + 1];
145
+ args.splice(configOptIdx, 2);
146
+ if (val.trim().startsWith("{")) {
147
+ try {
148
+ cliConfig = JSON.parse(val);
149
+ } catch (err) {
150
+ console.error("\u274C Error: Failed to parse inline JSON config:", err);
151
+ process.exit(1);
152
+ }
153
+ } else {
154
+ const customConfigPath = import_path2.default.resolve(process.cwd(), val);
155
+ if (!require("fs").existsSync(customConfigPath)) {
156
+ console.error(`\u274C Error: Config file not found at ${customConfigPath}`);
157
+ process.exit(1);
158
+ }
159
+ const imported = await jiti.import(customConfigPath);
160
+ cliConfig = imported.cliConfig || imported.default || cliConfig;
161
+ }
162
+ }
163
+ const [collection, action, input] = args;
126
164
  const root = process.cwd();
127
165
  if (!collection || !action || !input) {
128
- console.log("Usage: payload-collection-cli <collection> <action> <json|file.jsonl>");
166
+ console.log("Usage: payload-collection-cli [-c path] <collection> <action> <json|file.jsonl>");
129
167
  process.exit(1);
130
168
  }
131
169
  const configPath = [
@@ -134,12 +172,6 @@ async function run() {
134
172
  ].find((p) => require("fs").existsSync(p));
135
173
  if (!configPath) throw new Error("payload.config.ts not found.");
136
174
  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
175
  const payload = await (0, import_payload.getPayload)({ config: payloadConfig });
144
176
  try {
145
177
  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-I7HHN7EL.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);
@@ -55,24 +55,34 @@ async function processSingle(payload, collection, action, data, config) {
55
55
  case "create":
56
56
  return await payload.create({ collection, data: resolved });
57
57
  case "upsert":
58
- const existing = await payload.find({
58
+ if (data[lookupField] === void 0) {
59
+ throw new Error(`[upsert] Missing lookup field '${lookupField}' in data. Cannot perform upsert.`);
60
+ }
61
+ const existingUpsert = await payload.find({
59
62
  collection,
60
63
  where: { [lookupField]: { equals: data[lookupField] } }
61
64
  });
62
- if (existing.docs.length > 0) {
63
- return await payload.update({ collection, id: existing.docs[0].id, data: resolved });
65
+ if (existingUpsert.docs.length > 0) {
66
+ return await payload.update({ collection, id: existingUpsert.docs[0].id, data: resolved });
64
67
  }
65
68
  return await payload.create({ collection, data: resolved });
66
69
  case "update":
70
+ if (data[lookupField] === void 0) {
71
+ throw new Error(`[update] Missing lookup field '${lookupField}' in data. Cannot perform update.`);
72
+ }
67
73
  return await payload.update({
68
74
  collection,
69
75
  where: { [lookupField]: { equals: data[lookupField] } },
70
76
  data: resolved
71
77
  });
72
78
  case "delete":
79
+ const delVal = typeof data === "object" ? data[lookupField] : data;
80
+ if (delVal === void 0) {
81
+ throw new Error(`[delete] Missing lookup field '${lookupField}' in action. Cannot perform delete.`);
82
+ }
73
83
  return await payload.delete({
74
84
  collection,
75
- where: { [lookupField]: { equals: typeof data === "object" ? data[lookupField] : data } }
85
+ where: { [lookupField]: { equals: delVal } }
76
86
  });
77
87
  default:
78
88
  throw new Error(`Unsupported action: ${action}`);
package/dist/index.js CHANGED
@@ -85,24 +85,34 @@ async function processSingle(payload, collection, action, data, config) {
85
85
  case "create":
86
86
  return await payload.create({ collection, data: resolved });
87
87
  case "upsert":
88
- const existing = await payload.find({
88
+ if (data[lookupField] === void 0) {
89
+ throw new Error(`[upsert] Missing lookup field '${lookupField}' in data. Cannot perform upsert.`);
90
+ }
91
+ const existingUpsert = await payload.find({
89
92
  collection,
90
93
  where: { [lookupField]: { equals: data[lookupField] } }
91
94
  });
92
- if (existing.docs.length > 0) {
93
- return await payload.update({ collection, id: existing.docs[0].id, data: resolved });
95
+ if (existingUpsert.docs.length > 0) {
96
+ return await payload.update({ collection, id: existingUpsert.docs[0].id, data: resolved });
94
97
  }
95
98
  return await payload.create({ collection, data: resolved });
96
99
  case "update":
100
+ if (data[lookupField] === void 0) {
101
+ throw new Error(`[update] Missing lookup field '${lookupField}' in data. Cannot perform update.`);
102
+ }
97
103
  return await payload.update({
98
104
  collection,
99
105
  where: { [lookupField]: { equals: data[lookupField] } },
100
106
  data: resolved
101
107
  });
102
108
  case "delete":
109
+ const delVal = typeof data === "object" ? data[lookupField] : data;
110
+ if (delVal === void 0) {
111
+ throw new Error(`[delete] Missing lookup field '${lookupField}' in action. Cannot perform delete.`);
112
+ }
103
113
  return await payload.delete({
104
114
  collection,
105
- where: { [lookupField]: { equals: typeof data === "object" ? data[lookupField] : data } }
115
+ where: { [lookupField]: { equals: delVal } }
106
116
  });
107
117
  default:
108
118
  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-I7HHN7EL.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.8",
3
+ "version": "1.1.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"