@cedarjs/cli 7.0.1-next.467 → 7.1.0-rc.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/build/buildHandler.js +1 -2
- package/dist/commands/build.js +1 -1
- package/dist/commands/check.js +1 -2
- package/dist/commands/deploy/baremetal/baremetalHandler.js +73 -364
- package/dist/commands/deploy/baremetal.js +1 -7
- package/dist/commands/deploy/flightcontrolHandler.js +1 -1
- package/dist/commands/deploy/helpers/deployHandler.js +1 -1
- package/dist/commands/deploy/netlify.js +1 -1
- package/dist/commands/deploy/renderHandler.js +1 -1
- package/dist/commands/deploy/serverlessHandler.js +1 -2
- package/dist/commands/deploy/vercel.js +1 -1
- package/dist/commands/destroy/handlerHelpers.js +1 -1
- package/dist/commands/destroy/page/pageHandler.js +1 -2
- package/dist/commands/destroy/scaffold/scaffoldHandler.js +26 -65
- package/dist/commands/destroy/sdl/sdlHandler.js +1 -2
- package/dist/commands/dev/devHandler.js +4 -3
- package/dist/commands/dev/packageWatchCommands.js +3 -8
- package/dist/commands/dev.js +1 -1
- package/dist/commands/execHandler.js +1 -2
- package/dist/commands/experimental/live-queries/liveQueries.js +1 -1
- package/dist/commands/experimental/live-queries/liveQueriesHandler.js +3 -3
- package/dist/commands/experimental/setupInngest.js +1 -1
- package/dist/commands/experimental/setupInngestHandler.js +1 -1
- package/dist/commands/experimental/setupOpentelemetry.js +1 -1
- package/dist/commands/experimental/setupOpentelemetryHandler.js +1 -2
- package/dist/commands/experimental/setupReactCompiler.js +1 -1
- package/dist/commands/experimental/setupReactCompilerHandler.js +4 -4
- package/dist/commands/experimental/setupRsc.js +1 -1
- package/dist/commands/experimental/setupRscHandler.js +1 -2
- package/dist/commands/experimental/setupStreamingSsr.js +1 -1
- package/dist/commands/experimental/setupStreamingSsrHandler.js +2 -3
- package/dist/commands/generate/cell/cell.js +0 -20
- package/dist/commands/generate/cell/cellHandler.js +20 -68
- package/dist/commands/generate/cell/templates/cell.tsx.template +2 -21
- package/dist/commands/generate/cell/templates/cellList.tsx.template +2 -21
- package/dist/commands/generate/cell/templates/test.js.template +1 -1
- package/dist/commands/generate/dataMigration/dataMigration.js +1 -2
- package/dist/commands/generate/dbAuth/dbAuth.js +2 -6
- package/dist/commands/generate/dbAuth/dbAuthHandler.js +17 -80
- package/dist/commands/generate/dbAuth/templates/login.webAuthn.tsx.template +12 -20
- package/dist/commands/generate/directive/directiveHandler.js +1 -2
- package/dist/commands/generate/function/functionHandler.js +1 -2
- package/dist/commands/generate/helpers.js +0 -26
- package/dist/commands/generate/job/jobHandler.js +1 -2
- package/dist/commands/generate/model/modelHandler.js +1 -2
- package/dist/commands/generate/ogImage/ogImageHandler.js +1 -2
- package/dist/commands/generate/ogImage/templates/ogImage.og.tsx.template +1 -1
- package/dist/commands/generate/package/filesTask.js +12 -9
- package/dist/commands/generate/package/packageHandler.js +4 -3
- package/dist/commands/generate/package/templates/tsconfig.json.template +0 -1
- package/dist/commands/generate/page/pageHandler.js +1 -2
- package/dist/commands/generate/realtime/realtime.js +1 -1
- package/dist/commands/generate/realtime/realtimeHandler.js +9 -2
- package/dist/commands/generate/realtime/templates/liveQueries/blank/blank.service.ts.template +4 -12
- package/dist/commands/generate/realtime/templates/subscriptions/blank/blank.service.ts.template +2 -2
- package/dist/commands/generate/realtime/templates/subscriptions/blank/blank.ts.template +4 -4
- package/dist/commands/generate/scaffold/scaffold.js +1 -1
- package/dist/commands/generate/scaffold/scaffoldHandler.js +49 -93
- package/dist/commands/generate/scaffold/templates/assets/scaffold.tailwind.css.template +7 -12
- package/dist/commands/generate/scaffold/templates/components/EditNameCell.tsx.template +7 -5
- package/dist/commands/generate/scaffold/templates/components/NameForm.tsx.template +9 -10
- package/dist/commands/generate/scaffold/templates/lib/formatters.test.tsx.template +0 -11
- package/dist/commands/generate/scaffold/templates/lib/formatters.tsx.template +4 -16
- package/dist/commands/generate/script/scriptHandler.js +1 -2
- package/dist/commands/generate/script/templates/tsconfig.json.template +0 -1
- package/dist/commands/generate/sdl/sdlHandler.js +4 -10
- package/dist/commands/generate/secret/secret.js +1 -1
- package/dist/commands/generate/service/serviceHandler.js +49 -79
- package/dist/commands/generate/service/templates/service.ts.template +13 -40
- package/dist/commands/generate/service/templates/test.ts.template +6 -8
- package/dist/commands/generate/yargsCommandHelpers.js +1 -1
- package/dist/commands/generate/yargsHandlerHelpers.js +1 -2
- package/dist/commands/generate.js +1 -1
- package/dist/commands/info.js +1 -1
- package/dist/commands/jobsHandler.js +3 -5
- package/dist/commands/lint.js +1 -1
- package/dist/commands/prerenderHandler.js +4 -11
- package/dist/commands/prismaHandler.js +2 -7
- package/dist/commands/record/init.js +1 -1
- package/dist/commands/serve.js +1 -2
- package/dist/commands/setup/auth/auth.js +4 -2
- package/dist/commands/setup/cache/cache.js +1 -1
- package/dist/commands/setup/cache/cacheHandler.js +1 -2
- package/dist/commands/setup/cache/templates/memcached.ts.template +17 -14
- package/dist/commands/setup/cache/templates/redis.ts.template +20 -12
- package/dist/commands/setup/database/postgres.js +1 -1
- package/dist/commands/setup/database/postgresHandler.js +1 -3
- package/dist/commands/setup/deploy/providers/baremetal.js +0 -7
- package/dist/commands/setup/deploy/providers/baremetalHandler.js +21 -130
- package/dist/commands/setup/deploy/providers/coherence.js +1 -1
- package/dist/commands/setup/deploy/providers/coherenceHandler.js +6 -7
- package/dist/commands/setup/deploy/providers/flightcontrolHandler.js +1 -2
- package/dist/commands/setup/deploy/providers/netlifyHandler.js +1 -2
- package/dist/commands/setup/deploy/providers/renderHandler.js +1 -2
- package/dist/commands/setup/deploy/providers/serverlessHandler.js +1 -2
- package/dist/commands/setup/deploy/providers/universalDeployHandler.js +6 -4
- package/dist/commands/setup/deploy/providers/vercelHandler.js +1 -2
- package/dist/commands/setup/deploy/templates/baremetal.js +9 -119
- package/dist/commands/setup/deploy/templates/netlifyUD.js +0 -3
- package/dist/commands/setup/docker/docker.js +1 -1
- package/dist/commands/setup/docker/dockerHandler.js +1 -2
- package/dist/commands/setup/generator/generator.js +1 -1
- package/dist/commands/setup/generator/generatorHandler.js +1 -1
- package/dist/commands/setup/graphql/features/fragments/fragments.js +1 -1
- package/dist/commands/setup/graphql/features/fragments/fragmentsHandler.js +5 -3
- package/dist/commands/setup/graphql/features/trustedDocuments/trustedDocuments.js +1 -1
- package/dist/commands/setup/graphql/features/trustedDocuments/trustedDocumentsHandler.js +1 -2
- package/dist/commands/setup/i18n/i18n.js +1 -1
- package/dist/commands/setup/i18n/i18nHandler.js +1 -1
- package/dist/commands/setup/jobs/jobs.js +1 -1
- package/dist/commands/setup/jobs/jobsHandler.js +1 -4
- package/dist/commands/setup/mailer/mailer.js +1 -1
- package/dist/commands/setup/mailer/mailerHandler.js +1 -2
- package/dist/commands/setup/middleware/ogImage/ogImage.js +1 -1
- package/dist/commands/setup/middleware/ogImage/ogImageHandler.js +1 -2
- package/dist/commands/setup/monitoring/sentry/sentry.js +1 -1
- package/dist/commands/setup/monitoring/sentry/sentryHandler.js +9 -13
- package/dist/commands/setup/monitoring/sentry/templates/sentryApi.ts.template +4 -1
- package/dist/commands/setup/monitoring/sentry/templates/sentryWeb.ts.template +4 -7
- package/dist/commands/setup/neon/neon.js +1 -1
- package/dist/commands/setup/neon/neonHandler.js +9 -5
- package/dist/commands/setup/package/package.js +1 -1
- package/dist/commands/setup/package/packageHandler.js +4 -4
- package/dist/commands/setup/realtime/addRealtimeToGraphql.js +1 -1
- package/dist/commands/setup/realtime/realtime.js +1 -1
- package/dist/commands/setup/realtime/realtimeHandler.js +16 -4
- package/dist/commands/setup/realtime/templates/defer/fastAndSlowFields/fastAndSlowFields.ts.template +1 -4
- package/dist/commands/setup/realtime/templates/liveQueries/auctions/auctions.ts.template +4 -12
- package/dist/commands/setup/realtime/templates/subscriptions/countdown/countdown.ts.template +1 -1
- package/dist/commands/setup/realtime/templates/subscriptions/newMessage/newMessage.ts.template +2 -2
- package/dist/commands/setup/server-file/serverFile.js +1 -1
- package/dist/commands/setup/server-file/serverFileHandler.js +1 -2
- package/dist/commands/setup/ui/libraries/chakra-uiHandler.js +1 -2
- package/dist/commands/setup/ui/libraries/mantineHandler.js +1 -2
- package/dist/commands/setup/ui/libraries/tailwindcssHandler.js +82 -63
- package/dist/commands/setup/ui/templates/postcss.config.cjs.template +9 -0
- package/dist/commands/setup/uploads/dbCodemod.js +28 -0
- package/dist/commands/setup/uploads/templates/signedUrl.ts.template +21 -0
- package/dist/commands/setup/uploads/templates/srcLibUploads.ts.template +24 -23
- package/dist/commands/setup/uploads/uploads.js +7 -49
- package/dist/commands/setup/uploads/uploadsHandler.js +69 -277
- package/dist/commands/setup.js +1 -2
- package/dist/commands/studio.js +1 -1
- package/dist/commands/test/testHandler.js +71 -25
- package/dist/commands/test/testHandlerEsm.js +109 -0
- package/dist/commands/test.js +18 -10
- package/dist/commands/testEsm.js +41 -0
- package/dist/commands/type-checkHandler.js +1 -1
- package/dist/commands/upgrade/preUpgradeScripts.js +3 -3
- package/dist/commands/upgrade/upgrade.js +1 -1
- package/dist/commands/upgrade/upgradeHandler.js +1 -2
- package/dist/index.js +8 -5
- package/dist/lib/exit.js +1 -1
- package/dist/lib/index.js +31 -16
- package/dist/lib/loadEnvFiles.js +1 -1
- package/dist/lib/plugin.js +1 -1
- package/dist/lib/test.js +1 -1
- package/dist/lib/updateCheck.js +4 -4
- package/dist/middleware/checkNodeVersion.js +4 -4
- package/package.json +27 -27
- package/dist/commands/deploy/baremetal/monitors.js +0 -66
- package/dist/commands/generate/cell/templates/cellFragment.tsx.template +0 -43
- package/dist/commands/generate/cell/templates/storiesFragment.tsx.template +0 -23
- package/dist/commands/generate/cell/templates/testFragment.js.template +0 -30
- package/dist/commands/generate/dbAuth/templates/changePassword.tsx.template +0 -131
- package/dist/commands/generate/dbAuth/templates/login.oauth.tsx.template +0 -164
- package/dist/commands/generate/dbAuth/templates/signup.oauth.tsx.template +0 -147
- package/dist/commands/generate/scaffold/templates/layouts/NamesScaffoldLayout.tsx.template +0 -18
- package/dist/commands/setup/tenancy/authCodemod.js +0 -66
- package/dist/commands/setup/tenancy/dbCodemod.js +0 -64
- package/dist/commands/setup/tenancy/graphqlCodemod.js +0 -88
- package/dist/commands/setup/tenancy/preflight.js +0 -49
- package/dist/commands/setup/tenancy/schemaPrisma.js +0 -81
- package/dist/commands/setup/tenancy/signupCodemod.js +0 -154
- package/dist/commands/setup/tenancy/templates/InvitePage.tsx.template +0 -72
- package/dist/commands/setup/tenancy/templates/OrgScope.tsx.template +0 -40
- package/dist/commands/setup/tenancy/templates/ensureDefaultOrganizations.dataMigration.ts.template +0 -39
- package/dist/commands/setup/tenancy/templates/organizations.scenarios.ts.template +0 -57
- package/dist/commands/setup/tenancy/templates/organizations.sdl.ts.template +0 -89
- package/dist/commands/setup/tenancy/templates/organizations.test.ts.template +0 -381
- package/dist/commands/setup/tenancy/templates/organizations.ts.template +0 -394
- package/dist/commands/setup/tenancy/templates/requireMembership.test.ts.template +0 -35
- package/dist/commands/setup/tenancy/templates/requireMembership.ts.template +0 -25
- package/dist/commands/setup/tenancy/tenancy.js +0 -30
- package/dist/commands/setup/tenancy/tenancyHandler.js +0 -425
- package/dist/commands/setup/uploads/gitignore.js +0 -17
- package/dist/commands/setup/uploads/schemaPrisma.js +0 -41
- package/dist/commands/setup/uploads/serverFile.js +0 -217
- package/dist/commands/setup/uploads/templates/requireUploadToken.ts.template +0 -15
- package/dist/commands/setup/uploads/templates/uploads.sdl.ts.template +0 -79
- package/dist/commands/setup/uploads/templates/uploads.ts.template +0 -98
- package/dist/commands/setup/uploads/templates/withDataUri.ts.template +0 -14
- package/dist/commands/setup/uploads/templates/withSignedUrl.ts.template +0 -16
- package/dist/commands/test/viteVersionCheck.js +0 -75
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import { useEffect, useState } from 'react'
|
|
2
|
-
|
|
3
|
-
import type {
|
|
4
|
-
AcceptInvitationMutation,
|
|
5
|
-
AcceptInvitationMutationVariables,
|
|
6
|
-
} from 'types/graphql'
|
|
7
|
-
|
|
8
|
-
import { navigate, routes, useParams } from '@cedarjs/router'
|
|
9
|
-
import { Metadata, useMutation } from '@cedarjs/web'
|
|
10
|
-
import type { TypedDocumentNode } from '@cedarjs/web'
|
|
11
|
-
|
|
12
|
-
import { useAuth } from 'src/auth'
|
|
13
|
-
|
|
14
|
-
const ACCEPT_INVITATION_MUTATION: TypedDocumentNode<
|
|
15
|
-
AcceptInvitationMutation,
|
|
16
|
-
AcceptInvitationMutationVariables
|
|
17
|
-
> = gql`
|
|
18
|
-
mutation AcceptInvitationMutation($token: String!) {
|
|
19
|
-
acceptInvitation(token: $token) {
|
|
20
|
-
id
|
|
21
|
-
slug
|
|
22
|
-
name
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
`
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Landing page for an invitation link (`/invite/{token}`). Outside
|
|
29
|
-
* `OrgScope`, since the visitor is not a member of the organization yet.
|
|
30
|
-
*/
|
|
31
|
-
const InvitePage = () => {
|
|
32
|
-
const { token } = useParams()
|
|
33
|
-
const { isAuthenticated, reauthenticate } = useAuth()
|
|
34
|
-
const [error, setError] = useState<string | null>(null)
|
|
35
|
-
|
|
36
|
-
const [acceptInvitation] = useMutation(ACCEPT_INVITATION_MUTATION, {
|
|
37
|
-
onCompleted: async (data) => {
|
|
38
|
-
await reauthenticate()
|
|
39
|
-
// TODO: point this at your app's organization home route, e.g.
|
|
40
|
-
// navigate(routes.orgHome({ orgSlug: data.acceptInvitation.slug }))
|
|
41
|
-
navigate(`/org/${data.acceptInvitation.slug}`)
|
|
42
|
-
},
|
|
43
|
-
onError: (mutationError) => setError(mutationError.message),
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
useEffect(() => {
|
|
47
|
-
if (isAuthenticated && token) {
|
|
48
|
-
acceptInvitation({ variables: { token } })
|
|
49
|
-
}
|
|
50
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
51
|
-
}, [isAuthenticated, token])
|
|
52
|
-
|
|
53
|
-
if (!isAuthenticated) {
|
|
54
|
-
return (
|
|
55
|
-
<>
|
|
56
|
-
<Metadata title="Accept invitation" />
|
|
57
|
-
<p>Sign in or create an account to accept this invitation.</p>
|
|
58
|
-
<a href={`${routes.login()}?invitationToken=${token}`}>Log in</a>
|
|
59
|
-
<a href={`${routes.signup()}?invitationToken=${token}`}>Sign up</a>
|
|
60
|
-
</>
|
|
61
|
-
)
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
return (
|
|
65
|
-
<>
|
|
66
|
-
<Metadata title="Accept invitation" />
|
|
67
|
-
{error ? <p>{error}</p> : <p>Joining organization…</p>}
|
|
68
|
-
</>
|
|
69
|
-
)
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export default InvitePage
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import type { ReactNode } from 'react'
|
|
2
|
-
|
|
3
|
-
import { OrgScope as TenancyOrgScope } from '@cedarjs/tenancy/web'
|
|
4
|
-
|
|
5
|
-
import { useAuth } from 'src/auth'
|
|
6
|
-
|
|
7
|
-
export interface OrgScopeProps {
|
|
8
|
-
/** Overrides the `orgSlug` route param. */
|
|
9
|
-
orgSlug?: string
|
|
10
|
-
children: ReactNode
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Wraps organization routes with a per-organization Apollo client and a
|
|
15
|
-
* membership check, using this app's `useAuth`. Use with the router's
|
|
16
|
-
* `Set wrap`:
|
|
17
|
-
*
|
|
18
|
-
* <Set wrap={OrgScope}>
|
|
19
|
-
* <Route path="/org/{orgSlug}/projects" page={ProjectsPage} name="projects" />
|
|
20
|
-
* </Set>
|
|
21
|
-
*
|
|
22
|
-
* Routes under `OrgScope` must not carry `prerender`: `OrgScope`'s nested
|
|
23
|
-
* Apollo client shadows the per-route client the prerenderer injects above
|
|
24
|
-
* `App`, so a prerendered query under here would not go through it.
|
|
25
|
-
*/
|
|
26
|
-
export const OrgScope = ({ orgSlug, children }: OrgScopeProps) => {
|
|
27
|
-
return (
|
|
28
|
-
<TenancyOrgScope
|
|
29
|
-
useAuth={useAuth}
|
|
30
|
-
orgSlug={orgSlug}
|
|
31
|
-
notAMember={
|
|
32
|
-
<div>
|
|
33
|
-
<p>You don't have access to this organization.</p>
|
|
34
|
-
</div>
|
|
35
|
-
}
|
|
36
|
-
>
|
|
37
|
-
{children}
|
|
38
|
-
</TenancyOrgScope>
|
|
39
|
-
)
|
|
40
|
-
}
|
package/dist/commands/setup/tenancy/templates/ensureDefaultOrganizations.dataMigration.ts.template
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import type { PrismaClient } from 'src/lib/db'
|
|
2
|
-
import { ensureDefaultOrganization } from 'src/services/organizations/organizations'
|
|
3
|
-
|
|
4
|
-
const BATCH_SIZE = 100
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Existing users have no organization until this runs once: `setup tenancy`
|
|
8
|
-
* only wires new signups (via the GraphQL `context` function and, for dbAuth,
|
|
9
|
-
* the signup handler). Every user without a membership gets one, in batches,
|
|
10
|
-
* using the same `ensureDefaultOrganization` new signups use.
|
|
11
|
-
*
|
|
12
|
-
* Run with `__DATA_MIGRATE_UP_COMMAND__`, after
|
|
13
|
-
* `__PRISMA_MIGRATE_DEV_COMMAND__` has applied the
|
|
14
|
-
* `Organization`/`Membership` migration.
|
|
15
|
-
*/
|
|
16
|
-
export default async ({ db }: { db: PrismaClient }) => {
|
|
17
|
-
let cursor: string | undefined
|
|
18
|
-
let hasMore = true
|
|
19
|
-
|
|
20
|
-
while (hasMore) {
|
|
21
|
-
const users = await db.user.findMany({
|
|
22
|
-
take: BATCH_SIZE,
|
|
23
|
-
orderBy: { id: 'asc' },
|
|
24
|
-
...(cursor ? { skip: 1, cursor: { id: cursor } } : {}),
|
|
25
|
-
include: { memberships: { select: { id: true } } },
|
|
26
|
-
})
|
|
27
|
-
|
|
28
|
-
for (const user of users) {
|
|
29
|
-
if (user.memberships.length === 0) {
|
|
30
|
-
await ensureDefaultOrganization({
|
|
31
|
-
currentUser: { id: user.id, memberships: [] },
|
|
32
|
-
})
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
cursor = users[users.length - 1]?.id
|
|
37
|
-
hasMore = users.length === BATCH_SIZE
|
|
38
|
-
}
|
|
39
|
-
}
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import type { ScenarioData } from '@cedarjs/testing/api'
|
|
2
|
-
|
|
3
|
-
import type { Membership, Organization, Prisma } from 'src/lib/db'
|
|
4
|
-
|
|
5
|
-
// This scenario assumes a dbAuth-style `User` model (`email`,
|
|
6
|
-
// `hashedPassword`, `salt`). Adjust the `user` fixture below to match your
|
|
7
|
-
// app's actual `User` fields if you use a different auth provider.
|
|
8
|
-
export const standard = defineScenario<
|
|
9
|
-
Prisma.OrganizationCreateArgs | Prisma.MembershipCreateArgs
|
|
10
|
-
>({
|
|
11
|
-
organization: {
|
|
12
|
-
acme: {
|
|
13
|
-
data: {
|
|
14
|
-
name: 'Acme Inc',
|
|
15
|
-
slug: 'acme-inc',
|
|
16
|
-
},
|
|
17
|
-
},
|
|
18
|
-
},
|
|
19
|
-
membership: {
|
|
20
|
-
owner: {
|
|
21
|
-
data: {
|
|
22
|
-
role: 'owner',
|
|
23
|
-
organization: { connect: { slug: 'acme-inc' } },
|
|
24
|
-
user: {
|
|
25
|
-
create: {
|
|
26
|
-
email: 'owner@example.com',
|
|
27
|
-
hashedPassword: 'x',
|
|
28
|
-
salt: 'x',
|
|
29
|
-
},
|
|
30
|
-
},
|
|
31
|
-
},
|
|
32
|
-
},
|
|
33
|
-
admin: {
|
|
34
|
-
data: {
|
|
35
|
-
role: 'admin',
|
|
36
|
-
organization: { connect: { slug: 'acme-inc' } },
|
|
37
|
-
user: {
|
|
38
|
-
create: {
|
|
39
|
-
email: 'admin@example.com',
|
|
40
|
-
hashedPassword: 'x',
|
|
41
|
-
salt: 'x',
|
|
42
|
-
},
|
|
43
|
-
},
|
|
44
|
-
},
|
|
45
|
-
},
|
|
46
|
-
pending: {
|
|
47
|
-
data: {
|
|
48
|
-
role: 'admin',
|
|
49
|
-
invitationToken: 'invite-token',
|
|
50
|
-
organization: { connect: { slug: 'acme-inc' } },
|
|
51
|
-
},
|
|
52
|
-
},
|
|
53
|
-
},
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
export type StandardScenario = ScenarioData<Organization, 'organization', 'acme'> &
|
|
57
|
-
ScenarioData<Membership, 'membership', 'owner' | 'admin' | 'pending'>
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
export const schema = gql`
|
|
2
|
-
type Organization {
|
|
3
|
-
id: String!
|
|
4
|
-
name: String!
|
|
5
|
-
slug: String!
|
|
6
|
-
createdAt: DateTime!
|
|
7
|
-
updatedAt: DateTime!
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
type Membership {
|
|
11
|
-
id: String!
|
|
12
|
-
role: String!
|
|
13
|
-
userId: String
|
|
14
|
-
organizationId: String!
|
|
15
|
-
invitedById: String
|
|
16
|
-
createdAt: DateTime!
|
|
17
|
-
updatedAt: DateTime!
|
|
18
|
-
organization: Organization!
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
"""
|
|
22
|
-
inviteMember's result. invitationToken is deliberately not a Membership
|
|
23
|
-
field: Membership is readable by any member (myMemberships, removeMember),
|
|
24
|
-
and a selectable invitationToken there would let any member read a
|
|
25
|
-
pending invitation's token and claim it themselves. Invitation exists only
|
|
26
|
-
as inviteMember's return type, so the token reaches only the caller who
|
|
27
|
-
just created the invitation -- the one the generated flow expects to email
|
|
28
|
-
it onward.
|
|
29
|
-
"""
|
|
30
|
-
type Invitation {
|
|
31
|
-
membership: Membership!
|
|
32
|
-
invitationToken: String!
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
input CreateOrganizationInput {
|
|
36
|
-
name: String!
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
input InviteMemberInput {
|
|
40
|
-
email: String!
|
|
41
|
-
role: String!
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
type Query {
|
|
45
|
-
"Every membership belonging to the signed-in user, across all organizations they belong to."
|
|
46
|
-
myMemberships: [Membership!]! @requireAuth
|
|
47
|
-
|
|
48
|
-
"The current organization, resolved by resolveCurrentOrg for this request."
|
|
49
|
-
organization(id: String!): Organization @requireMembership
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
type Mutation {
|
|
53
|
-
"Creates a new organization with the caller as its owner."
|
|
54
|
-
createOrganization(input: CreateOrganizationInput!): Organization!
|
|
55
|
-
@requireAuth
|
|
56
|
-
|
|
57
|
-
"""
|
|
58
|
-
Creates a pending membership (an invitation) in the current organization,
|
|
59
|
-
for a role the caller's own role may grant (see ROLE_GRANTS in
|
|
60
|
-
organizations.ts) -- an admin can invite an admin or a member, but not an
|
|
61
|
-
owner. The app is responsible for emailing input.email a link built from
|
|
62
|
-
the returned invitationToken; Membership has no email column of its own.
|
|
63
|
-
Web callers must call reauthenticate() from useAuth() in onCompleted so
|
|
64
|
-
the invite shows up if the inviter is also the invitee.
|
|
65
|
-
"""
|
|
66
|
-
inviteMember(input: InviteMemberInput!): Invitation!
|
|
67
|
-
@requireMembership(roles: ["owner", "admin"])
|
|
68
|
-
|
|
69
|
-
"""
|
|
70
|
-
Attaches the pending membership matching token to the signed-in user, or
|
|
71
|
-
is a no-op if it is already attached to them. Throws if the token is
|
|
72
|
-
invalid or was already claimed by someone else.
|
|
73
|
-
Web callers must call reauthenticate() from useAuth() in onCompleted so
|
|
74
|
-
the new membership is reflected in currentUser.memberships.
|
|
75
|
-
"""
|
|
76
|
-
acceptInvitation(token: String!): Organization! @requireAuth
|
|
77
|
-
|
|
78
|
-
"""
|
|
79
|
-
Removes a membership from the current organization, for a role the
|
|
80
|
-
caller's own role may grant (the same rule inviteMember uses) -- an
|
|
81
|
-
admin can never remove an owner, and an organization's last owner can
|
|
82
|
-
never be removed.
|
|
83
|
-
Web callers must call reauthenticate() from useAuth() in onCompleted when
|
|
84
|
-
removing their own membership.
|
|
85
|
-
"""
|
|
86
|
-
removeMember(membershipId: String!): Membership!
|
|
87
|
-
@requireMembership(roles: ["owner", "admin"])
|
|
88
|
-
}
|
|
89
|
-
`
|
|
@@ -1,381 +0,0 @@
|
|
|
1
|
-
import { setContext } from '@cedarjs/context'
|
|
2
|
-
|
|
3
|
-
import { db } from 'src/lib/db'
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
myMemberships,
|
|
7
|
-
organization,
|
|
8
|
-
inviteMember,
|
|
9
|
-
acceptInvitation,
|
|
10
|
-
removeMember,
|
|
11
|
-
ensureDefaultOrganization,
|
|
12
|
-
} from './organizations.js'
|
|
13
|
-
import type { StandardScenario } from './organizations.scenarios.js'
|
|
14
|
-
|
|
15
|
-
// Generated boilerplate tests do not account for all circumstances and can
|
|
16
|
-
// fail without adjustments, e.g. a `User` model shaped by a different auth
|
|
17
|
-
// provider than the dbAuth one this scenario assumes. Please refer to the
|
|
18
|
-
// CedarJS Testing Docs:
|
|
19
|
-
// https://cedarjs.com/docs/testing#testing-services
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* `context.currentUser` is typed from this app's `getCurrentUser`, which
|
|
23
|
-
* selects more fields than these tests need.
|
|
24
|
-
*/
|
|
25
|
-
const asCurrentUser = (user: { id: string; memberships?: unknown[] }) =>
|
|
26
|
-
user as unknown as typeof context.currentUser
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Sets `context.currentUser` and `context.currentOrg` the way the GraphQL
|
|
30
|
-
* `context` function does for an authenticated, in-organization request.
|
|
31
|
-
*/
|
|
32
|
-
const asMember = (
|
|
33
|
-
scenario: StandardScenario,
|
|
34
|
-
membership: { id: string; userId: string | null; role: string },
|
|
35
|
-
) => {
|
|
36
|
-
setContext({
|
|
37
|
-
currentUser: asCurrentUser({ id: membership.userId! }),
|
|
38
|
-
currentOrg: {
|
|
39
|
-
id: scenario.organization.acme.id,
|
|
40
|
-
slug: scenario.organization.acme.slug,
|
|
41
|
-
role: membership.role,
|
|
42
|
-
membershipId: membership.id,
|
|
43
|
-
},
|
|
44
|
-
})
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
describe('organizations service', () => {
|
|
48
|
-
scenario(
|
|
49
|
-
"myMemberships returns the signed-in user's memberships",
|
|
50
|
-
async (scenario: StandardScenario) => {
|
|
51
|
-
setContext({
|
|
52
|
-
currentUser: asCurrentUser({ id: scenario.membership.owner.userId! }),
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
const result = await myMemberships()
|
|
56
|
-
|
|
57
|
-
expect(result).toHaveLength(1)
|
|
58
|
-
expect(result[0]).toMatchObject({ id: scenario.membership.owner.id })
|
|
59
|
-
},
|
|
60
|
-
)
|
|
61
|
-
|
|
62
|
-
scenario(
|
|
63
|
-
'organization returns the current organization',
|
|
64
|
-
async (scenario: StandardScenario) => {
|
|
65
|
-
asMember(scenario, scenario.membership.owner)
|
|
66
|
-
|
|
67
|
-
const result = await organization({ id: scenario.organization.acme.id })
|
|
68
|
-
|
|
69
|
-
expect(result?.id).toEqual(scenario.organization.acme.id)
|
|
70
|
-
},
|
|
71
|
-
)
|
|
72
|
-
|
|
73
|
-
scenario(
|
|
74
|
-
'organization refuses an id other than the current organization',
|
|
75
|
-
async (scenario: StandardScenario) => {
|
|
76
|
-
asMember(scenario, scenario.membership.owner)
|
|
77
|
-
|
|
78
|
-
await expect(organization({ id: 'some-other-org' })).rejects.toThrow()
|
|
79
|
-
},
|
|
80
|
-
)
|
|
81
|
-
|
|
82
|
-
describe('inviteMember', () => {
|
|
83
|
-
scenario(
|
|
84
|
-
'an owner can invite a member',
|
|
85
|
-
async (scenario: StandardScenario) => {
|
|
86
|
-
asMember(scenario, scenario.membership.owner)
|
|
87
|
-
|
|
88
|
-
const result = await inviteMember({
|
|
89
|
-
input: { email: 'new-member@example.com', role: 'member' },
|
|
90
|
-
})
|
|
91
|
-
|
|
92
|
-
expect(result.invitationToken).toBeTruthy()
|
|
93
|
-
|
|
94
|
-
// `Invitation.membership` is resolver-backed like any relation
|
|
95
|
-
// field, so its generated type allows a promise; read the row back
|
|
96
|
-
// from `db` instead of asserting on `result.membership` directly.
|
|
97
|
-
const created = await db.membership.findFirst({
|
|
98
|
-
where: { invitedById: scenario.membership.owner.id },
|
|
99
|
-
})
|
|
100
|
-
|
|
101
|
-
expect(created?.role).toEqual('member')
|
|
102
|
-
expect(created?.invitationToken).toEqual(result.invitationToken)
|
|
103
|
-
},
|
|
104
|
-
)
|
|
105
|
-
|
|
106
|
-
scenario(
|
|
107
|
-
'an admin cannot invite an owner',
|
|
108
|
-
async (scenario: StandardScenario) => {
|
|
109
|
-
asMember(scenario, scenario.membership.admin)
|
|
110
|
-
|
|
111
|
-
await expect(
|
|
112
|
-
inviteMember({
|
|
113
|
-
input: { email: 'new-owner@example.com', role: 'owner' },
|
|
114
|
-
}),
|
|
115
|
-
).rejects.toThrow()
|
|
116
|
-
},
|
|
117
|
-
)
|
|
118
|
-
|
|
119
|
-
scenario(
|
|
120
|
-
'an unknown role is rejected',
|
|
121
|
-
async (scenario: StandardScenario) => {
|
|
122
|
-
asMember(scenario, scenario.membership.owner)
|
|
123
|
-
|
|
124
|
-
await expect(
|
|
125
|
-
inviteMember({
|
|
126
|
-
input: { email: 'typo@example.com', role: 'superadmin' },
|
|
127
|
-
}),
|
|
128
|
-
).rejects.toThrow()
|
|
129
|
-
},
|
|
130
|
-
)
|
|
131
|
-
})
|
|
132
|
-
|
|
133
|
-
describe('removeMember', () => {
|
|
134
|
-
scenario(
|
|
135
|
-
'an owner can remove an admin',
|
|
136
|
-
async (scenario: StandardScenario) => {
|
|
137
|
-
asMember(scenario, scenario.membership.owner)
|
|
138
|
-
|
|
139
|
-
await removeMember({ membershipId: scenario.membership.admin.id })
|
|
140
|
-
|
|
141
|
-
const remaining = await db.membership.findUnique({
|
|
142
|
-
where: { id: scenario.membership.admin.id },
|
|
143
|
-
})
|
|
144
|
-
|
|
145
|
-
expect(remaining).toBeNull()
|
|
146
|
-
},
|
|
147
|
-
)
|
|
148
|
-
|
|
149
|
-
scenario(
|
|
150
|
-
'an admin cannot remove an owner',
|
|
151
|
-
async (scenario: StandardScenario) => {
|
|
152
|
-
asMember(scenario, scenario.membership.admin)
|
|
153
|
-
|
|
154
|
-
await expect(
|
|
155
|
-
removeMember({ membershipId: scenario.membership.owner.id }),
|
|
156
|
-
).rejects.toThrow()
|
|
157
|
-
},
|
|
158
|
-
)
|
|
159
|
-
|
|
160
|
-
scenario(
|
|
161
|
-
"the organization's last owner cannot be removed",
|
|
162
|
-
async (scenario: StandardScenario) => {
|
|
163
|
-
asMember(scenario, scenario.membership.owner)
|
|
164
|
-
|
|
165
|
-
await expect(
|
|
166
|
-
removeMember({ membershipId: scenario.membership.owner.id }),
|
|
167
|
-
).rejects.toThrow()
|
|
168
|
-
},
|
|
169
|
-
)
|
|
170
|
-
|
|
171
|
-
scenario(
|
|
172
|
-
'an unclaimed owner invitation does not count as the remaining owner',
|
|
173
|
-
async (scenario: StandardScenario) => {
|
|
174
|
-
asMember(scenario, scenario.membership.owner)
|
|
175
|
-
|
|
176
|
-
// Nobody has accepted this one yet, so it leaves the organization
|
|
177
|
-
// with no owner who can administer it.
|
|
178
|
-
await db.membership.create({
|
|
179
|
-
data: {
|
|
180
|
-
role: 'owner',
|
|
181
|
-
organizationId: scenario.organization.acme.id,
|
|
182
|
-
invitationToken: 'pending-owner-token',
|
|
183
|
-
},
|
|
184
|
-
})
|
|
185
|
-
|
|
186
|
-
await expect(
|
|
187
|
-
removeMember({ membershipId: scenario.membership.owner.id }),
|
|
188
|
-
).rejects.toThrow()
|
|
189
|
-
},
|
|
190
|
-
)
|
|
191
|
-
|
|
192
|
-
scenario(
|
|
193
|
-
'an owner can be removed once another owner has accepted',
|
|
194
|
-
async (scenario: StandardScenario) => {
|
|
195
|
-
asMember(scenario, scenario.membership.owner)
|
|
196
|
-
|
|
197
|
-
const second = await db.user.create({
|
|
198
|
-
data: {
|
|
199
|
-
email: 'second-owner@example.com',
|
|
200
|
-
hashedPassword: 'x',
|
|
201
|
-
salt: 'x',
|
|
202
|
-
},
|
|
203
|
-
})
|
|
204
|
-
await db.membership.create({
|
|
205
|
-
data: {
|
|
206
|
-
role: 'owner',
|
|
207
|
-
organizationId: scenario.organization.acme.id,
|
|
208
|
-
userId: second.id,
|
|
209
|
-
},
|
|
210
|
-
})
|
|
211
|
-
|
|
212
|
-
const result = await removeMember({
|
|
213
|
-
membershipId: scenario.membership.owner.id,
|
|
214
|
-
})
|
|
215
|
-
|
|
216
|
-
expect(result.id).toEqual(scenario.membership.owner.id)
|
|
217
|
-
await expect(
|
|
218
|
-
db.membership.findUnique({
|
|
219
|
-
where: { id: scenario.membership.owner.id },
|
|
220
|
-
}),
|
|
221
|
-
).resolves.toBeNull()
|
|
222
|
-
},
|
|
223
|
-
)
|
|
224
|
-
})
|
|
225
|
-
|
|
226
|
-
describe('acceptInvitation', () => {
|
|
227
|
-
scenario(
|
|
228
|
-
'attaches a pending invitation to the signed-in user',
|
|
229
|
-
async (scenario: StandardScenario) => {
|
|
230
|
-
const invitee = await db.user.create({
|
|
231
|
-
data: {
|
|
232
|
-
email: 'invitee@example.com',
|
|
233
|
-
hashedPassword: 'x',
|
|
234
|
-
salt: 'x',
|
|
235
|
-
},
|
|
236
|
-
})
|
|
237
|
-
|
|
238
|
-
setContext({ currentUser: asCurrentUser({ id: invitee.id }) })
|
|
239
|
-
|
|
240
|
-
const result = await acceptInvitation({
|
|
241
|
-
token: scenario.membership.pending.invitationToken!,
|
|
242
|
-
})
|
|
243
|
-
|
|
244
|
-
expect(result.id).toEqual(scenario.organization.acme.id)
|
|
245
|
-
},
|
|
246
|
-
)
|
|
247
|
-
|
|
248
|
-
scenario(
|
|
249
|
-
'a repeat call from the same user still succeeds',
|
|
250
|
-
async (scenario: StandardScenario) => {
|
|
251
|
-
const invitee = await db.user.create({
|
|
252
|
-
data: {
|
|
253
|
-
email: 'invitee@example.com',
|
|
254
|
-
hashedPassword: 'x',
|
|
255
|
-
salt: 'x',
|
|
256
|
-
},
|
|
257
|
-
})
|
|
258
|
-
|
|
259
|
-
setContext({ currentUser: asCurrentUser({ id: invitee.id }) })
|
|
260
|
-
|
|
261
|
-
const token = scenario.membership.pending.invitationToken!
|
|
262
|
-
|
|
263
|
-
const first = await acceptInvitation({ token })
|
|
264
|
-
const second = await acceptInvitation({ token })
|
|
265
|
-
|
|
266
|
-
expect(second.id).toEqual(first.id)
|
|
267
|
-
},
|
|
268
|
-
)
|
|
269
|
-
|
|
270
|
-
scenario(
|
|
271
|
-
'a second user cannot claim an already-claimed token',
|
|
272
|
-
async (scenario: StandardScenario) => {
|
|
273
|
-
const firstUser = await db.user.create({
|
|
274
|
-
data: {
|
|
275
|
-
email: 'first@example.com',
|
|
276
|
-
hashedPassword: 'x',
|
|
277
|
-
salt: 'x',
|
|
278
|
-
},
|
|
279
|
-
})
|
|
280
|
-
const secondUser = await db.user.create({
|
|
281
|
-
data: {
|
|
282
|
-
email: 'second@example.com',
|
|
283
|
-
hashedPassword: 'x',
|
|
284
|
-
salt: 'x',
|
|
285
|
-
},
|
|
286
|
-
})
|
|
287
|
-
|
|
288
|
-
const token = scenario.membership.pending.invitationToken!
|
|
289
|
-
|
|
290
|
-
setContext({ currentUser: asCurrentUser({ id: firstUser.id }) })
|
|
291
|
-
await acceptInvitation({ token })
|
|
292
|
-
|
|
293
|
-
setContext({ currentUser: asCurrentUser({ id: secondUser.id }) })
|
|
294
|
-
await expect(acceptInvitation({ token })).rejects.toThrow()
|
|
295
|
-
},
|
|
296
|
-
)
|
|
297
|
-
|
|
298
|
-
scenario(
|
|
299
|
-
'clears the invitation token once claimed',
|
|
300
|
-
async (scenario: StandardScenario) => {
|
|
301
|
-
const invitee = await db.user.create({
|
|
302
|
-
data: {
|
|
303
|
-
email: 'invitee@example.com',
|
|
304
|
-
hashedPassword: 'x',
|
|
305
|
-
salt: 'x',
|
|
306
|
-
},
|
|
307
|
-
})
|
|
308
|
-
|
|
309
|
-
const token = scenario.membership.pending.invitationToken!
|
|
310
|
-
|
|
311
|
-
setContext({ currentUser: asCurrentUser({ id: invitee.id }) })
|
|
312
|
-
await acceptInvitation({ token })
|
|
313
|
-
|
|
314
|
-
const claimed = await db.membership.findUnique({
|
|
315
|
-
where: { id: scenario.membership.pending.id },
|
|
316
|
-
})
|
|
317
|
-
|
|
318
|
-
expect(claimed?.userId).toEqual(invitee.id)
|
|
319
|
-
expect(claimed?.invitationToken).not.toEqual(token)
|
|
320
|
-
},
|
|
321
|
-
)
|
|
322
|
-
|
|
323
|
-
it('rejects a token that never matched a pending invitation', async () => {
|
|
324
|
-
setContext({ currentUser: asCurrentUser({ id: 'some-user-id' }) })
|
|
325
|
-
|
|
326
|
-
await expect(
|
|
327
|
-
acceptInvitation({ token: 'not-a-real-token' }),
|
|
328
|
-
).rejects.toThrow()
|
|
329
|
-
})
|
|
330
|
-
})
|
|
331
|
-
|
|
332
|
-
it('ensureDefaultOrganization is idempotent when the user already has memberships', async () => {
|
|
333
|
-
const existingMemberships = [
|
|
334
|
-
{ id: 'membership-1', organizationId: 'org-1', role: 'owner' },
|
|
335
|
-
]
|
|
336
|
-
|
|
337
|
-
const result = await ensureDefaultOrganization({
|
|
338
|
-
currentUser: { id: 'user-1', memberships: existingMemberships },
|
|
339
|
-
})
|
|
340
|
-
|
|
341
|
-
expect(result).toBe(existingMemberships)
|
|
342
|
-
})
|
|
343
|
-
|
|
344
|
-
scenario(
|
|
345
|
-
'ensureDefaultOrganization attaches a pending invitation',
|
|
346
|
-
async (scenario: StandardScenario) => {
|
|
347
|
-
const invitee = await db.user.create({
|
|
348
|
-
data: {
|
|
349
|
-
email: 'invitee@example.com',
|
|
350
|
-
hashedPassword: 'x',
|
|
351
|
-
salt: 'x',
|
|
352
|
-
},
|
|
353
|
-
})
|
|
354
|
-
|
|
355
|
-
const result = await ensureDefaultOrganization({
|
|
356
|
-
currentUser: { id: invitee.id, memberships: [] },
|
|
357
|
-
invitationToken: scenario.membership.pending.invitationToken,
|
|
358
|
-
})
|
|
359
|
-
|
|
360
|
-
expect(result).toHaveLength(1)
|
|
361
|
-
expect(result[0].organizationId).toEqual(scenario.organization.acme.id)
|
|
362
|
-
},
|
|
363
|
-
)
|
|
364
|
-
|
|
365
|
-
it('ensureDefaultOrganization creates a new organization when there is no invitation', async () => {
|
|
366
|
-
const newUser = await db.user.create({
|
|
367
|
-
data: {
|
|
368
|
-
email: 'brand-new@example.com',
|
|
369
|
-
hashedPassword: 'x',
|
|
370
|
-
salt: 'x',
|
|
371
|
-
},
|
|
372
|
-
})
|
|
373
|
-
|
|
374
|
-
const result = await ensureDefaultOrganization({
|
|
375
|
-
currentUser: { id: newUser.id, memberships: [] },
|
|
376
|
-
})
|
|
377
|
-
|
|
378
|
-
expect(result).toHaveLength(1)
|
|
379
|
-
expect(result[0].role).toEqual('owner')
|
|
380
|
-
})
|
|
381
|
-
})
|