@cedarjs/cli 7.0.1-next.314 → 7.0.1-next.466

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 (59) hide show
  1. package/dist/commands/deploy/baremetal/baremetalHandler.js +375 -76
  2. package/dist/commands/deploy/baremetal/monitors.js +66 -0
  3. package/dist/commands/deploy/baremetal.js +6 -0
  4. package/dist/commands/destroy/scaffold/scaffoldHandler.js +63 -25
  5. package/dist/commands/dev/devHandler.js +13 -1
  6. package/dist/commands/generate/dbAuth/dbAuth.js +6 -2
  7. package/dist/commands/generate/dbAuth/dbAuthHandler.js +57 -14
  8. package/dist/commands/generate/dbAuth/templates/changePassword.tsx.template +131 -0
  9. package/dist/commands/generate/dbAuth/templates/login.webAuthn.tsx.template +20 -12
  10. package/dist/commands/generate/directive/directive.js +15 -2
  11. package/dist/commands/generate/helpers.js +26 -0
  12. package/dist/commands/generate/ogImage/templates/ogImage.og.tsx.template +1 -1
  13. package/dist/commands/generate/package/templates/tsconfig.json.template +1 -0
  14. package/dist/commands/generate/realtime/templates/liveQueries/blank/blank.service.ts.template +12 -4
  15. package/dist/commands/generate/realtime/templates/subscriptions/blank/blank.service.ts.template +2 -2
  16. package/dist/commands/generate/realtime/templates/subscriptions/blank/blank.ts.template +3 -3
  17. package/dist/commands/generate/scaffold/scaffold.js +1 -1
  18. package/dist/commands/generate/scaffold/scaffoldHandler.js +83 -44
  19. package/dist/commands/generate/scaffold/templates/assets/scaffold.tailwind.css.template +12 -7
  20. package/dist/commands/generate/scaffold/templates/components/EditNameCell.tsx.template +7 -7
  21. package/dist/commands/generate/scaffold/templates/components/NameForm.tsx.template +7 -6
  22. package/dist/commands/generate/scaffold/templates/layouts/NamesScaffoldLayout.tsx.template +18 -0
  23. package/dist/commands/generate/scaffold/templates/lib/formatters.tsx.template +3 -3
  24. package/dist/commands/generate/script/templates/tsconfig.json.template +1 -0
  25. package/dist/commands/generate/sdl/sdlHandler.js +8 -3
  26. package/dist/commands/generate/service/serviceHandler.js +32 -11
  27. package/dist/commands/generate/service/templates/service.ts.template +21 -8
  28. package/dist/commands/generate/service/templates/test.ts.template +7 -5
  29. package/dist/commands/jobsHandler.js +5 -3
  30. package/dist/commands/setup/cache/templates/memcached.ts.template +14 -17
  31. package/dist/commands/setup/cache/templates/redis.ts.template +12 -20
  32. package/dist/commands/setup/deploy/helpers/index.js +3 -1
  33. package/dist/commands/setup/deploy/providers/baremetal.js +7 -0
  34. package/dist/commands/setup/deploy/providers/baremetalHandler.js +128 -20
  35. package/dist/commands/setup/deploy/providers/netlifyHandler.js +25 -3
  36. package/dist/commands/setup/deploy/templates/baremetal.js +119 -9
  37. package/dist/commands/setup/deploy/templates/netlifyUD.js +3 -0
  38. package/dist/commands/setup/jobs/jobsHandler.js +2 -0
  39. package/dist/commands/setup/monitoring/sentry/sentryHandler.js +5 -2
  40. package/dist/commands/setup/monitoring/sentry/templates/sentryApi.ts.template +1 -4
  41. package/dist/commands/setup/monitoring/sentry/templates/sentryWeb.ts.template +7 -4
  42. package/dist/commands/setup/realtime/templates/defer/fastAndSlowFields/fastAndSlowFields.ts.template +4 -1
  43. package/dist/commands/setup/realtime/templates/liveQueries/auctions/auctions.ts.template +12 -4
  44. package/dist/commands/setup/realtime/templates/subscriptions/newMessage/newMessage.ts.template +1 -1
  45. package/dist/commands/setup/tenancy/signupCodemod.js +2 -1
  46. package/dist/commands/setup/tenancy/templates/InvitePage.tsx.template +10 -1
  47. package/dist/commands/setup/tenancy/templates/organizations.scenarios.ts.template +0 -2
  48. package/dist/commands/setup/tenancy/templates/organizations.test.ts.template +0 -8
  49. package/dist/commands/setup/tenancy/templates/organizations.ts.template +1 -1
  50. package/dist/commands/setup/tenancy/templates/requireMembership.test.ts.template +1 -1
  51. package/dist/commands/setup/tenancy/tenancyHandler.js +1 -1
  52. package/dist/commands/setup/ui/libraries/tailwindcssHandler.js +61 -81
  53. package/dist/commands/setup/uploads/gitignore.js +17 -0
  54. package/dist/commands/setup/uploads/serverFile.js +93 -27
  55. package/dist/commands/setup/uploads/templates/uploads.ts.template +14 -5
  56. package/dist/commands/setup/uploads/uploadsHandler.js +22 -1
  57. package/dist/lib/index.js +10 -6
  58. package/package.json +24 -24
  59. package/dist/commands/setup/ui/templates/postcss.config.cjs.template +0 -9
