convex 1.41.0 → 1.42.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.
Files changed (167) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/dist/browser.bundle.js +1 -1
  3. package/dist/browser.bundle.js.map +1 -1
  4. package/dist/cjs/browser/simple_client-node.js +85 -27
  5. package/dist/cjs/browser/simple_client-node.js.map +3 -3
  6. package/dist/cjs/cli/env.js +12 -5
  7. package/dist/cjs/cli/env.js.map +2 -2
  8. package/dist/cjs/cli/envDefault.js +12 -7
  9. package/dist/cjs/cli/envDefault.js.map +2 -2
  10. package/dist/cjs/cli/insights.js +11 -1
  11. package/dist/cjs/cli/insights.js.map +2 -2
  12. package/dist/cjs/cli/lib/env.js +5 -1
  13. package/dist/cjs/cli/lib/env.js.map +2 -2
  14. package/dist/cjs/cli/lib/login.js +16 -12
  15. package/dist/cjs/cli/lib/login.js.map +2 -2
  16. package/dist/cjs/cli/lib/logs.js +4 -1
  17. package/dist/cjs/cli/lib/logs.js.map +2 -2
  18. package/dist/cjs/cli/lib/run.js +27 -2
  19. package/dist/cjs/cli/lib/run.js.map +2 -2
  20. package/dist/cjs/cli/lib/utils/utils.js +30 -0
  21. package/dist/cjs/cli/lib/utils/utils.js.map +2 -2
  22. package/dist/cjs/cli/program.js +2 -1
  23. package/dist/cjs/cli/program.js.map +2 -2
  24. package/dist/cjs/cli/project.js +27 -0
  25. package/dist/cjs/cli/project.js.map +7 -0
  26. package/dist/cjs/cli/projectCreate.js +90 -0
  27. package/dist/cjs/cli/projectCreate.js.map +7 -0
  28. package/dist/cjs/index.js +1 -1
  29. package/dist/cjs/index.js.map +1 -1
  30. package/dist/cjs/server/database.js.map +1 -1
  31. package/dist/cjs/server/impl/meta_impl.js +6 -6
  32. package/dist/cjs/server/impl/meta_impl.js.map +2 -2
  33. package/dist/cjs/server/impl/registration_impl.js +9 -2
  34. package/dist/cjs/server/impl/registration_impl.js.map +2 -2
  35. package/dist/cjs/server/index.js.map +2 -2
  36. package/dist/cjs/server/meta.js.map +1 -1
  37. package/dist/cjs/server/query.js.map +1 -1
  38. package/dist/cjs/server/registration.js.map +1 -1
  39. package/dist/cjs/server/storage.js.map +1 -1
  40. package/dist/cjs/server/system_fields.js.map +1 -1
  41. package/dist/cjs-types/cli/env.d.ts.map +1 -1
  42. package/dist/cjs-types/cli/envDefault.d.ts.map +1 -1
  43. package/dist/cjs-types/cli/insights.d.ts +1 -0
  44. package/dist/cjs-types/cli/insights.d.ts.map +1 -1
  45. package/dist/cjs-types/cli/lib/env.d.ts +3 -1
  46. package/dist/cjs-types/cli/lib/env.d.ts.map +1 -1
  47. package/dist/cjs-types/cli/lib/env.test.d.ts +2 -0
  48. package/dist/cjs-types/cli/lib/env.test.d.ts.map +1 -0
  49. package/dist/cjs-types/cli/lib/login.d.ts.map +1 -1
  50. package/dist/cjs-types/cli/lib/logs.d.ts +1 -1
  51. package/dist/cjs-types/cli/lib/logs.d.ts.map +1 -1
  52. package/dist/cjs-types/cli/lib/run.d.ts +1 -1
  53. package/dist/cjs-types/cli/lib/run.d.ts.map +1 -1
  54. package/dist/cjs-types/cli/lib/utils/utils.d.ts +10 -0
  55. package/dist/cjs-types/cli/lib/utils/utils.d.ts.map +1 -1
  56. package/dist/cjs-types/cli/program.d.ts.map +1 -1
  57. package/dist/cjs-types/cli/project.d.ts +3 -0
  58. package/dist/cjs-types/cli/project.d.ts.map +1 -0
  59. package/dist/cjs-types/cli/projectCreate.d.ts +5 -0
  60. package/dist/cjs-types/cli/projectCreate.d.ts.map +1 -0
  61. package/dist/cjs-types/index.d.ts +1 -1
  62. package/dist/cjs-types/server/database.d.ts +21 -3
  63. package/dist/cjs-types/server/database.d.ts.map +1 -1
  64. package/dist/cjs-types/server/impl/meta_impl.d.ts.map +1 -1
  65. package/dist/cjs-types/server/impl/registration_impl.d.ts.map +1 -1
  66. package/dist/cjs-types/server/index.d.ts +1 -1
  67. package/dist/cjs-types/server/index.d.ts.map +1 -1
  68. package/dist/cjs-types/server/meta.d.ts +10 -0
  69. package/dist/cjs-types/server/meta.d.ts.map +1 -1
  70. package/dist/cjs-types/server/query.d.ts +1 -1
  71. package/dist/cjs-types/server/registration.d.ts +15 -3
  72. package/dist/cjs-types/server/registration.d.ts.map +1 -1
  73. package/dist/cjs-types/server/storage.d.ts +20 -3
  74. package/dist/cjs-types/server/storage.d.ts.map +1 -1
  75. package/dist/cli.bundle.cjs +1047 -619
  76. package/dist/cli.bundle.cjs.map +4 -4
  77. package/dist/esm/browser/simple_client-node.js +85 -27
  78. package/dist/esm/browser/simple_client-node.js.map +3 -3
  79. package/dist/esm/cli/env.js +12 -5
  80. package/dist/esm/cli/env.js.map +2 -2
  81. package/dist/esm/cli/envDefault.js +12 -7
  82. package/dist/esm/cli/envDefault.js.map +2 -2
  83. package/dist/esm/cli/insights.js +11 -1
  84. package/dist/esm/cli/insights.js.map +2 -2
  85. package/dist/esm/cli/lib/env.js +5 -1
  86. package/dist/esm/cli/lib/env.js.map +2 -2
  87. package/dist/esm/cli/lib/login.js +18 -13
  88. package/dist/esm/cli/lib/login.js.map +2 -2
  89. package/dist/esm/cli/lib/logs.js +5 -2
  90. package/dist/esm/cli/lib/logs.js.map +2 -2
  91. package/dist/esm/cli/lib/run.js +32 -3
  92. package/dist/esm/cli/lib/run.js.map +2 -2
  93. package/dist/esm/cli/lib/utils/utils.js +29 -0
  94. package/dist/esm/cli/lib/utils/utils.js.map +2 -2
  95. package/dist/esm/cli/program.js +2 -1
  96. package/dist/esm/cli/program.js.map +2 -2
  97. package/dist/esm/cli/project.js +5 -0
  98. package/dist/esm/cli/project.js.map +7 -0
  99. package/dist/esm/cli/projectCreate.js +68 -0
  100. package/dist/esm/cli/projectCreate.js.map +7 -0
  101. package/dist/esm/index.js +1 -1
  102. package/dist/esm/index.js.map +1 -1
  103. package/dist/esm/server/impl/meta_impl.js +6 -6
  104. package/dist/esm/server/impl/meta_impl.js.map +2 -2
  105. package/dist/esm/server/impl/registration_impl.js +9 -2
  106. package/dist/esm/server/impl/registration_impl.js.map +2 -2
  107. package/dist/esm/server/index.js.map +2 -2
  108. package/dist/esm-types/cli/env.d.ts.map +1 -1
  109. package/dist/esm-types/cli/envDefault.d.ts.map +1 -1
  110. package/dist/esm-types/cli/insights.d.ts +1 -0
  111. package/dist/esm-types/cli/insights.d.ts.map +1 -1
  112. package/dist/esm-types/cli/lib/env.d.ts +3 -1
  113. package/dist/esm-types/cli/lib/env.d.ts.map +1 -1
  114. package/dist/esm-types/cli/lib/env.test.d.ts +2 -0
  115. package/dist/esm-types/cli/lib/env.test.d.ts.map +1 -0
  116. package/dist/esm-types/cli/lib/login.d.ts.map +1 -1
  117. package/dist/esm-types/cli/lib/logs.d.ts +1 -1
  118. package/dist/esm-types/cli/lib/logs.d.ts.map +1 -1
  119. package/dist/esm-types/cli/lib/run.d.ts +1 -1
  120. package/dist/esm-types/cli/lib/run.d.ts.map +1 -1
  121. package/dist/esm-types/cli/lib/utils/utils.d.ts +10 -0
  122. package/dist/esm-types/cli/lib/utils/utils.d.ts.map +1 -1
  123. package/dist/esm-types/cli/program.d.ts.map +1 -1
  124. package/dist/esm-types/cli/project.d.ts +3 -0
  125. package/dist/esm-types/cli/project.d.ts.map +1 -0
  126. package/dist/esm-types/cli/projectCreate.d.ts +5 -0
  127. package/dist/esm-types/cli/projectCreate.d.ts.map +1 -0
  128. package/dist/esm-types/index.d.ts +1 -1
  129. package/dist/esm-types/server/database.d.ts +21 -3
  130. package/dist/esm-types/server/database.d.ts.map +1 -1
  131. package/dist/esm-types/server/impl/meta_impl.d.ts.map +1 -1
  132. package/dist/esm-types/server/impl/registration_impl.d.ts.map +1 -1
  133. package/dist/esm-types/server/index.d.ts +1 -1
  134. package/dist/esm-types/server/index.d.ts.map +1 -1
  135. package/dist/esm-types/server/meta.d.ts +10 -0
  136. package/dist/esm-types/server/meta.d.ts.map +1 -1
  137. package/dist/esm-types/server/query.d.ts +1 -1
  138. package/dist/esm-types/server/registration.d.ts +15 -3
  139. package/dist/esm-types/server/registration.d.ts.map +1 -1
  140. package/dist/esm-types/server/storage.d.ts +20 -3
  141. package/dist/esm-types/server/storage.d.ts.map +1 -1
  142. package/dist/react.bundle.js +1 -1
  143. package/dist/react.bundle.js.map +1 -1
  144. package/package.json +4 -4
  145. package/src/cli/env.ts +16 -6
  146. package/src/cli/envDefault.ts +13 -7
  147. package/src/cli/insights.ts +12 -0
  148. package/src/cli/lib/env.test.ts +60 -0
  149. package/src/cli/lib/env.ts +9 -1
  150. package/src/cli/lib/login.ts +18 -18
  151. package/src/cli/lib/logs.ts +7 -2
  152. package/src/cli/lib/run.ts +37 -3
  153. package/src/cli/lib/utils/utils.ts +56 -0
  154. package/src/cli/program.ts +2 -0
  155. package/src/cli/project.ts +7 -0
  156. package/src/cli/projectCreate.ts +99 -0
  157. package/src/index.ts +1 -1
  158. package/src/server/database.ts +21 -3
  159. package/src/server/impl/meta_impl.ts +7 -6
  160. package/src/server/impl/registration_impl.ts +11 -3
  161. package/src/server/index.ts +1 -0
  162. package/src/server/meta.ts +10 -0
  163. package/src/server/query.ts +1 -1
  164. package/src/server/registration.test.ts +25 -1
  165. package/src/server/registration.ts +16 -1
  166. package/src/server/storage.ts +22 -3
  167. package/src/server/system_fields.ts +1 -1
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "convex",
3
3
  "description": "Client for the Convex Cloud",
