Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .changesets/release_notes_v8.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,25 @@ npx @tailwindcss/upgrade --config ./web/config/tailwind.config.cjs
See Tailwind's [upgrade guide](https://tailwindcss.com/docs/upgrade-guide) for
the changes v4 makes to utilities and configuration.

## TypeScript strict mode in new projects

New TypeScript projects created with `yarn create cedar-app` have TypeScript's
`strict` mode turned on in `web/tsconfig.json`, `api/tsconfig.json` and
`scripts/tsconfig.json`. The code Cedar's generators and setup commands write
type-checks with `strict` on.

It's recommended to turn strict mode on in your existing project by adding
`"strict": true` to `compilerOptions` in each of your tsconfig files. Then run
`yarn cedar g types` and `yarn cedar type-check`. Code generated before v8 may
need a few changes; see
[TypeScript Strict Mode](https://cedarjs.com/docs/typescript/strict-mode) for
the common ones.

Strict mode relies on the types described in "More precise generated types"
below. The SDL generator also writes Prisma scalar and enum lists with
non-nullable items (`tags: [String!]!`), because Prisma lists never contain
`null`.

## More precise generated types

To better support TypeScript's strict mode a few of the types Cedar generates
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
export {}

declare global {
// Set by Cedar's vitest setup when a test file imports the database
// eslint-disable-next-line no-var
var __cedarjs_db_imported__: boolean | undefined
}

test('Cedar is correctly tracking db imports 1', () => {
// It'll currently be `undefined` here, but all that's important is that it's
// falsy
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { posts } from 'src/services/posts/posts.js'

declare global {
// Set by Cedar's vitest setup when a test file imports the database
// eslint-disable-next-line no-var
var __cedarjs_db_imported__: boolean | undefined
}

test('Cedar is correctly tracking db imports 2', async () => {
const allPosts = await posts()
expect(Array.isArray(allPosts)).toEqual(true)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
export {}

declare global {
// Set by Cedar's vitest setup when a test file imports the database
// eslint-disable-next-line no-var
var __cedarjs_db_imported__: boolean | undefined
}

test('Cedar is correctly tracking db imports 3', () => {
// The previous test (2-db-import.test.ts) imported the database but this one
// doesn't, so __cedarjs_db_imported__ should be falsy again
Expand Down
2 changes: 1 addition & 1 deletion __fixtures__/test-project/api/src/functions/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ export const handler = async (
email: username,
hashedPassword: hashedPassword,
salt: salt,
fullName: userAttributes['full-name'],
fullName: userAttributes?.['full-name'] ?? '',
},
})
},
Expand Down
2 changes: 1 addition & 1 deletion __fixtures__/test-project/api/src/graphql/contacts.sdl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export const schema = gql`
}

type Mutation {
createContact(input: CreateContactInput!): Contact @skipAuth
createContact(input: CreateContactInput!): Contact! @skipAuth
updateContact(id: Int!, input: UpdateContactInput!): Contact! @requireAuth
deleteContact(id: Int!): Contact! @requireAuth(roles: ["ADMIN"])
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,14 @@ export const updateContact: MutationResolvers['updateContact'] = ({
input,
}) => {
return db.contact.update({
data: input,
data: {
...input,
// Every field in the GraphQL input is nullable, but Prisma doesn't
// accept null for required fields. undefined leaves the field unchanged
name: input.name ?? undefined,
email: input.email ?? undefined,
message: input.message ?? undefined,
},
where: { id },
})
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ describeScenario<StandardScenario>('contacts', (getScenario) => {
},
})

expect(contactCreatedInAboveTest.message).toEqual(
expect(contactCreatedInAboveTest?.message).toEqual(
'Describe scenario works!'
)
})
Expand Down
11 changes: 9 additions & 2 deletions __fixtures__/test-project/api/src/services/posts/posts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,14 @@ export const createPost: MutationResolvers['createPost'] = ({ input }) => {

export const updatePost: MutationResolvers['updatePost'] = ({ id, input }) => {
return db.post.update({
data: input,
data: {
...input,
// Every field in the GraphQL input is nullable, but Prisma doesn't
// accept null for required fields. undefined leaves the field unchanged
title: input.title ?? undefined,
body: input.body ?? undefined,
authorId: input.authorId ?? undefined,
},
where: { id },
})
}
Expand All @@ -42,6 +49,6 @@ export const deletePost: MutationResolvers['deletePost'] = ({ id }) => {

export const Post: PostRelationResolvers = {
author: (_obj, { root }) => {
return db.post.findUnique({ where: { id: root?.id } }).author()
return db.post.findUniqueOrThrow({ where: { id: root.id } }).author()
},
}
2 changes: 1 addition & 1 deletion __fixtures__/test-project/api/src/services/users/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@ export const user: QueryResolvers['user'] = ({ id }) => {

export const User: UserRelationResolvers = {
posts: (_obj, { root }) => {
return db.user.findUnique({ where: { id: root?.id } }).posts()
return db.user.findUniqueOrThrow({ where: { id: root.id } }).posts()
},
}
1 change: 1 addition & 0 deletions __fixtures__/test-project/api/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"compilerOptions": {
"noEmit": true,
"allowJs": true,
"strict": true,
"esModuleInterop": true,
"target": "es2023",
"module": "ESNext",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"target": "es2023",
"module": "node20",
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true,
"baseUrl": ".",
"rootDir": "src",
Expand Down
1 change: 1 addition & 0 deletions __fixtures__/test-project/scripts/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"compilerOptions": {
"noEmit": true,
"allowJs": true,
"strict": true,
"esModuleInterop": true,
"target": "es2023",
"module": "node20",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { EditContactById, UpdateContactInput } from 'types/graphql'
import type { EditContactById, CreateContactInput } from 'types/graphql'

import type { RWGqlError } from '@cedarjs/forms'
import {
Expand All @@ -14,14 +14,14 @@ type FormContact = NonNullable<EditContactById['contact']>

interface ContactFormProps {
contact?: EditContactById['contact']
onSave: (data: UpdateContactInput, id?: FormContact['id']) => void
error: RWGqlError
onSave: (data: CreateContactInput) => void
error?: RWGqlError
loading: boolean
}

const ContactForm = (props: ContactFormProps) => {
const onSubmit = (data: FormContact) => {
props.onSave(data, props?.contact?.id)
props.onSave(data)
}

return (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type {
EditContactById,
UpdateContactInput,
UpdateContactMutation,
UpdateContactMutationVariables,
} from 'types/graphql'

Expand Down Expand Up @@ -28,7 +29,7 @@ export const QUERY: TypedDocumentNode<EditContactById> = gql`
`

const UPDATE_CONTACT_MUTATION: TypedDocumentNode<
EditContactById,
UpdateContactMutation,
UpdateContactMutationVariables
> = gql`
mutation UpdateContactMutation($id: Int!, $input: UpdateContactInput!) {
Expand Down Expand Up @@ -62,18 +63,15 @@ export const Success = ({ contact }: CellSuccessProps<EditContactById>) => {
}
)

const onSave = (
input: UpdateContactInput,
id: EditContactById['contact']['id']
) => {
updateContact({ variables: { id, input } })
const onSave = (input: UpdateContactInput) => {
updateContact({ variables: { id: contact.id, input } })
}

return (
<div className="rw-segment">
<header className="rw-segment-header">
<h2 className="rw-heading rw-heading-secondary">
Edit Contact {contact?.id}
Edit Contact {contact.id}
</h2>
</header>
<div className="rw-segment-main">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type {
EditPostById,
UpdatePostInput,
UpdatePostMutation,
UpdatePostMutationVariables,
} from 'types/graphql'

Expand Down Expand Up @@ -28,7 +29,7 @@ export const QUERY: TypedDocumentNode<EditPostById> = gql`
`

const UPDATE_POST_MUTATION: TypedDocumentNode<
EditPostById,
UpdatePostMutation,
UpdatePostMutationVariables
> = gql`
mutation UpdatePostMutation($id: Int!, $input: UpdatePostInput!) {
Expand Down Expand Up @@ -59,16 +60,14 @@ export const Success = ({ post }: CellSuccessProps<EditPostById>) => {
},
})

const onSave = (input: UpdatePostInput, id: EditPostById['post']['id']) => {
updatePost({ variables: { id, input } })
const onSave = (input: UpdatePostInput) => {
updatePost({ variables: { id: post.id, input } })
}

return (
<div className="rw-segment">
<header className="rw-segment-header">
<h2 className="rw-heading rw-heading-secondary">
Edit Post {post?.id}
</h2>
<h2 className="rw-heading rw-heading-secondary">Edit Post {post.id}</h2>
</header>
<div className="rw-segment-main">
<PostForm post={post} onSave={onSave} error={error} loading={loading} />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { EditPostById, UpdatePostInput } from 'types/graphql'
import type { EditPostById, CreatePostInput } from 'types/graphql'

import type { RWGqlError } from '@cedarjs/forms'
import {
Expand All @@ -14,14 +14,14 @@ type FormPost = NonNullable<EditPostById['post']>

interface PostFormProps {
post?: EditPostById['post']
onSave: (data: UpdatePostInput, id?: FormPost['id']) => void
error: RWGqlError
onSave: (data: CreatePostInput) => void
error?: RWGqlError
loading: boolean
}

const PostForm = (props: PostFormProps) => {
const onSubmit = (data: FormPost) => {
props.onSave(data, props?.post?.id)
props.onSave(data)
}

return (
Expand Down
6 changes: 3 additions & 3 deletions __fixtures__/test-project/web/src/lib/formatters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export const jsonDisplay = (obj: unknown) => {
)
}

export const truncate = (value: string | number) => {
export const truncate = (value: string | number | null | undefined) => {
let output = value?.toString() ?? ''

if (output.length > MAX_STRING_LENGTH) {
Expand Down Expand Up @@ -53,6 +53,6 @@ export function timeTag(dateTime: string | undefined | null) {
return output
}

export const checkboxInputTag = (checked: boolean) => {
return <input type="checkbox" checked={checked} disabled />
export const checkboxInputTag = (checked: boolean | null | undefined) => {
return <input type="checkbox" checked={!!checked} disabled />
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@ type Story = StoryObj<typeof AggregatedBlogPostPage>

export const Primary: Story = {
render: (args) => {
return <AggregatedBlogPostPage id={42} {...args} />
return <AggregatedBlogPostPage {...args} id={42} />
},
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@ type Story = StoryObj<typeof BlogPostPage>

export const Primary: Story = {
render: (args) => {
return (
<BlogPostPage id={'4c3d3e8e-2b1a-4f5c-8c7d-000000000042'} {...args} />
)
return <BlogPostPage {...args} id={42} />
},
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useState } from 'react'

import { validateEmail } from '@my-org/validators'
import { useForm } from 'react-hook-form'
import type { CreateContactInput } from 'types/graphql'

import {
Form,
Expand All @@ -24,7 +25,7 @@ const CREATE_CONTACT = gql`
`

const ContactUsPage = () => {
const formMethods = useForm()
const formMethods = useForm<CreateContactInput>()
const [isSubmitting, setIsSubmitting] = useState(false)
const blocker = useBlocker({
when: formMethods.formState.isDirty && !isSubmitting,
Expand All @@ -39,7 +40,7 @@ const ContactUsPage = () => {
},
})

const onSubmit = async (data) => {
const onSubmit = async (data: CreateContactInput) => {
setIsSubmitting(true)
try {
await create({ variables: { input: data } })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ const ProfilePage = () => {
return <p>Loading...</p>
}

if (!currentUser) {
return <p>Not logged in</p>
}

return (
<>
<Metadata title="Profile" description="Profile page" og />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@ type Story = StoryObj<typeof WaterfallPage>

export const Primary: Story = {
render: (args) => {
return (
<WaterfallPage id={'4c3d3e8e-2b1a-4f5c-8c7d-000000000042'} {...args} />
)
return <WaterfallPage {...args} id={42} />
},
}
1 change: 1 addition & 0 deletions __fixtures__/test-project/web/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"compilerOptions": {
"noEmit": true,
"allowJs": true,
"strict": true,
"esModuleInterop": true,
"target": "ES2022",
"module": "ESNext",
Expand Down
4 changes: 2 additions & 2 deletions docs/docs/typescript/introduction.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,10 @@ You don't need to learn all the utility types up front, but they're documented i

### 3. Cedar won't force you to type everything

The Cedar philosophy is to keep things as simple as possible at first. Cedar generates as much as possible, avoids forcing you to type every little detail, and doesn't have [strict mode](https://www.typescriptlang.org/tsconfig#strict) on by default.
The Cedar philosophy is to keep things as simple as possible at first. Cedar generates as much as possible and avoids forcing you to type every little detail.
Where needed (e.g. the [`DbAuthHandler`](/typescript/utility-types.md#dbauthhandleroptions)) you can make use of generics to be more specific with your types.

But if you're comfortable with TypeScript and want that extra level of safety, take a look at our [Strict Mode](/typescript/strict-mode.md) doc.
New projects have TypeScript's [strict mode](https://www.typescriptlang.org/tsconfig#strict) turned on, and the code Cedar generates is fully typed for it. See our [Strict Mode](/typescript/strict-mode.md) doc for how to turn it on in an existing project, and for tips on writing code in strict mode.

## A Few Useful Tips

Expand Down
Loading
Loading