@@ -1,8 +1,7 @@
1
+ import { parse } from "@babel/parser";
1
2
  import { Listr } from "listr2";
2
- import pascalcase from "pascalcase";
3
3
  import { colors as c } from "@cedarjs/cli-helpers/colors";
4
4
  import { recordTelemetryAttributes } from "@cedarjs/cli-helpers/telemetry";
5
- import { pluralize } from "@cedarjs/utils/cedarPluralize";
6
5
  import {
7
6
  deleteFilesTask,
8
7
  getPaths,
@@ -13,6 +12,7 @@ import {
13
12
  import { verifyModelName } from "../../../lib/schemaHelpers.js";
14
13
  import {
15
14
  files,
15
+ getScaffoldLayoutNames,
16
16
  routes as scaffoldRoutes,
17
17
  splitPathAndModel
18
18
  } from "../../generate/scaffold/scaffoldHandler.js";
@@ -23,45 +23,83 @@ const removeRoutesWithSet = async ({
23
23
  }) => {
24
24
  const routes = await scaffoldRoutes({ model, path, nestScaffoldByModel });
25
25
  const routeNames = routes.map(extractRouteName);
26
- const pluralPascalName = pascalcase(pluralize(model));
27
- const layoutName = `${pluralPascalName}Layout`;
28
- return removeRoutesFromRouterTask(routeNames, layoutName);
26
+ const { importName } = getScaffoldLayoutNames(model, path);
27
+ return removeRoutesFromRouterTask(routeNames, importName);
28
+ };
29
+ const removeNamedImport = (code, source, name) => {
30
+ const program = parse(code, {
31
+ sourceType: "module",
32
+ plugins: ["jsx", "typescript"]
33
+ }).program;
34
+ for (const statement of program.body) {
35
+ if (statement.type !== "ImportDeclaration" || statement.source.value !== source) {
36
+ continue;
37
+ }
38
+ const specifiers = statement.specifiers;
39
+ const index = specifiers.findIndex(
40
+ (specifier2) => specifier2.type === "ImportSpecifier" && specifier2.imported.type === "Identifier" && specifier2.imported.name === name && specifier2.local.name === name
41
+ );
42
+ if (index === -1) {
43
+ continue;
44
+ }
45
+ const specifier = specifiers[index];
46
+ let start;
47
+ let end;
48
+ if (specifiers.length === 1) {
49
+ start = statement.start ?? 0;
50
+ end = statement.end ?? 0;
51
+ if (code[end] === "\n") {
52
+ end++;
53
+ }
54
+ } else if (index === specifiers.length - 1) {
55
+ start = specifiers[index - 1].end ?? 0;
56
+ end = specifier.end ?? 0;
57
+ } else {
58
+ start = specifier.start ?? 0;
59
+ end = specifiers[index + 1].start ?? 0;
60
+ }
61
+ return code.slice(0, start) + code.slice(end);
62
+ }
63
+ return code;
29
64
  };
30
65
  const removeSetImport = () => {
31
66
  const routesPath = getPaths().web.routes;
32
67
  const routesContent = readFile(routesPath).toString();
33
- if (routesContent.match("<Set")) {
68
+ const unusedNames = ["Set", "PrivateSet"].filter(
69
+ (name) => !new RegExp(`<${name}\\b`).test(routesContent)
70
+ );
71
+ if (unusedNames.length === 0) {
34
72
  return "Skipping removal of Set import in Routes.{jsx,tsx}";
35
73
  }
36
- const cedarRouterImportMatch = routesContent.match(
37
- /import {[^]*} from '@cedarjs\/router'/
38
- );
39
- if (!cedarRouterImportMatch) {
40
- return "No @cedarjs/router import found in Routes.{jsx,tsx}";
74
+ let newRoutesContent = routesContent;
75
+ try {
76
+ for (const name of unusedNames) {
77
+ newRoutesContent = removeNamedImport(
78
+ newRoutesContent,
79
+ "@cedarjs/router",
80
+ name
81
+ );
82
+ }
83
+ } catch {
84
+ return "Skipping removal of Set import, Routes.{jsx,tsx} couldn't be parsed";
85
+ }
86
+ if (newRoutesContent === routesContent) {
87
+ return "No unused Set or PrivateSet import found in Routes.{jsx,tsx}";
41
88
  }
42
- const [cedarRouterImport] = cedarRouterImportMatch;
43
- const removedSetImport = cedarRouterImport.replace(/,*\s*Set,*/, "");
44
- const newRoutesContent = routesContent.replace(
45
- cedarRouterImport,
46
- removedSetImport
47
- );
48
89
  writeFile(routesPath, newRoutesContent, { overwriteExisting: true });
49
- return "Removed Set import in Routes.{jsx,tsx}";
90
+ return "Removed unused Set and PrivateSet imports in Routes.{jsx,tsx}";
50
91
  };
51
92
  const removeLayoutImport = ({
52
93
  model: name,
53
94
  path: scaffoldPath = ""
54
95
  }) => {
55
- const pluralPascalName = pascalcase(pluralize(name));
56
- const pascalScaffoldPath = scaffoldPath === "" ? scaffoldPath : scaffoldPath.split("/").map(pascalcase).join("/") + "/";
57
- const layoutName = `${pluralPascalName}Layout`;
58
- const importLayout = `import ${pluralPascalName}Layout from 'src/layouts/${pascalScaffoldPath}${layoutName}'`;
96
+ const { importName, importPath } = getScaffoldLayoutNames(name, scaffoldPath);
59
97
  const routesPath = getPaths().web.routes;
60
98
  const routesContent = readFile(routesPath).toString();
61
- const newRoutesContent = routesContent.replace(
62
- new RegExp(`\\s*${importLayout}`),
63
- ""
99
+ const importLayout = new RegExp(
100
+ `\\s*import ${importName} from (['"])${importPath}\\1;?`
64
101
  );
102
+ const newRoutesContent = routesContent.replace(importLayout, "");
65
103
  writeFile(routesPath, newRoutesContent, { overwriteExisting: true });
66
104
  return "Removed layout import from Routes.{jsx,tsx}";
67
105
  };
@@ -185,21 +185,33 @@ const handler = async ({
185
185
  const rootPackageJson = JSON.parse(
186
186
  fs.readFileSync(rootPackageJsonPath, "utf8")
187
187
  );
188
+ const warnUdFallback = (reason) => {
189
+ console.warn(
190
+ c.warning(
191
+ `--ud has no effect: ${reason}. Falling back to classic dev-server mode.`
192
+ )
193
+ );
194
+ };
188
195
  const buildUnifiedDevCommand = () => {
189
196
  if (!ud) {
190
197
  return null;
191
198
  }
192
199
  if (streamingSsrEnabled) {
200
+ warnUdFallback("streaming SSR uses its own dev server");
193
201
  return null;
194
202
  }
195
203
  if (!workspace.includes("api") || !workspace.includes("web")) {
204
+ warnUdFallback("the unified dev server needs both the api and web sides");
196
205
  return null;
197
206
  }
198
207
  if (serverFile) {
208
+ warnUdFallback(
209
+ "the unified dev server does not support a custom server file"
210
+ );
199
211
  return null;
200
212
  }
201
213
  if (!fs.existsSync(cedarPaths.api.src) || !fs.existsSync(cedarPaths.web.src)) {
202
- console.log("api.src or web.src does not exist");
214
+ warnUdFallback("api.src or web.src does not exist");
203
215
  return null;
204
216
  }
205
217
  return [
@@ -1,9 +1,13 @@
1
1
  import { terminalLink } from "termi-link";
2
2
  import { getYargsDefaults, createHandler } from "../yargsCommandHelpers.js";
3
3
  const command = "dbAuth";
4
- const description = "Generate Login, Signup and Forgot Password pages for dbAuth";
4
+ const description = "Generate Login, Signup, Forgot Password, Reset Password and Change Password pages for dbAuth";
5
5
  const builder = (yargs) => {
6
- yargs.option("skip-forgot", {
6
+ yargs.option("skip-change", {
7
+ description: "Skip generating the Change Password page",
8
+ type: "boolean",
9
+ default: false
10
+ }).option("skip-forgot", {
7
11
  description: "Skip generating the Forgot Password page",
8
12
  type: "boolean",
9
13
  default: false
@@ -18,12 +18,25 @@ import {
18
18
  } from "../../../lib/index.js";
19
19
  import { prepareForRollback } from "../../../lib/rollback.js";
20
20
  import { templateForComponentFile } from "../yargsHandlerHelpers.js";
21
- const ROUTES = [
22
- `<Route path="/login" page={LoginPage} name="login" />`,
23
- `<Route path="/signup" page={SignupPage} name="signup" />`,
24
- `<Route path="/forgot-password" page={ForgotPasswordPage} name="forgotPassword" />`,
25
- `<Route path="/reset-password" page={ResetPasswordPage} name="resetPassword" />`
26
- ];
21
+ function routesFor({
22
+ skipChange,
23
+ skipForgot,
24
+ skipLogin,
25
+ skipReset,
26
+ skipSignup
27
+ }) {
28
+ return [
29
+ !skipLogin && `<Route path="/login" page={LoginPage} name="login" />`,
30
+ !skipSignup && `<Route path="/signup" page={SignupPage} name="signup" />`,
31
+ !skipForgot && `<Route path="/forgot-password" page={ForgotPasswordPage} name="forgotPassword" />`,
32
+ !skipReset && `<Route path="/reset-password" page={ResetPasswordPage} name="resetPassword" />`,
33
+ !skipChange && `<Route path="/change-password" page={ChangePasswordPage} name="changePassword" />`
34
+ ].filter((route) => typeof route === "string");
35
+ }
36
+ function pascalCase(str) {
37
+ const camelCased = camelCase(str);
38
+ return camelCased.charAt(0).toUpperCase() + camelCased.slice(1);
39
+ }
27
40
  function getPostInstallMessage(isDbAuthSetup2) {
28
41
  return [
29
42
  ` ${c.warning("Pages created! But you're not done yet:")}
@@ -35,9 +48,11 @@ function getPostInstallMessage(isDbAuthSetup2) {
35
48
  " navigate(routes.home())",
36
49
  " }\n",
37
50
  " and change the route to where you want them to go if the user is already",
38
- " logged in. Also take a look in the onSubmit() functions in ForgotPasswordPage",
39
- " and ResetPasswordPage to change where the user redirects to after submitting",
40
- " those forms.\n",
51
+ " logged in. Also take a look in the onSubmit() functions in ForgotPasswordPage,",
52
+ " ResetPasswordPage and ChangePasswordPage to change where the user redirects",
53
+ " to after submitting those forms.\n",
54
+ " ChangePasswordPage is for users who are already logged in. Link to it from",
55
+ " wherever your app lets users manage their account.\n",
41
56
  !isDbAuthSetup2 && ` Oh, and if you haven't already, add the necessary dbAuth functions and
42
57
  app setup by running:
43
58
 
@@ -58,9 +73,11 @@ function getPostInstallWebauthnMessage(isDbAuthSetup2) {
58
73
  " navigate(routes.home())",
59
74
  " }\n",
60
75
  " and change the route to where you want them to go if the user is already",
61
- " logged in. Also take a look in the onSubmit() functions in ForgotPasswordPage",
62
- " and ResetPasswordPage to change where the user redirects to after submitting",
63
- " those forms.\n",
76
+ " logged in. Also take a look in the onSubmit() functions in ForgotPasswordPage,",
77
+ " ResetPasswordPage and ChangePasswordPage to change where the user redirects",
78
+ " to after submitting those forms.\n",
79
+ " ChangePasswordPage is for users who are already logged in. Link to it from",
80
+ " wherever your app lets users manage their account.\n",
64
81
  !isDbAuthSetup2 && ` Oh, and if you haven't already, add the necessary dbAuth functions and
65
82
  app setup by running:
66
83
 
@@ -71,6 +88,7 @@ function getPostInstallWebauthnMessage(isDbAuthSetup2) {
71
88
  }
72
89
  const files = async ({
73
90
  typescript,
91
+ skipChange,
74
92
  skipForgot,
75
93
  skipLogin,
76
94
  skipReset,
@@ -90,7 +108,8 @@ const files = async ({
90
108
  usernameTitleCase: titleCase(usernameLabel),
91
109
  passwordLowerCase: passwordLabel.toLowerCase(),
92
110
  passwordCamelCase: camelCase(passwordLabel),
93
- passwordTitleCase: titleCase(passwordLabel)
111
+ passwordTitleCase: titleCase(passwordLabel),
112
+ passwordPascalCase: pascalCase(passwordLabel)
94
113
  };
95
114
  if (!skipForgot) {
96
115
  filesList.push(
@@ -131,6 +150,19 @@ const files = async ({
131
150
  })
132
151
  );
133
152
  }
153
+ if (!skipChange) {
154
+ filesList.push(
155
+ await templateForComponentFile({
156
+ name: "ChangePassword",
157
+ suffix: "Page",
158
+ extension: typescript ? ".tsx" : ".jsx",
159
+ webPathSection: "pages",
160
+ generator: "dbAuth",
161
+ templatePath: "changePassword.tsx.template",
162
+ templateVars
163
+ })
164
+ );
165
+ }
134
166
  if (!skipSignup) {
135
167
  filesList.push(
136
168
  await templateForComponentFile({
@@ -179,6 +211,7 @@ const tasks = ({
179
211
  listr2,
180
212
  force,
181
213
  typescript,
214
+ skipChange,
182
215
  skipForgot,
183
216
  skipLogin,
184
217
  skipReset,
@@ -289,6 +322,7 @@ const tasks = ({
289
322
  task: async () => {
290
323
  const filesObj = await files({
291
324
  typescript,
325
+ skipChange,
292
326
  skipForgot,
293
327
  skipLogin,
294
328
  skipReset,
@@ -305,7 +339,15 @@ const tasks = ({
305
339
  {
306
340
  title: "Adding routes...",
307
341
  task: async () => {
308
- addRoutesToRouterTask(ROUTES);
342
+ addRoutesToRouterTask(
343
+ routesFor({
344
+ skipChange,
345
+ skipForgot,
346
+ skipLogin,
347
+ skipReset,
348
+ skipSignup
349
+ })
350
+ );
309
351
  }
310
352
  },
311
353
  {
@@ -335,6 +377,7 @@ const tasks = ({
335
377
  const handler = async (yargs) => {
336
378
  recordTelemetryAttributes({
337
379
  command: "generate dbAuth",
380
+ skipChange: yargs.skipChange,
338
381
  skipForgot: yargs.skipForgot,
339
382
  skipLogin: yargs.skipLogin,
340
383
  skipReset: yargs.skipReset,
@@ -0,0 +1,131 @@
1
+ import { useEffect, useRef } from 'react'
2
+
3
+ import {
4
+ Form,
5
+ Label,
6
+ PasswordField,
7
+ Submit,
8
+ FieldError,
9
+ } from '@cedarjs/forms'
10
+ import { navigate, routes } from '@cedarjs/router'
11
+ import { Metadata } from '@cedarjs/web'
12
+ import { toast, Toaster } from '@cedarjs/web/toast'
13
+
14
+ import { useAuth } from 'src/auth'
15
+
16
+ const ChangePasswordPage = () => {
17
+ const { loading, isAuthenticated, reauthenticate, changePassword } =
18
+ useAuth()
19
+
20
+ // Only a logged in user can change their password
21
+ useEffect(() => {
22
+ if (!loading && !isAuthenticated) {
23
+ navigate(routes.login())
24
+ }
25
+ }, [loading, isAuthenticated])
26
+
27
+ const current${passwordPascalCase}Ref = useRef<HTMLInputElement>(null)
28
+ useEffect(() => {
29
+ current${passwordPascalCase}Ref.current?.focus()
30
+ }, [])
31
+
32
+ const onSubmit = async (data: Record<string, string>) => {
33
+ const response = await changePassword({
34
+ currentPassword: data.current${passwordPascalCase},
35
+ newPassword: data.new${passwordPascalCase},
36
+ })
37
+
38
+ if (response.error) {
39
+ toast.error(response.error)
40
+ } else {
41
+ // The function `changePassword.handler` in api/src/functions/auth.js
42
+ // decides if the user stays logged in. `reauthenticate()` picks up
43
+ // whichever it was
44
+ toast.success('${passwordTitleCase} changed!')
45
+ await reauthenticate()
46
+ navigate(routes.home())
47
+ }
48
+ }
49
+
50
+ return (
51
+ <>
52
+ <Metadata title="Change ${passwordTitleCase}" />
53
+
54
+ <main className="rw-main">
55
+ <Toaster toastOptions={{ className: 'rw-toast', duration: 6000 }} />
56
+ <div className="rw-scaffold rw-login-container">
57
+ <div className="rw-segment">
58
+ <header className="rw-segment-header">
59
+ <h2 className="rw-heading rw-heading-secondary">
60
+ Change ${passwordTitleCase}
61
+ </h2>
62
+ </header>
63
+
64
+ <div className="rw-segment-main">
65
+ <div className="rw-form-wrapper">
66
+ <Form onSubmit={onSubmit} className="rw-form-wrapper">
67
+ <Label
68
+ name="current${passwordPascalCase}"
69
+ className="rw-label"
70
+ errorClassName="rw-label rw-label-error"
71
+ >
72
+ Current ${passwordTitleCase}
73
+ </Label>
74
+ <PasswordField
75
+ name="current${passwordPascalCase}"
76
+ autoComplete="current-password"
77
+ className="rw-input"
78
+ errorClassName="rw-input rw-input-error"
79
+ ref={current${passwordPascalCase}Ref}
80
+ validation={{
81
+ required: {
82
+ value: true,
83
+ message: 'Current ${passwordTitleCase} is required',
84
+ },
85
+ }}
86
+ />
87
+
88
+ <FieldError
89
+ name="current${passwordPascalCase}"
90
+ className="rw-field-error"
91
+ />
92
+
93
+ <Label
94
+ name="new${passwordPascalCase}"
95
+ className="rw-label"
96
+ errorClassName="rw-label rw-label-error"
97
+ >
98
+ New ${passwordTitleCase}
99
+ </Label>
100
+ <PasswordField
101
+ name="new${passwordPascalCase}"
102
+ autoComplete="new-password"
103
+ className="rw-input"
104
+ errorClassName="rw-input rw-input-error"
105
+ validation={{
106
+ required: {
107
+ value: true,
108
+ message: 'New ${passwordTitleCase} is required',
109
+ },
110
+ }}
111
+ />
112
+
113
+ <FieldError
114
+ name="new${passwordPascalCase}"
115
+ className="rw-field-error"
116
+ />
117
+
118
+ <div className="rw-button-group">
119
+ <Submit className="rw-button rw-button-blue">Submit</Submit>
120
+ </div>
121
+ </Form>
122
+ </div>
123
+ </div>
124
+ </div>
125
+ </div>
126
+ </main>
127
+ </>
128
+ )
129
+ }
130
+
131
+ export default ChangePasswordPage
@@ -17,7 +17,7 @@ import { useAuth } from 'src/auth'
17
17
  const WELCOME_MESSAGE = 'Welcome back!'
18
18
  const REDIRECT = routes.home()
19
19
 
20
- const LoginPage = ({ type }) => {
20
+ const LoginPage = ({ type }: { type?: string }) => {
21
21
  const {
22
22
  isAuthenticated,
23
23
  client: webAuthn,
@@ -27,12 +27,12 @@ const LoginPage = ({ type }) => {
27
27
  } = useAuth()
28
28
  const [shouldShowWebAuthn, setShouldShowWebAuthn] = useState(false)
29
29
  const [showWebAuthn, setShowWebAuthn] = useState(
30
- webAuthn.isEnabled() && type !== 'password'
30
+ !!webAuthn?.isEnabled() && type !== 'password'
31
31
  )
32
32
 
33
33
  // should redirect right after login or wait to show the webAuthn prompts?
34
34
  useEffect(() => {
35
- if (isAuthenticated && (!shouldShowWebAuthn || webAuthn.isEnabled())) {
35
+ if (isAuthenticated && (!shouldShowWebAuthn || webAuthn?.isEnabled())) {
36
36
  navigate(REDIRECT)
37
37
  }
38
38
  }, [isAuthenticated, shouldShowWebAuthn])
@@ -45,13 +45,13 @@ const LoginPage = ({ type }) => {
45
45
  }, [loading, isAuthenticated])
46
46
 
47
47
  // focus on the ${usernameLowerCase} field as soon as the page loads
48
- const ${usernameCamelCase}Ref = useRef()
48
+ const ${usernameCamelCase}Ref = useRef<HTMLInputElement>(null)
49
49
  useEffect(() => {
50
- ${usernameCamelCase}Ref.current && ${usernameCamelCase}Ref.current.focus()
50
+ ${usernameCamelCase}Ref.current?.focus()
51
51
  }, [])
52
52
 
53
- const onSubmit = async (data) => {
54
- const webAuthnSupported = await webAuthn.isSupported()
53
+ const onSubmit = async (data: Record<string, string>) => {
54
+ const webAuthnSupported = !!(await webAuthn?.isSupported())
55
55
 
56
56
  if (webAuthnSupported) {
57
57
  setShouldShowWebAuthn(true)
@@ -75,30 +75,38 @@ const LoginPage = ({ type }) => {
75
75
  }
76
76
 
77
77
  const onAuthenticate = async () => {
78
+ if (!webAuthn) {
79
+ return
80
+ }
81
+
78
82
  try {
79
83
  await webAuthn.authenticate()
80
84
  await reauthenticate()
81
85
  toast.success(WELCOME_MESSAGE)
82
86
  navigate(REDIRECT)
83
87
  } catch (e) {
84
- if (e.name === 'WebAuthnDeviceNotFoundError') {
88
+ if (e instanceof Error && e.name === 'WebAuthnDeviceNotFoundError') {
85
89
  toast.error(
86
90
  'Device not found, log in with ${usernameTitleCase}/${passwordTitleCase} to continue'
87
91
  )
88
92
  setShowWebAuthn(false)
89
93
  } else {
90
- toast.error(e.message)
94
+ toast.error(e instanceof Error ? e.message : String(e))
91
95
  }
92
96
  }
93
97
  }
94
98
 
95
99
  const onRegister = async () => {
100
+ if (!webAuthn) {
101
+ return
102
+ }
103
+
96
104
  try {
97
105
  await webAuthn.register()
98
106
  toast.success(WELCOME_MESSAGE)
99
107
  navigate(REDIRECT)
100
108
  } catch (e) {
101
- toast.error(e.message)
109
+ toast.error(e instanceof Error ? e.message : String(e))
102
110
  }
103
111
  }
104
112
 
@@ -200,7 +208,7 @@ const LoginPage = ({ type }) => {
200
208
 
201
209
  const formToRender = () => {
202
210
  if (showWebAuthn) {
203
- if (webAuthn.isEnabled()) {
211
+ if (webAuthn?.isEnabled()) {
204
212
  return <AuthWebAuthnPrompt />
205
213
  } else {
206
214
  return <RegisterWebAuthnPrompt />
@@ -212,7 +220,7 @@ const LoginPage = ({ type }) => {
212
220
 
213
221
  const linkToRender = () => {
214
222
  if (showWebAuthn) {
215
- if (webAuthn.isEnabled()) {
223
+ if (webAuthn?.isEnabled()) {
216
224
  return (
217
225
  <div className="rw-login-link">
218
226
  <span>or login with </span>{' '}
@@ -1,11 +1,24 @@
1
1
  import {
2
2
  createBuilder,
3
3
  createCommand,
4
- createHandler
4
+ createHandler,
5
+ getYargsDefaults
5
6
  } from "../yargsCommandHelpers.js";
6
7
  const command = createCommand("directive");
7
8
  const description = "Generate a new GraphQL directive";
8
- const builder = createBuilder({ componentName: "directive" });
9
+ const builder = createBuilder({
10
+ componentName: "directive",
11
+ optionsObj: () => ({
12
+ ...getYargsDefaults(),
13
+ // No default value, so the handler prompts for a type when the option is
14
+ // omitted
15
+ type: {
16
+ description: "Directive type. If omitted, the generator prompts for one",
17
+ type: "string",
18
+ choices: ["validator", "transformer"]
19
+ }
20
+ })
21
+ });
9
22
  const handler = createHandler("directive");
10
23
  export {
11
24
  builder,
@@ -37,11 +37,33 @@ const redactedModelFields = (fieldNames) => {
37
37
  }
38
38
  return sensitive;
39
39
  };
40
+ const DEFAULT_IGNORE_FIELDS_FOR_INPUT = ["createdAt", "updatedAt"];
41
+ const requiredFieldsInUpdateInput = (model) => {
42
+ return inputFields(model).filter((field) => {
43
+ return field.isRequired && !field.isList && !field.isId;
44
+ }).map((field) => field.name);
45
+ };
46
+ const nullableJsonFieldsInInput = (model) => {
47
+ return inputFields(model).filter((field) => {
48
+ return field.type === "Json" && !field.isRequired && !field.isList;
49
+ }).map((field) => field.name);
50
+ };
51
+ const inputFields = (model) => {
52
+ const redactedFields = redactedModelFields(
53
+ model.fields.map((field) => field.name)
54
+ );
55
+ return model.fields.filter((field) => {
56
+ return field.kind !== "object" && !DEFAULT_IGNORE_FIELDS_FOR_INPUT.includes(field.name) && !redactedFields.includes(field.name);
57
+ });
58
+ };
40
59
  const relationsForModel = (model) => {
41
60
  return model?.fields.filter((f) => f.relationName).map((field) => {
42
61
  return field.name;
43
62
  });
44
63
  };
64
+ const nullableRelationsForModel = (model) => {
65
+ return model.fields.filter((f) => f.kind === "object" && !f.isList && !f.isRequired).map((f) => f.name);
66
+ };
45
67
  const intForeignKeysForModel = (model) => {
46
68
  return model.fields.filter((f) => f.name.match(/Id$/) && f.type === "Int").map((f) => f.name);
47
69
  };
@@ -74,14 +96,18 @@ const mapPrismaScalarToPagePropTsType = (scalarType) => {
74
96
  return prismaScalarToTsType[scalarType] || "unknown";
75
97
  };
76
98
  export {
99
+ DEFAULT_IGNORE_FIELDS_FOR_INPUT,
77
100
  SENSITIVE_FIELDS,
78
101
  forcePluralizeWord,
79
102
  intForeignKeysForModel,
80
103
  mapPrismaScalarToPagePropTsType,
81
104
  mapRouteParamTypeToTsType,
105
+ nullableJsonFieldsInInput,
106
+ nullableRelationsForModel,
82
107
  pathName,
83
108
  redactedModelFields,
84
109
  relationsForModel,
85
110
  removeGeneratorName,
111
+ requiredFieldsInUpdateInput,
86
112
  validateName
87
113
  };
@@ -2,7 +2,7 @@ export const data = async () => {
2
2
  return new Date()
3
3
  }
4
4
 
5
- export const output = ({ data }) => {
5
+ export const output = ({ data }: { data: Date }) => {
6
6
  return (
7
7
  <>
8
8
  <h1>${pageName} og:image</h1>
@@ -4,6 +4,7 @@
4
4
  "target": "es2023",
5
5
  "module": "node20",
6
6
  "esModuleInterop": true,
7
+ "strict": true,
7
8
  "skipLibCheck": true,
8
9
  "baseUrl": ".",
9
10
  "rootDir": "src",