4
- "version": "1.41.0",
4
+ "version": "1.42.0",
5
5
  "author": "Convex, Inc. <no-reply@convex.dev>",
6
6
  "homepage": "https://convex.dev",
7
7
  "repository": {
@@ -171,7 +171,7 @@
171
171
  "dependencies": {
172
172
  "esbuild": "0.27.0",
173
173
  "prettier": "^3.0.0",
174
- "ws": "8.20.1"
174
+ "ws": "8.21.0"
175
175
  },
176
176
  "peerDependencies": {
177
177
  "@auth0/auth0-react": "^2.0.1",
@@ -266,13 +266,13 @@
266
266
  "serve-handler": "~6.1.6",
267
267
  "shx": "~0.4.0",
268
268
  "skott": "~0.35.4",
269
- "tar": "^7.5.11",
269
+ "tar": "^7.5.16",
270
270
  "tsx": "^4.19.4",
271
271
  "typedoc": "^0.24.6",
272
272
  "typescript": "~5.0.3",
273
273
  "typescript-eslint": "~8.57.0",
274
274
  "vitest": "^4.1.0",
275
- "undici": "7.24.0",
275
+ "undici": "7.28.0",
276
276
  "wait-for-expect": "~4.0.0",
277
277
  "zod": "^3.24.0",
278
278
  "zod-to-json-schema": "^3.24.0"
package/src/cli/env.ts CHANGED
@@ -164,18 +164,25 @@ const envRemoveCmd = new Command("remove")
164
164
  });
