@cedarjs/cli 8.0.0-canary.3408 → 8.0.0-canary.3411

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.
@@ -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>{' '}
@@ -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",
@@ -1,9 +1,17 @@
1
1
  // api/src/services/${name}s/${name}s.ts
2
+ import type { ${typeName}ItemInput } from 'types/graphql'
3
+
2
4
  import type { LiveQueryStorageMechanism } from '@cedarjs/realtime'
3
5
 
4
6
  import { logger } from 'src/lib/logger'
5
7
 
6
- const ${collectionName} = [
8
+ interface ${typeName}Data {
9
+ id: string
10
+ title: string
11
+ items: { amount: number }[]
12
+ }
13
+
14
+ const ${collectionName}: ${typeName}Data[] = [
7
15
  { id: '1', title: 'First ${modelName}', items: [{ amount: 11 }] },
8
16
  { id: '2', title: '2nd ${modelName}', items: [{ amount: 22 }] },
9
17
  { id: '3', title: 'Third ${modelName}', items: [{ amount: 33 }] },
@@ -35,14 +43,14 @@ const ${collectionName} = [
35
43
  * }
36
44
  * }
37
45
  */
38
- export const ${liveQueryName} = async ({ id }) => {
46
+ export const ${liveQueryName} = async ({ id }: { id: string }) => {
39
47
  const found${modelName} = ${collectionName}.find((a) => a.id === id)
40
48
  logger.debug({ id, ${name}: found${modelName} }, `${name} details`)
41
49
  return found${modelName}
42
50
  }
43
51
 
44
52
  export const create${typeName}Item = async (
45
- { input },
53
+ { input }: { input: ${typeName}ItemInput },
46
54
  { context }: { context: { liveQueryStore: LiveQueryStorageMechanism } }
47
55
  ) => {
48
56
  const { ${camelName}Id, amount } = input
@@ -63,7 +71,7 @@ export const create${typeName}Item = async (
63
71
  }
64
72
 
65
73
  export const ${modelName} = {
66
- largest${typeName}Item: (obj, { root }) => {
74
+ largest${typeName}Item: (obj: unknown, { root }: { root: ${typeName}Data }) => {
67
75
  const [largest] = root.items.sort((a, b) => b.amount - a.amount)
68
76
 
69
77
  logger.debug({ obj, root }, 'largest ${typeName}Item')
@@ -1,9 +1,9 @@
1
1
  import type { ${subscriptionInputType} } from 'types/graphql'
2
2
 
3
3
  import { logger } from 'src/lib/logger'
4
- import type { Publish${typeName}ChannelType } from 'src/subscriptions/${name}/${name}.service.js'
4
+ import type { Publish${typeName}ChannelType } from 'src/subscriptions/${name}/${name}.js'
5
5
 
6
- export const ${subscriptionQueryName} = ({ id }) => [id]
6
+ export const ${subscriptionQueryName} = ({ id }: { id: string }) => [id]
7
7
 
8
8
  export const ${subscriptionServiceResolver} = async (
9
9
  { input }: { input: ${subscriptionInputType} },
@@ -39,15 +39,15 @@ export type Publish${typeName}ChannelType = PubSub<Publish${typeName}Channel>
39
39
  const ${subscriptionName}Subscription = {
40
40
  ${subscriptionName}: {
41
41
  subscribe: (
42
- _,
43
- { id },
42
+ _: unknown,
43
+ { id }: { id: string },
44
44
  { pubSub }: { pubSub: Publish${typeName}ChannelType }
45
45
  ) => {
46
46
  logger.debug({ id }, '${name} subscription')
47
47
 
48
48
  return pubSub.subscribe('${typeName}', id)
49
49
  },
50
- resolve: (payload) => {
50
+ resolve: (payload: Publish${typeName}Channel['${typeName}'][1]) => {
51
51
  logger.debug({ payload }, '${name} subscription resolve')
52
52
 
53
53
  return payload
@@ -1,6 +1,7 @@
1
1
  ${useClientDirective}import type {
2
2
  Edit${singularPascalName}By${pascalIdName},
3
3
  Update${singularPascalName}Input,
4
+ Update${singularPascalName}Mutation,
4
5
  Update${singularPascalName}MutationVariables
5
6
  } from 'types/graphql'
6
7
 
@@ -24,7 +25,7 @@ export const QUERY: TypedDocumentNode<Edit${singularPascalName}By${pascalIdName}
24
25
  `
25
26
 
26
27
  const UPDATE_${singularConstantName}_MUTATION: TypedDocumentNode<
27
- Edit${singularPascalName}ById,
28
+ Update${singularPascalName}Mutation,
28
29
  Update${singularPascalName}MutationVariables
29
30
  > = gql`
30
31
  mutation Update${singularPascalName}Mutation($${idName}: ${idType}!, $input: Update${singularPascalName}Input!) {
@@ -54,17 +55,14 @@ export const Success = ({ ${singularCamelName} }: CellSuccessProps<Edit${singula
54
55
  }
55
56
  )
56
57
 
57
- const onSave = (
58
- input: Update${singularPascalName}Input,
59
- id: Edit${singularPascalName}By${pascalIdName}['${singularCamelName}']['id']
60
- ) => {
61
- update${singularPascalName}({ variables: { id, input } })
58
+ const onSave = (input: Update${singularPascalName}Input) => {
59
+ update${singularPascalName}({ variables: { ${idName}: ${singularCamelName}.${idName}, input } })
62
60
  }
63
61
 
64
62
  return (
65
63
  <div className="rw-segment">
66
64
  <header className="rw-segment-header">
67
- <h2 className="rw-heading rw-heading-secondary">Edit ${singularPascalName} {${singularCamelName}?.id}</h2>
65
+ <h2 className="rw-heading rw-heading-secondary">Edit ${singularPascalName} {${singularCamelName}.${idName}}</h2>
68
66
  </header>
69
67
  <div className="rw-segment-main">
70
68
  <${singularPascalName}Form ${singularCamelName}={${singularCamelName}} onSave={onSave} error={error} loading={loading} />
@@ -1,4 +1,4 @@
1
- import type { Edit${singularPascalName}By${pascalIdName}, Update${singularPascalName}Input } from 'types/graphql'
1
+ import type { Edit${singularPascalName}By${pascalIdName}, Create${singularPascalName}Input } from 'types/graphql'
2
2
 
3
3
  import type { RWGqlError } from '@cedarjs/forms'
4
4
  import {
@@ -48,8 +48,8 @@ type Form${singularPascalName} = NonNullable<Edit${singularPascalName}By${pascal
48
48
 
49
49
  interface ${singularPascalName}FormProps {
50
50
  ${singularCamelName}?: Edit${singularPascalName}By${pascalIdName}['${singularCamelName}']
51
- onSave: (data: Update${singularPascalName}Input, ${idName}?: Form${singularPascalName}['${idName}']) => void
52
- error: RWGqlError
51
+ onSave: (data: Create${singularPascalName}Input) => void
52
+ error?: RWGqlError
53
53
  loading: boolean
54
54
  }
55
55
 
@@ -70,7 +70,8 @@ const ${singularPascalName}Form = (props: ${singularPascalName}FormProps) => {
70
70
  }
71
71
  <% } %>
72
72
  <% if (column.isEnum && !column.isList && !column.isRequired) { %>
73
- if (data.${column.name} === '') {
73
+ // The "None" option has an empty value
74
+ if (!data.${column.name}) {
74
75
  data.${column.name} = null
75
76
  }
76
77
  <% } %>
@@ -80,7 +81,7 @@ const ${singularPascalName}Form = (props: ${singularPascalName}FormProps) => {
80
81
  }
81
82
  <% } %>
82
83
  <% }) %>
83
- props.onSave(data, props?.${singularCamelName}?.${idName})
84
+ props.onSave(data)
84
85
  }
85
86
 
86
87
  return (
@@ -136,7 +137,7 @@ const ${singularPascalName}Form = (props: ${singularPascalName}FormProps) => {
136
137
  <% } else { %>
137
138
  <${column.component}
138
139
  name="${column.name}"
139
- ${column.defaultProp}={${column.deserializeFunction && column.deserializeFunction + '('}props.${singularCamelName}?.<%= column.name %>${column.deserializeFunction && ')'}}
140
+ ${column.defaultProp}={${column.deserializeFunction && column.deserializeFunction + '('}props.${singularCamelName}?.<%= column.name %>${column.deserializeFunction && ')'}<%= !column.deserializeFunction && !column.isRequired ? ' ?? undefined' : '' %>}
140
141
  className="rw-input"
141
142
  errorClassName="rw-input rw-input-error"
142
143
  <%= !column.validation ? '' : `validation=${column.validation}` %>
@@ -25,7 +25,7 @@ export const jsonDisplay = (obj: unknown) => {
25
25
  )
26
26
  }
27
27
 
28
- export const truncate = (value: string | number) => {
28
+ export const truncate = (value: string | number | null | undefined) => {
29
29
  let output = value?.toString() ?? ''
30
30
 
31
31
  if (output.length > MAX_STRING_LENGTH) {
@@ -65,6 +65,6 @@ export function timeTag(dateTime: <%= parsedDateTime ? 'Date' : 'string' %> | un
65
65
  return output
66
66
  }
67
67
 
68
- export const checkboxInputTag = (checked: boolean) => {
69
- return <input type="checkbox" checked={checked} disabled />
68
+ export const checkboxInputTag = (checked: boolean | null | undefined) => {
69
+ return <input type="checkbox" checked={!!checked} disabled />
70
70
  }
@@ -2,6 +2,7 @@
2
2
  "compilerOptions": {
3
3
  "noEmit": true,
4
4
  "allowJs": true,
5
+ "strict": true,
5
6
  "esModuleInterop": true,
6
7
  "baseUrl": "./",
7
8
  "rootDirs": [
@@ -19,7 +19,11 @@ import {
19
19
  getEnum,
20
20
  verifyModelName
21
21
  } from "../../../lib/schemaHelpers.js";
22
- import { redactedModelFields, relationsForModel } from "../helpers.js";
22
+ import {
23
+ DEFAULT_IGNORE_FIELDS_FOR_INPUT,
24
+ redactedModelFields,
25
+ relationsForModel
26
+ } from "../helpers.js";
23
27
  import { files as serviceFiles } from "../service/serviceHandler.js";
24
28
  import { templateForFile } from "../yargsHandlerHelpers.js";
25
29
  import {
@@ -27,7 +31,6 @@ import {
27
31
  missingRelatedModels,
28
32
  writeFilesWithStubsTask
29
33
  } from "./stubFiles.js";
30
- const DEFAULT_IGNORE_FIELDS_FOR_INPUT = ["createdAt", "updatedAt"];
31
34
  const missingIdConsoleMessage = () => {
32
35
  const line1 = ansis.bold.yellow("WARNING") + ": Cannot generate CRUD SDL without an `@id` database column.";
33
36
  const line2 = "If you are trying to generate for a many-to-many join table ";
@@ -61,7 +64,8 @@ const modelFieldToSDL = ({
61
64
  Bytes: "Byte"
62
65
  };
63
66
  const gqlType = prismaTypeToGraphqlType[field.type] || field.type;
64
- const type = field.isList ? `[${gqlType}]` : gqlType;
67
+ const listItemType = field.kind === "object" ? gqlType : `${gqlType}!`;
68
+ const type = field.isList ? `[${listItemType}]` : gqlType;
65
69
  const isRequired = field.isRequired && required || field.isList || field.isId;
66
70
  const fieldContent = `${field.name}: ${type}${isRequired ? "!" : ""}`;
67
71
  if (docs) {
@@ -332,6 +336,7 @@ const handler = async ({
332
336
  export {
333
337
  files,
334
338
  handler,
339
+ modelFieldToSDL,
335
340
  printRedactedFieldsNote,
336
341
  redactedSensitiveFields,
337
342
  stubFiles
@@ -3,7 +3,14 @@ import camelcase from "camelcase";
3
3
  import { pluralize, singularize } from "@cedarjs/utils/cedarPluralize";
4
4
  import { transformTSToJS } from "../../../lib/index.js";
5
5
  import { getSchema, verifyModelName } from "../../../lib/schemaHelpers.js";
6
- import { redactedModelFields, relationsForModel } from "../helpers.js";
6
+ import {
7
+ DEFAULT_IGNORE_FIELDS_FOR_INPUT,
8
+ nullableJsonFieldsInInput,
9
+ nullableRelationsForModel,
10
+ redactedModelFields,
11
+ relationsForModel,
12
+ requiredFieldsInUpdateInput
13
+ } from "../helpers.js";
7
14
  import { createHandler, templateForFile } from "../yargsHandlerHelpers.js";
8
15
  function isServiceModel(schema) {
9
16
  return typeof schema === "object" && schema !== null && "fields" in schema && "name" in schema;
@@ -59,11 +66,13 @@ function scenarioFieldValue(field) {
59
66
  return randInt;
60
67
  case "Json":
61
68
  return { foo: "bar" };
62
- case "String":
69
+ case "String": {
70
+ const isUnique = field.isUnique || field.isId;
63
71
  if (field.name?.toLowerCase().includes("email")) {
64
- return field.isUnique ? `foo${randInt}@bar.com` : "foo@bar.com";
72
+ return isUnique ? `foo${randInt}@bar.com` : "foo@bar.com";
65
73
  }
66
- return field.isUnique ? `String${randInt}` : "String";
74
+ return isUnique ? `String${randInt}` : "String";
75
+ }
67
76
  case "Bytes":
68
77
  return `new Uint8Array([${randIntArray}])`;
69
78
  default: {
@@ -152,11 +161,14 @@ const fieldsToInput = async (model) => {
152
161
  const modelName = camelcase(singularize(model));
153
162
  const inputObj = {};
154
163
  if (scalarFields.some(
155
- (field) => redactedFields.includes(field.name)
164
+ (field) => redactedFields.includes(field.name) || DEFAULT_IGNORE_FIELDS_FOR_INPUT.includes(field.name) && !field.isUpdatedAt
156
165
  )) {
157
166
  return false;
158
167
  }
159
168
  scalarFields.forEach((field) => {
169
+ if (DEFAULT_IGNORE_FIELDS_FOR_INPUT.includes(field.name)) {
170
+ return;
171
+ }
160
172
  if (foreignKeys.includes(field.name)) {
161
173
  inputObj[field.name] = `scenario.${modelName}.two.${field.name}`;
162
174
  } else {
@@ -174,7 +186,7 @@ const fieldsToUpdate = async (model) => {
174
186
  const modelName = camelcase(singularize(model));
175
187
  let field, newValue, fieldName;
176
188
  const updatableFields = scalarFields.filter(
177
- (scalar) => !redactedFields.includes(scalar.name)
189
+ (scalar) => !redactedFields.includes(scalar.name) && !DEFAULT_IGNORE_FIELDS_FOR_INPUT.includes(scalar.name)
178
190
  );
179
191
  field = updatableFields.find(
180
192
  (scalar) => !foreignKeys.includes(scalar.name)
@@ -273,13 +285,19 @@ const files = async ({
273
285
  }
274
286
  const prismaImportSource = "src/lib/db";
275
287
  let modelRelations;
288
+ let updateInputRequiredFields = [];
289
+ let nullableJsonFields = [];
290
+ let nullableRelations = [];
291
+ const schemaResult = await getSchema(model);
292
+ if (isServiceModel(schemaResult)) {
293
+ updateInputRequiredFields = requiredFieldsInUpdateInput(schemaResult);
294
+ nullableJsonFields = nullableJsonFieldsInInput(schemaResult);
295
+ nullableRelations = nullableRelationsForModel(schemaResult);
296
+ }
276
297
  if (relations) {
277
298
  modelRelations = relations;
278
- } else {
279
- const schemaResult = await getSchema(model);
280
- if (isServiceModel(schemaResult)) {
281
- modelRelations = relationsForModel(schemaResult);
282
- }
299
+ } else if (isServiceModel(schemaResult)) {
300
+ modelRelations = relationsForModel(schemaResult);
283
301
  }
284
302
  const serviceFile = await templateForFile({
285
303
  name,
@@ -292,6 +310,9 @@ const files = async ({
292
310
  relations: modelRelations,
293
311
  idName,
294
312
  prismaImportSource,
313
+ updateInputRequiredFields,
314
+ nullableJsonFields,
315
+ nullableRelations,
295
316
  ...rest
296
317
  }
297
318
  });
@@ -1,6 +1,6 @@
1
1
  import type { QueryResolvers<% if (crud) { %>, MutationResolvers<% } %><% if (relations.length) { %>, ${singularPascalName}RelationResolvers<% } %> } from 'types/graphql'
2
2
 
3
- import { db } from 'src/lib/db'
3
+ import { db<% if (crud && nullableJsonFields.length) { %>, Prisma<% } %> } from 'src/lib/db'
4
4
  <%
5
5
  // A compound `@@id([...])` key comes through as `{ fields, whereArg }`
6
6
  // instead of a single field name -- build the destructure/where-clause
@@ -13,8 +13,8 @@ import { db } from 'src/lib/db'
13
13
  ? `${idName.whereArg}: { ${idFields.join(', ')} }`
14
14
  : idName
15
15
  const idWhereFromRoot = idIsComposite
16
- ? `${idName.whereArg}: { ${idFields.map((f) => `${f}: root?.${f}`).join(', ')} }`
17
- : `${idName}: root?.${idName}`
16
+ ? `${idName.whereArg}: { ${idFields.map((f) => `${f}: root.${f}`).join(', ')} }`
17
+ : `${idName}: root.${idName}`
18
18
  %>
19
19
  export const ${pluralCamelName}: QueryResolvers['${pluralCamelName}'] = () => {
20
20
  return db.${singularCamelName}.findMany()
@@ -27,14 +27,27 @@ export const ${singularCamelName}: QueryResolvers['${singularCamelName}'] = ({ $
27
27
  }<% } %><% if (crud) { %>
28
28
 
29
29
  export const create${singularPascalName}: MutationResolvers['create${singularPascalName}'] = ({ input }) => {
30
- return db.${singularCamelName}.create({
31
- data: input,
30
+ return db.${singularCamelName}.create({<% if (nullableJsonFields.length) { %>
31
+ data: {
32
+ ...input,
33
+ // Prisma uses Prisma.DbNull, not null, to set a Json field to NULL<% nullableJsonFields.forEach((field) => { %>
34
+ ${field}: input.${field} === null ? Prisma.DbNull : input.${field},<% }) %>
35
+ },<% } else { %>
36
+ data: input,<% } %>
32
37
  })
33
38
  }
34
39
 
35
40
  export const update${singularPascalName}: MutationResolvers['update${singularPascalName}'] = ({ ${idArgs}, input }) => {
36
- return db.${singularCamelName}.update({
37
- data: input,
41
+ return db.${singularCamelName}.update({<% if (updateInputRequiredFields.length || nullableJsonFields.length) { %>
42
+ data: {
43
+ ...input,<% if (updateInputRequiredFields.length) { %>
44
+ // Every field in the GraphQL input is nullable, but Prisma doesn't
45
+ // accept null for required fields. undefined leaves the field unchanged<% updateInputRequiredFields.forEach((field) => { %>
46
+ ${field}: input.${field} ?? undefined,<% }) %><% } %><% if (nullableJsonFields.length) { %>
47
+ // Prisma uses Prisma.DbNull, not null, to set a Json field to NULL<% nullableJsonFields.forEach((field) => { %>
48
+ ${field}: input.${field} === null ? Prisma.DbNull : input.${field},<% }) %><% } %>
49
+ },<% } else { %>
50
+ data: input,<% } %>
38
51
  where: { ${idWhere} },
39
52
  })
40
53
  }
@@ -47,6 +60,6 @@ export const delete${singularPascalName}: MutationResolvers['delete${singularPas
47
60
 
48
61
  export const ${singularPascalName}: ${singularPascalName}RelationResolvers = {<% relations.forEach(relation => { %>
49
62
  ${relation}: (_obj, { root }) => {
50
- return db.${singularCamelName}.findUnique({ where: { ${idWhereFromRoot} } }).${relation}()
63
+ return db.${singularCamelName}.findUnique<%= nullableRelations.includes(relation) ? '' : 'OrThrow' %>({ where: { ${idWhereFromRoot} } }).${relation}()
51
64
  },<% }) %>
52
65
  }<% } %>
@@ -7,8 +7,10 @@
7
7
  // Looks for quoted strings, either by single (') or double (") quotes.
8
8
  // When found
9
9
  // - Removes the quotes around `scenario` variables.
10
- // - Removes the quotes around `BigInt` fields.
11
- const transformValue = (obj, type) => {
10
+ // - Removes the quotes around `BigInt` fields. GraphQL inputs type BigInt
11
+ // values as numbers, so the `n` suffix is also removed when `asInput` is
12
+ // set
13
+ const transformValue = (obj, type, asInput = false) => {
12
14
  if (type === 'DateTime') {
13
15
  return `new Date('${obj.toISOString()}')`
14
16
  } else if (type === 'Decimal') {
@@ -22,7 +24,7 @@
22
24
 
23
25
  // BigInt
24
26
  if (string.match(/^\"\d+n\"$/)) {
25
- return string.slice(1, string.length - 1)
27
+ return string.slice(1, string.length - (asInput ? 2 : 1))
26
28
  }
27
29
 
28
30
  return string
@@ -57,7 +59,7 @@ describe('${pluralCamelName}', () => {
57
59
 
58
60
  <% if (create) { %>scenario('creates a ${singularCamelName}', async (${transformValue(create).includes('scenario.') ? 'scenario: StandardScenario' : ''}) => {
59
61
  const result = await create${singularPascalName}({
60
- input: ${transformValue(create)},
62
+ input: ${transformValue(create, undefined, true)},
61
63
  })
62
64
 
63
65
  <% for (const [name, value] of Object.entries(create)) { %>
@@ -68,7 +70,7 @@ describe('${pluralCamelName}', () => {
68
70
  const original = await (${singularCamelName}({ ${idName}: scenario.${singularCamelName}.one.${idName} })) as ${prismaModel}
69
71
  const result = await update${singularPascalName}({
70
72
  ${idName}: original.${idName},
71
- input: ${transformValue(update)},
73
+ input: ${transformValue(update, undefined, true)},
72
74
  })
73
75
 
74
76
  <% for (const [name, value] of Object.entries(update)) { %>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cedarjs/cli",
3
- "version": "8.0.0-canary.3408",
3
+ "version": "8.0.0-canary.3411",
4
4
  "description": "The CedarJS Command Line",
5
5
  "repository": {
6
6
  "type": "git",
@@ -36,17 +36,17 @@
36
36
  "@babel/preset-typescript": "7.29.7",
37
37
  "@babel/traverse": "7.29.10",
38
38
  "@babel/types": "7.29.8",
39
- "@cedarjs/api-server": "8.0.0-canary.3408",
40
- "@cedarjs/babel-config": "8.0.0-canary.3408",
41
- "@cedarjs/cli-helpers": "8.0.0-canary.3408",
42
- "@cedarjs/internal": "8.0.0-canary.3408",
43
- "@cedarjs/prerender": "8.0.0-canary.3408",
44
- "@cedarjs/project-config": "8.0.0-canary.3408",
45
- "@cedarjs/structure": "8.0.0-canary.3408",
46
- "@cedarjs/telemetry": "8.0.0-canary.3408",
47
- "@cedarjs/utils": "8.0.0-canary.3408",
48
- "@cedarjs/vite": "8.0.0-canary.3408",
49
- "@cedarjs/web-server": "8.0.0-canary.3408",
39
+ "@cedarjs/api-server": "8.0.0-canary.3411",
40
+ "@cedarjs/babel-config": "8.0.0-canary.3411",
41
+ "@cedarjs/cli-helpers": "8.0.0-canary.3411",
42
+ "@cedarjs/internal": "8.0.0-canary.3411",
43
+ "@cedarjs/prerender": "8.0.0-canary.3411",
44
+ "@cedarjs/project-config": "8.0.0-canary.3411",
45
+ "@cedarjs/structure": "8.0.0-canary.3411",
46
+ "@cedarjs/telemetry": "8.0.0-canary.3411",
47
+ "@cedarjs/utils": "8.0.0-canary.3411",
48
+ "@cedarjs/vite": "8.0.0-canary.3411",
49
+ "@cedarjs/web-server": "8.0.0-canary.3411",
50
50
  "@listr2/prompt-adapter-enquirer": "4.3.3",
51
51
  "@opentelemetry/api": "1.9.1",
52
52
  "@opentelemetry/core": "2.12.0",
@@ -94,7 +94,7 @@
94
94
  "yargs": "17.7.3"
95
95
  },
96
96
  "devDependencies": {
97
- "@cedarjs/framework-tools": "8.0.0-canary.3408",
97
+ "@cedarjs/framework-tools": "8.0.0-canary.3411",
98
98
  "@prisma/dmmf": "7.10.0",
99
99
  "@types/archiver": "^7.0.0",
100
100
  "memfs": "4.80.0",