165
165
 
166
166
  const envListCmd = new Command("list")
167
- .summary("List all variables and their values")
167
+ .summary("List all environment variables and their values")
168
168
  .description(
169
169
  [
170
- "• List all variables: `npx convex env list`",
170
+ "• List all variables and their values: `npx convex env list`",
171
+ "• List only variable names (no values): `npx convex env list --names-only`",
171
172
  "• Save all variables to a file: `npx convex env list > .env.convex`",
172
173
  "• Append to a file: `npx convex env list >> .env.convex`",
173
174
  ].join("\n"),
174
175
  )
176
+ .option(
177
+ "--names-only",
178
+ "List only the names of environment variables, without their values",
179
+ )
175
180
  .configureHelp({ showGlobalOptions: true })
176
181
  .allowExcessArguments(false)
177
- .action(async (_options, cmd) => {
178
- const options = cmd.optsWithGlobals();
182
+ .action(async (cmdOptions, cmd) => {
183
+ // Note: We use `as` here because optsWithGlobals() type inference doesn't
184
+ // include global options from the parent command (added via addDeploymentSelectionOptions)
185
+ const options = cmd.optsWithGlobals() as DeploymentSelectionOptions;
179
186
  const { ctx, deployment } = await selectEnvDeployment(options);
180
187
  await ensureHasConvexDependency(ctx, "env list");
181
188
  await withRunningBackend({
@@ -183,7 +190,9 @@ const envListCmd = new Command("list")
183
190
  deployment,
184
191
  action: async () => {
185
192
  const backend = deploymentEnvBackend(ctx, deployment);
186
- await envList(ctx, backend);
193
+ await envList(ctx, backend, {
194
+ namesOnly: cmdOptions.namesOnly ?? false,
195
+ });
187
196
  },
188
197
  });
189
198
  });
@@ -198,7 +207,8 @@ export const env = new Command("env")
198
207
  "• Set interactively: `npx convex env set NAME`",
199
208
  "• Set multiple from file: `npx convex env set --from-file .env`",
200
209
  "• Unset a variable: `npx convex env remove NAME`",
201
- "• List all variables: `npx convex env list`",
210
+ "• List all variables and their values: `npx convex env list`",
211
+ "• List only variable names (no values): `npx convex env list --names-only`",
202
212
  "• Print a variable's value: `npx convex env get NAME`",
203
213
  "",
204
214
  "By default, this sets and views variables on your dev deployment.",
@@ -141,24 +141,29 @@ const envDefaultRemove = addEnvDefaultOptions(
141
141
 
142
142
  const envDefaultList = addEnvDefaultOptions(
143
143
  new Command("list")
144
- .summary("List all default variables")
144
+ .summary("List all default environment variables and their values")
145
145
  .description(
146
146
  [
147
- "List all default variables.",
148
- "",
149
- "• `npx convex env default list`",
147
+ "List all default variables and their values: `npx convex env default list`",
148
+ "• List only default variable names (no values): `npx convex env default list --names-only`",
150
149
  "",
151
150
  "The deployment type is determined by the current deployment (local maps to dev), or by --type if provided.",
152
151
  ].join("\n"),
153
152
  )
153
+ .option(
154
+ "--names-only",
155
+ "List only the names of environment variables, without their values",
156
+ )
154
157
  .configureHelp({ showGlobalOptions: true })
155
158
  .allowExcessArguments(false),
156
- ).action(async (_options, cmd) => {
159
+ ).action(async (cmdOptions, cmd) => {
157
160
  const options = cmd.optsWithGlobals() as DeploymentSelectionOptions &
158
161
  EnvDefaultExtraOptions;
159
162
  const { ctx, backend } = await resolveEnvDefaultBackend(options);
160
163
  await ensureHasConvexDependency(ctx, "env default list");
161
- await envList(ctx, backend);
164
+ await envList(ctx, backend, {
165
+ namesOnly: cmdOptions.namesOnly ?? false,
166
+ });
162
167
  });
163
168
 
164
169
  export const envDefault = new Command("default")
@@ -171,7 +176,8 @@ export const envDefault = new Command("default")
171
176
  "",
172
177
  "• Set a default variable: `npx convex env default set NAME 'value'`",
173
178
  "• Unset a default variable: `npx convex env default remove NAME`",
174
- "• List all default variables: `npx convex env default list`",
179
+ "• List all default variables and their values: `npx convex env default list`",
180
+ "• List only default variable names (no values): `npx convex env default list --names-only`",
175
181
  "• Print a default variable's value: `npx convex env default get NAME`",
176
182
  ].join("\n"),
177
183
  )
@@ -101,6 +101,7 @@ export const insights = new Command("insights")
101
101
  )
102
102
  .allowExcessArguments(false)
103
103
  .option("--details", "Show recent events for each insight", false)
104
+ .option("--json", "Output insights as JSON", false)
104
105
  .addDeploymentSelectionOptions(actionDescription("Show insights for"))
105
106
  .showHelpAfterError()
106
107
  .action(async (cmdOptions) => {
@@ -145,6 +146,17 @@ export const insights = new Command("insights")
145
146
  "?view=insights",
146
147
  );
147
148
 
149
+ if (cmdOptions.json) {
150
+ logOutput(
151
+ JSON.stringify(
152
+ { deploymentName, dashboardUrl, insights: insightsList },
153
+ null,
154
+ 2,
155
+ ),
156
+ );
157
+ return;
158
+ }
159
+
148
160
  if (insightsList.length === 0) {
149
161
  logOutput(
150
162
  chalk.green(
@@ -0,0 +1,60 @@
1
+ import { beforeEach, describe, expect, test, vi } from "vitest";
2
+ import type { Context } from "../../bundler/context.js";
3
+ import { envList, type EnvVar, type EnvVarBackend } from "./env.js";
4
+
5
+ const logMocks = vi.hoisted(() => ({
6
+ logFailure: vi.fn(),
7
+ logFinishedStep: vi.fn(),
8
+ logMessage: vi.fn(),
9
+ logOutput: vi.fn(),
10
+ }));
11
+
12
+ vi.mock("../../bundler/log.js", () => ({
13
+ logFailure: logMocks.logFailure,
14
+ logFinishedStep: logMocks.logFinishedStep,
15
+ logMessage: logMocks.logMessage,
16
+ logOutput: logMocks.logOutput,
17
+ }));
18
+
19
+ function fakeBackend(envs: EnvVar[]): EnvVarBackend {
20
+ return {
21
+ get() {
22
+ return Promise.resolve(null);
23
+ },
24
+ list() {
25
+ return Promise.resolve(envs);
26
+ },
27
+ update() {
28
+ return Promise.reject(new Error("Unexpected update"));
29
+ },
30
+ notice: "",
31
+ };
32
+ }
33
+
34
+ describe("envList", () => {
35
+ beforeEach(() => {
36
+ vi.clearAllMocks();
37
+ });
38
+
39
+ test("prints only names without values or warnings when namesOnly is true", async () => {
40
+ await envList(
41
+ {} as Context,
42
+ fakeBackend([
43
+ { name: "PLAIN", value: "plain-value" },
44
+ { name: "DANGEROUS", value: "secret'value#fragment" },
45
+ ]),
46
+ { namesOnly: true },
47
+ );
48
+
49
+ expect(logMocks.logOutput.mock.calls).toEqual([["PLAIN"], ["DANGEROUS"]]);
50
+ expect(logMocks.logMessage).not.toHaveBeenCalled();
51
+
52
+ const emitted = [
53
+ ...logMocks.logOutput.mock.calls.flat(),
54
+ ...logMocks.logMessage.mock.calls.flat(),
55
+ ].join("\n");
56
+ expect(emitted).not.toContain("plain-value");
57
+ expect(emitted).not.toContain("secret'value#fragment");
58
+ expect(emitted).not.toContain("Warning (");
59
+ });
60
+ });
@@ -171,13 +171,21 @@ export async function envRemove(
171
171
  );
172
172
  }
173
173
 
174
- export async function envList(ctx: Context, backend: EnvVarBackend) {
174
+ export async function envList(
175
+ ctx: Context,
176
+ backend: EnvVarBackend,
177
+ options?: { namesOnly?: boolean },
178
+ ) {
175
179
  const envs = await backend.list();
176
180
  if (envs.length === 0) {
177
181
  logMessage(`No environment variables set${backend.notice}`);
178
182
  return;
179
183
  }
180
184
  for (const { name, value } of envs) {
185
+ if (options?.namesOnly) {
186
+ logOutput(name);
187
+ continue;
188
+ }
181
189
  const { formatted, warning } = formatEnvValueForDotfile(value);
182
190
  if (warning) {
183
191
  logMessage(`Warning (${name}): ${warning}`);
@@ -5,6 +5,7 @@ import {
5
5
  logAndHandleFetchError,
6
6
  throwingFetch,
7
7
  isWebContainer,
8
+ typedPlatformClient,
8
9
  } from "./utils/utils.js";
9
10
  import open from "open";
10
11
  import { chalkStderr } from "chalk";
@@ -37,12 +38,6 @@ custom.setHttpOptionsDefaults({
37
38
  timeout: parseInt(process.env.OPENID_CLIENT_TIMEOUT || "10000"),
38
39
  });
39
40
 
40
- interface AuthorizeArgs {
41
- authnToken: string;
42
- deviceName: string;
43
- anonymousId?: string | undefined;
44
- }
45
-
46
41
  export async function checkAuthorization(
47
42
  ctx: Context,
48
43
  acceptOptIns: boolean,
@@ -390,18 +385,23 @@ export async function performLogin(
390
385
  });
391
386
  }
392
387
 
393
- const authorizeArgs: AuthorizeArgs = {
394
- authnToken: accessToken!,
395
- deviceName: deviceName,
396
- anonymousId: anonymousId,
397
- };
398
- const data = await bigBrainAPI({
399
- ctx,
400
- method: "POST",
401
- path: "authorize",
402
- data: authorizeArgs,
388
+ // Exchange the WorkOS access token for a Convex personal access token.
389
+ ctx._updateBigBrainAuth({
390
+ accessToken: accessToken!,
391
+ kind: "accessToken",
392
+ header: `Bearer ${accessToken!}`,
403
393
  });
404
- const globalConfig = { accessToken: data.accessToken };
394
+ const response = await typedPlatformClient(ctx).POST(
395
+ "/create_personal_access_token",
396
+ {
397
+ // `anonymousId` links a prior anonymous session to this account. It's
398
+ // intentionally absent from the generated platform API types,
399
+ // so cast to pass it through.
400
+ body: { name: deviceName, anonymousId } as { name: string },
401
+ },
402
+ );
403
+ const newAccessToken = response.data!.accessToken;
404
+ const globalConfig = { accessToken: newAccessToken };
405
405
  try {
406
406
  await modifyGlobalConfig(ctx, globalConfig);
407
407
  const path = globalConfigPath();
@@ -416,7 +416,7 @@ export async function performLogin(
416
416
  }
417
417
 
418
418
  logVerbose(`performLogin: updating big brain auth after login`);
419
- await updateBigBrainAuthAfterLogin(ctx, data.accessToken);
419
+ await updateBigBrainAuthAfterLogin(ctx, newAccessToken);
420
420
 
421
421
  logVerbose(`performLogin: checking opt ins, acceptOptIns: ${acceptOptIns}`);
422
422
  // Do opt in to TOS and Privacy Policy stuff
@@ -5,7 +5,7 @@ import { nextBackoff } from "./dev.js";
5
5
  import chalk, { chalkStderr } from "chalk";
6
6
  import { stripVTControlCharacters } from "node:util";
7
7
  import { format } from "node:util";
8
- import { deploymentFetch } from "./utils/utils.js";
8
+ import { deploymentFetch, ThrowingFetchError } from "./utils/utils.js";
9
9
  import { FunctionExecution } from "./apiTypes.js";
10
10
 
11
11
  export type LogMode = "always" | "pause-on-deploy" | "disable";
@@ -122,7 +122,12 @@ export async function watchLogs(
122
122
  options?.jsonl,
123
123
  );
124
124
  }
125
- } catch {
125
+ } catch (e) {
126
+ // A 403 means the admin key isn't authorized for this deployment.
127
+ // Retrying won't help, so surface the error and fail immediately.
128
+ if (e instanceof ThrowingFetchError && e.response.status === 403) {
129
+ return await e.handle(ctx);
130
+ }
126
131
  numFailures += 1;
127
132
  }
128
133
  // Handle backoff
@@ -11,7 +11,11 @@ import {
11
11
  import { Value, convexToJson, jsonToConvex } from "../../values/value.js";
12
12
  import { Context, OneoffCtx } from "../../bundler/context.js";
13
13
  import { logFinishedStep, logMessage, logOutput } from "../../bundler/log.js";
14
- import { waitForever, waitUntilCalled } from "./utils/utils.js";
14
+ import {
15
+ permissionDeniedErrorMessage,
16
+ waitForever,
17
+ waitUntilCalled,
18
+ } from "./utils/utils.js";
15
19
  import JSON5 from "json5";
16
20
  import path from "path";
17
21
  import { readProjectConfig } from "./config.js";
@@ -59,6 +63,15 @@ export async function runFunctionAndLog(
59
63
  } catch (err) {
60
64
  const errorMessage = (err as Error).toString().trim();
61
65
 
66
+ const permissionError = permissionDeniedErrorMessage(errorMessage);
67
+ if (permissionError !== null) {
68
+ return await ctx.crash({
69
+ exitCode: 1,
70
+ errorType: "fatal",
71
+ printedMessage: chalkStderr.red(permissionError),
72
+ });
73
+ }
74
+
62
75
  if (errorMessage.includes("Could not find function")) {
63
76
  const functions = (await runSystemQuery(ctx, {
64
77
  deploymentUrl: args.deploymentUrl,
@@ -377,7 +390,7 @@ export async function subscribeAndLog(
377
390
  }
378
391
 
379
392
  export async function subscribe(
380
- _ctx: Context,
393
+ ctx: Context,
381
394
  args: {
382
395
  deploymentUrl: string;
383
396
  adminKey: string;
@@ -399,7 +412,28 @@ export async function subscribe(
399
412
  args.deploymentUrl,
400
413
  (updatedQueries) => {
401
414
  for (const queryToken of updatedQueries) {
402
- args.callbacks?.onChange?.(client.localQueryResultByToken(queryToken)!);
415
+ let result: Value;
416
+ try {
417
+ result = client.localQueryResultByToken(queryToken)!;
418
+ } catch (err) {
419
+ // A failed query (e.g. a permission error) throws here. Surface
420
+ // permission errors cleanly instead of an uncaught stack trace, and
421
+ // let any other error propagate as before.
422
+ const permissionError = permissionDeniedErrorMessage(
423
+ (err as Error).toString(),
424
+ );
425
+ if (permissionError !== null) {
426
+ void ctx.crash({
427
+ exitCode: 1,
428
+ errorType: "fatal",
429
+ printedMessage: chalkStderr.red(permissionError),
430
+ });
431
+ return;
432
+ }
433
+ // eslint-disable-next-line no-restricted-syntax
434
+ throw err;
435
+ }
436
+ args.callbacks?.onChange?.(result);
403
437
  }
404
438
  },
405
439
  {
@@ -101,6 +101,52 @@ export function readDeployKeyFromEnv(
101
101
  return fromDeployKey || fromToken || undefined;
102
102
  }
103
103
 
104
+ // The prefix the backend's `require_operation` checks use when an identity
105
+ // lacks a permission. The full message names the missing permission, e.g.
106
+ // `You do not have permission to perform this operation (deployment:data:view).`
107
+ const PERMISSION_DENIED_MESSAGE_PREFIX =
108
+ "You do not have permission to perform this operation";
109
+
110
+ function deployKeyPermissionAdvice(): string | null {
111
+ // Name whichever env var is actually set. `CONVEX_DEPLOY_KEY` takes
112
+ // precedence over `CONVEX_DEPLOYMENT_TOKEN` when both are present.
113
+ const envVarName = process.env[CONVEX_DEPLOY_KEY_ENV_VAR_NAME]
114
+ ? CONVEX_DEPLOY_KEY_ENV_VAR_NAME
115
+ : process.env[CONVEX_DEPLOYMENT_TOKEN_ENV_VAR_NAME]
116
+ ? CONVEX_DEPLOYMENT_TOKEN_ENV_VAR_NAME
117
+ : null;
118
+ if (envVarName === null) {
119
+ return null;
120
+ }
121
+ return `This is determined by the permissions granted to ${envVarName}.`;
122
+ }
123
+
124
+ /**
125
+ * Detects a server-side authorization failure (a `require_operation` error,
126
+ * surfaced both as an HTTP 403 and as an error from a system UDF query) and
127
+ * produces a clean, single-line message naming the missing permission. When a
128
+ * deploy key is in use, appends advice for configuring its permissions.
129
+ *
130
+ * Returns `null` if `errorMessage` is not a permission error, so callers can
131
+ * fall back to their normal error handling.
132
+ */
133
+ export function permissionDeniedErrorMessage(
134
+ errorMessage: string,
135
+ ): string | null {
136
+ const index = errorMessage.indexOf(PERMISSION_DENIED_MESSAGE_PREFIX);
137
+ if (index === -1) {
138
+ return null;
139
+ }
140
+ // Drop any wrapping context (e.g. the `[CONVEX Q(...)]` prefix) and stack
141
+ // trace, keeping just the permission sentence.
142
+ let msg = errorMessage.slice(index).split("\n")[0].trim();
143
+ const advice = deployKeyPermissionAdvice();
144
+ if (advice) {
145
+ msg = `${msg}\n${advice}`;
146
+ }
147
+ return msg;
148
+ }
149
+
104
150
  export function parsePositiveInteger(value: string) {
105
151
  const parsedValue = parseInteger(value);
106
152
  if (parsedValue <= 0) {
@@ -177,6 +223,16 @@ export class ThrowingFetchError extends Error {
177
223
  } else if (this.response.status === 401) {
178
224
  error_type = "fatal";
179
225
  msg = `${msg}\nAuthenticate with \`npx convex dev\``;
226
+ } else if (this.response.status === 403) {
227
+ error_type = "fatal";
228
+ // Surface the missing permission cleanly (e.g. the server includes the
229
+ // required action like `deployment:data:view`) instead of the noisy
230
+ // "Error fetching ..." wrapper.
231
+ msg = this.serverErrorData?.message ?? msg;
232
+ const advice = deployKeyPermissionAdvice();
233
+ if (advice) {
234
+ msg = `${msg}\n${advice}`;
235
+ }
180
236
  } else if (this.response.status === 404) {
181
237
  error_type = "fatal";
182
238
  msg = `${msg}: ${this.response.url}`;
@@ -21,6 +21,7 @@ import { logout } from "./logout.js";
21
21
  import { logs } from "./logs.js";
22
22
  import { mcp } from "./mcp.js";
23
23
  import { networkTest } from "./network_test.js";
24
+ import { project } from "./project.js";
24
25
  import { reinit } from "./reinit.js";
25
26
  import { run } from "./run.js";
26
27
  import { typecheck } from "./typecheck.js";
@@ -51,6 +52,7 @@ export function buildProgram() {
51
52
  .addCommand(env)
52
53
  .addCommand(data)
53
54
  .addCommand(deployment)
55
+ .addCommand(project)
54
56
  .addCommand(codegen)
55
57
  .addCommand(update)
56
58
  .addCommand(logout)
@@ -0,0 +1,7 @@
1
+ import { Command } from "@commander-js/extra-typings";
2
+ import { projectCreate } from "./projectCreate.js";
3
+
4
+ export const project = new Command("project")
5
+ .summary("Manage projects")
6
+ .description("Manage projects in your team.")
7
+ .addCommand(projectCreate);
@@ -0,0 +1,99 @@
1
+ import { Command, Option } from "@commander-js/extra-typings";
2
+ import { oneoffContext } from "../bundler/context.js";
3
+ import { logFinishedStep, logMessage, showSpinner } from "../bundler/log.js";
4
+ import { chalkStderr } from "chalk";
5
+ import { createProject } from "./lib/api.js";
6
+ import { ensureAuthCanCreateDeployment } from "./lib/deploymentSelection.js";
7
+ import { validateOrSelectTeam } from "./lib/utils/utils.js";
8
+ import { promptString } from "./lib/utils/prompts.js";
9
+ import { projectDashboardUrl } from "./lib/dashboard.js";
10
+
11
+ type ProjectCreateOptions = {
12
+ team?: string | undefined;
13
+ };
14
+
15
+ async function runProjectCreate(
16
+ nameArg: string | undefined,
17
+ options: ProjectCreateOptions,
18
+ ): Promise<void> {
19
+ const ctx = await oneoffContext({
20
+ url: undefined,
21
+ adminKey: undefined,
22
+ envFile: undefined,
23
+ });
24
+
25
+ // Creating a project goes through the platform API, which accepts personal
26
+ // access tokens and project keys but not deployment/preview deploy keys.
27
+ // Fail fast (and clearly) for the unsupported keys rather than surfacing an
28
+ // opaque error later.
29
+ await ensureAuthCanCreateDeployment(ctx);
30
+
31
+ const { team } = await validateOrSelectTeam(ctx, options.team, "Team:");
32
+
33
+ let projectName = nameArg;
34
+ if (!projectName) {
35
+ if (!process.stdin.isTTY) {
36
+ return await ctx.crash({
37
+ exitCode: 1,
38
+ errorType: "fatal",
39
+ printedMessage:
40
+ "Specify a project name:\n" + " `npx convex project create my-app`",
41
+ });
42
+ }
43
+ projectName = await promptString(ctx, {
44
+ message: "Project name:",
45
+ });
46
+ }
47
+
48
+ showSpinner(`Creating project ${projectName}...`);
49
+ // `project create` only creates the project. Provisioning a deployment is a
50
+ // separate, explicit step (`npx convex deployment create`) so this command
51
+ // doesn't have to mirror every deployment setting (region, class, ...).
52
+ const { projectSlug } = await createProject(ctx, {
53
+ teamId: team.id,
54
+ projectName,
55
+ deploymentToProvision: null,
56
+ });
57
+
58
+ logFinishedStep(
59
+ `Created project ${chalkStderr.bold(projectSlug)} in team ` +
60
+ `${chalkStderr.bold(team.slug)}, manage it at ${chalkStderr.bold(
61
+ projectDashboardUrl(team.slug, projectSlug),
62
+ )}`,
63
+ );
64
+ logMessage(
65
+ chalkStderr.gray(
66
+ `Next, add a deployment with ` +
67
+ `\`npx convex deployment create ${team.slug}:${projectSlug}:dev --type dev\` ` +
68
+ `(pass \`--region us\` to choose a region).`,
69
+ ),
70
+ );
71
+ }
72
+
73
+ export const projectCreate = new Command("create")
74
+ .summary("Create a new project")
75
+ .description(
76
+ [
77
+ "Create a new project.",
78
+ "",
79
+ "Provisioning a deployment is a separate step — after creating the",
80
+ "project, run `npx convex deployment create` to add one.",
81
+ "",
82
+ "• Create a project in your only team: `npx convex project create my-app`",
83
+ "• Pick the team: `npx convex project create my-app --team my-team`",
84
+ ].join("\n"),
85
+ )
86
+ .allowExcessArguments(false)
87
+ .argument(
88
+ "[name]",
89
+ "The name of the new project. Prompted for when omitted in an " +
90
+ "interactive terminal; required otherwise.",
91
+ )
92
+ .addOption(
93
+ new Option(
94
+ "--team <team_slug>",
95
+ "The team to create the project in. Defaults to your only team, or " +
96
+ "prompts when you belong to several.",
97
+ ),
98
+ )
99
+ .action(runProjectCreate);
package/src/index.ts CHANGED
@@ -1 +1 @@
1
- export const version = "1.41.0";
1
+ export const version = "1.42.0";
@@ -14,7 +14,13 @@ import {
14
14
 
15
15
  interface BaseDatabaseReader<DataModel extends GenericDataModel> {
16
16
  /**
17
- * Fetch a single document from the database by its {@link values.GenericId}.
17
+ * Fetch a single document from the database by table name and
18
+ * {@link values.GenericId}.
19
+ *
20
+ * @example
21
+ * ```typescript
22
+ * const user = await ctx.db.get("users", userId);
23
+ * ```
18
24
  *
19
25
  * @param table - The name of the table to fetch the document from.
20
26
  * @param id - The {@link values.GenericId} of the document to fetch from the database.
@@ -28,6 +34,9 @@ interface BaseDatabaseReader<DataModel extends GenericDataModel> {
28
34
  /**
29
35
  * Fetch a single document from the database by its {@link values.GenericId}.
30
36
  *
37
+ * Supported for backwards compatibility. Prefer `db.get(tableName, id)` in
38
+ * new code, or `db.system.get(tableName, id)` for system tables.
39
+ *
31
40
  * @param id - The {@link values.GenericId} of the document to fetch from the database.
32
41
  * @returns - The {@link GenericDocument} of the document at the given {@link values.GenericId}, or `null` if it no longer exists.
33
42
  */
@@ -55,7 +64,7 @@ interface BaseDatabaseReader<DataModel extends GenericDataModel> {
55
64
  * This accepts the string ID format as well as the `.toString()` representation
56
65
  * of the legacy class-based ID format.
57
66
  *
58
- * This does not guarantee that the ID exists (i.e. `db.get(id)` may return `null`).
67
+ * This does not guarantee that the ID exists (i.e. `db.get(tableName, id)` may return `null`).
59
68
  *
60
69
  * @param tableName - The name of the table.
61
70
  * @param id - The ID string.
@@ -109,7 +118,7 @@ export interface BaseTableReader<
109
118
  *
110
119
  * The two entry points are:
111
120
  * - {@link GenericDatabaseReader.get}, which fetches a single document
112
- * by its {@link values.GenericId}.
121
+ * by table name and {@link values.GenericId}.
113
122
  * - {@link GenericDatabaseReader.query}, which starts building a query.
114
123
  *
115
124
  * @example
@@ -272,6 +281,9 @@ export interface GenericDatabaseWriter<DataModel extends GenericDataModel>
272
281
  *
273
282
  * This method will throw if the document does not exist.
274
283
  *
284
+ * Supported for backwards compatibility. Prefer `db.patch(tableName, id, value)`
285
+ * in new code.
286
+ *
275
287
  * @param id - The {@link values.GenericId} of the document to patch.
276
288
  * @param value - The partial document to merge into the existing document.
277
289
  */
@@ -316,6 +328,9 @@ export interface GenericDatabaseWriter<DataModel extends GenericDataModel>
316
328
  * Unlike `patch`, which does a shallow merge, `replace` overwrites the
317
329
  * entire document.
318
330
  *
331
+ * Supported for backwards compatibility. Prefer `db.replace(tableName, id, value)`
332
+ * in new code.
333
+ *
319
334
  * @param id - The {@link values.GenericId} of the document to replace.
320
335
  * @param value - The new document. System fields can be omitted.
321
336
  */
@@ -343,6 +358,9 @@ export interface GenericDatabaseWriter<DataModel extends GenericDataModel>
343
358
  /**
344
359
  * Delete an existing document.
345
360
  *
361
+ * Supported for backwards compatibility. Prefer `db.delete(tableName, id)` in
362
+ * new code.
363
+ *
346
364
  * **Note:** Convex queries do not support `.delete()` directly on query
347
365
  * results. To delete multiple documents, `.collect()` them first, then
348
366
  * delete each one individually.