@cedarjs/cli 7.0.0 → 8.0.0-canary.3263
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 +2 -1
- package/dist/commands/build.js +1 -1
- package/dist/commands/check.js +2 -1
- package/dist/commands/deploy/baremetal/baremetalHandler.js +1 -1
- package/dist/commands/deploy/baremetal.js +1 -1
- 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 +2 -1
- package/dist/commands/deploy/vercel.js +1 -1
- package/dist/commands/destroy/handlerHelpers.js +1 -1
- package/dist/commands/destroy/page/pageHandler.js +2 -1
- package/dist/commands/destroy/scaffold/scaffoldHandler.js +2 -1
- package/dist/commands/destroy/sdl/sdlHandler.js +2 -1
- package/dist/commands/dev/devHandler.js +3 -4
- package/dist/commands/dev/packageWatchCommands.js +8 -3
- package/dist/commands/dev.js +1 -1
- package/dist/commands/execHandler.js +2 -1
- 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 +2 -1
- 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 +2 -1
- package/dist/commands/experimental/setupStreamingSsr.js +1 -1
- package/dist/commands/experimental/setupStreamingSsrHandler.js +3 -2
- package/dist/commands/generate/cell/cell.js +20 -0
- package/dist/commands/generate/cell/cellHandler.js +68 -20
- package/dist/commands/generate/cell/templates/cell.tsx.template +21 -2
- package/dist/commands/generate/cell/templates/cellFragment.tsx.template +43 -0
- package/dist/commands/generate/cell/templates/cellList.tsx.template +21 -2
- package/dist/commands/generate/cell/templates/storiesFragment.tsx.template +23 -0
- package/dist/commands/generate/cell/templates/test.js.template +1 -1
- package/dist/commands/generate/cell/templates/testFragment.js.template +30 -0
- package/dist/commands/generate/dataMigration/dataMigration.js +2 -1
- package/dist/commands/generate/dbAuth/dbAuthHandler.js +23 -3
- package/dist/commands/generate/dbAuth/templates/login.oauth.tsx.template +164 -0
- package/dist/commands/generate/dbAuth/templates/signup.oauth.tsx.template +147 -0
- package/dist/commands/generate/directive/directiveHandler.js +2 -1
- package/dist/commands/generate/function/functionHandler.js +2 -1
- package/dist/commands/generate/job/jobHandler.js +2 -1
- package/dist/commands/generate/model/modelHandler.js +2 -1
- package/dist/commands/generate/ogImage/ogImageHandler.js +2 -1
- package/dist/commands/generate/package/filesTask.js +9 -12
- package/dist/commands/generate/package/packageHandler.js +3 -4
- package/dist/commands/generate/page/pageHandler.js +2 -1
- package/dist/commands/generate/realtime/realtime.js +1 -1
- package/dist/commands/generate/realtime/realtimeHandler.js +2 -9
- package/dist/commands/generate/realtime/templates/subscriptions/blank/blank.ts.template +1 -1
- package/dist/commands/generate/scaffold/scaffoldHandler.js +10 -5
- package/dist/commands/generate/scaffold/templates/components/NameForm.tsx.template +3 -3
- package/dist/commands/generate/scaffold/templates/lib/formatters.test.tsx.template +11 -0
- package/dist/commands/generate/scaffold/templates/lib/formatters.tsx.template +13 -1
- package/dist/commands/generate/script/scriptHandler.js +2 -1
- package/dist/commands/generate/sdl/sdlHandler.js +2 -1
- package/dist/commands/generate/secret/secret.js +1 -1
- package/dist/commands/generate/service/serviceHandler.js +47 -38
- package/dist/commands/generate/service/templates/service.ts.template +22 -8
- package/dist/commands/generate/service/templates/test.ts.template +1 -1
- package/dist/commands/generate/yargsCommandHelpers.js +1 -1
- package/dist/commands/generate/yargsHandlerHelpers.js +2 -1
- package/dist/commands/generate.js +1 -1
- package/dist/commands/info.js +1 -1
- package/dist/commands/lint.js +1 -1
- package/dist/commands/prerenderHandler.js +11 -4
- package/dist/commands/prismaHandler.js +7 -2
- package/dist/commands/record/init.js +1 -1
- package/dist/commands/serve.js +2 -1
- package/dist/commands/setup/auth/auth.js +2 -4
- package/dist/commands/setup/cache/cache.js +1 -1
- package/dist/commands/setup/cache/cacheHandler.js +2 -1
- package/dist/commands/setup/database/postgres.js +1 -1
- package/dist/commands/setup/database/postgresHandler.js +3 -1
- package/dist/commands/setup/deploy/providers/baremetalHandler.js +2 -1
- package/dist/commands/setup/deploy/providers/coherence.js +1 -1
- package/dist/commands/setup/deploy/providers/coherenceHandler.js +7 -6
- package/dist/commands/setup/deploy/providers/flightcontrolHandler.js +2 -1
- package/dist/commands/setup/deploy/providers/netlifyHandler.js +2 -1
- package/dist/commands/setup/deploy/providers/renderHandler.js +2 -1
- package/dist/commands/setup/deploy/providers/serverlessHandler.js +2 -1
- package/dist/commands/setup/deploy/providers/universalDeployHandler.js +4 -6
- package/dist/commands/setup/deploy/providers/vercelHandler.js +2 -1
- package/dist/commands/setup/docker/docker.js +1 -1
- package/dist/commands/setup/docker/dockerHandler.js +2 -1
- 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 +3 -5
- package/dist/commands/setup/graphql/features/trustedDocuments/trustedDocuments.js +1 -1
- package/dist/commands/setup/graphql/features/trustedDocuments/trustedDocumentsHandler.js +2 -1
- 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 +2 -1
- package/dist/commands/setup/mailer/mailer.js +1 -1
- package/dist/commands/setup/mailer/mailerHandler.js +2 -1
- package/dist/commands/setup/middleware/ogImage/ogImage.js +1 -1
- package/dist/commands/setup/middleware/ogImage/ogImageHandler.js +2 -1
- package/dist/commands/setup/monitoring/sentry/sentry.js +1 -1
- package/dist/commands/setup/monitoring/sentry/sentryHandler.js +8 -7
- package/dist/commands/setup/neon/neon.js +1 -1
- package/dist/commands/setup/neon/neonHandler.js +5 -9
- 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 +4 -16
- package/dist/commands/setup/realtime/templates/subscriptions/countdown/countdown.ts.template +1 -1
- package/dist/commands/setup/realtime/templates/subscriptions/newMessage/newMessage.ts.template +1 -1
- package/dist/commands/setup/server-file/serverFile.js +1 -1
- package/dist/commands/setup/server-file/serverFileHandler.js +2 -1
- package/dist/commands/setup/tenancy/authCodemod.js +66 -0
- package/dist/commands/setup/tenancy/dbCodemod.js +64 -0
- package/dist/commands/setup/tenancy/graphqlCodemod.js +88 -0
- package/dist/commands/setup/tenancy/preflight.js +49 -0
- package/dist/commands/setup/tenancy/schemaPrisma.js +81 -0
- package/dist/commands/setup/tenancy/signupCodemod.js +153 -0
- package/dist/commands/setup/tenancy/templates/InvitePage.tsx.template +63 -0
- package/dist/commands/setup/tenancy/templates/OrgScope.tsx.template +40 -0
- package/dist/commands/setup/tenancy/templates/ensureDefaultOrganizations.dataMigration.ts.template +39 -0
- package/dist/commands/setup/tenancy/templates/organizations.scenarios.ts.template +59 -0
- package/dist/commands/setup/tenancy/templates/organizations.sdl.ts.template +89 -0
- package/dist/commands/setup/tenancy/templates/organizations.test.ts.template +389 -0
- package/dist/commands/setup/tenancy/templates/organizations.ts.template +394 -0
- package/dist/commands/setup/tenancy/templates/requireMembership.test.ts.template +35 -0
- package/dist/commands/setup/tenancy/templates/requireMembership.ts.template +25 -0
- package/dist/commands/setup/tenancy/tenancy.js +30 -0
- package/dist/commands/setup/tenancy/tenancyHandler.js +425 -0
- package/dist/commands/setup/ui/libraries/chakra-uiHandler.js +2 -1
- package/dist/commands/setup/ui/libraries/mantineHandler.js +2 -1
- package/dist/commands/setup/ui/libraries/tailwindcssHandler.js +2 -1
- package/dist/commands/setup/uploads/schemaPrisma.js +41 -0
- package/dist/commands/setup/uploads/serverFile.js +151 -0
- package/dist/commands/setup/uploads/templates/requireUploadToken.ts.template +15 -0
- package/dist/commands/setup/uploads/templates/srcLibUploads.ts.template +23 -24
- package/dist/commands/setup/uploads/templates/uploads.sdl.ts.template +79 -0
- package/dist/commands/setup/uploads/templates/uploads.ts.template +89 -0
- package/dist/commands/setup/uploads/templates/withDataUri.ts.template +14 -0
- package/dist/commands/setup/uploads/templates/withSignedUrl.ts.template +16 -0
- package/dist/commands/setup/uploads/uploads.js +49 -7
- package/dist/commands/setup/uploads/uploadsHandler.js +256 -69
- package/dist/commands/setup.js +2 -1
- package/dist/commands/studio.js +1 -1
- package/dist/commands/test/testHandler.js +25 -71
- package/dist/commands/test/viteVersionCheck.js +75 -0
- package/dist/commands/test.js +10 -18
- 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 +2 -1
- package/dist/index.js +5 -8
- package/dist/lib/exit.js +1 -1
- package/dist/lib/index.js +6 -25
- 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 +18 -18
- package/dist/commands/setup/uploads/dbCodemod.js +0 -28
- package/dist/commands/setup/uploads/templates/signedUrl.ts.template +0 -21
- package/dist/commands/test/testHandlerEsm.js +0 -109
- package/dist/commands/testEsm.js +0 -41
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { formatCedarCommand } from "@cedarjs/cli-helpers/packageManager/display";
|
|
4
|
+
const WEB_AUTH_EXTENSIONS = ["ts", "tsx", "js", "jsx"];
|
|
5
|
+
function hasWebAuthFile(webSrcPath) {
|
|
6
|
+
return WEB_AUTH_EXTENSIONS.some(
|
|
7
|
+
(extension) => fs.existsSync(path.join(webSrcPath, `auth.${extension}`))
|
|
8
|
+
);
|
|
9
|
+
}
|
|
10
|
+
function authNotSetUpMessage() {
|
|
11
|
+
return [
|
|
12
|
+
"Auth is not set up in this project.",
|
|
13
|
+
"",
|
|
14
|
+
"Tenancy builds on your app's auth: it adds memberships to",
|
|
15
|
+
"`getCurrentUser`, and resolves the current organization from the",
|
|
16
|
+
"signed-in user's memberships. Set up auth first, then run this command",
|
|
17
|
+
"again:",
|
|
18
|
+
"",
|
|
19
|
+
` ${formatCedarCommand(["setup", "auth", "dbAuth"])}`,
|
|
20
|
+
"",
|
|
21
|
+
"Any provider works. See https://cedarjs.com/docs/authentication"
|
|
22
|
+
].join("\n");
|
|
23
|
+
}
|
|
24
|
+
function noUserModelMessage(schemaPath) {
|
|
25
|
+
return [
|
|
26
|
+
`No \`User\` model found in ${schemaPath}.`,
|
|
27
|
+
"",
|
|
28
|
+
"Tenancy hangs memberships off your app's `User` model, so it needs one",
|
|
29
|
+
"before it can add `Organization` and `Membership`.",
|
|
30
|
+
"",
|
|
31
|
+
"dbAuth adds this model for you. Providers that authenticate against an",
|
|
32
|
+
"external service leave it to you: give `User` its own id and store the",
|
|
33
|
+
"provider's id in a column beside it, so memberships keep working if you",
|
|
34
|
+
"switch providers or add a second one.",
|
|
35
|
+
"",
|
|
36
|
+
`Add a model like this one, run \`${formatCedarCommand(["prisma", "migrate", "dev"])}\`, then`,
|
|
37
|
+
"run this command again:",
|
|
38
|
+
"",
|
|
39
|
+
" model User {",
|
|
40
|
+
" id String @id @default(cuid())",
|
|
41
|
+
" email String @unique",
|
|
42
|
+
" }"
|
|
43
|
+
].join("\n");
|
|
44
|
+
}
|
|
45
|
+
export {
|
|
46
|
+
authNotSetUpMessage,
|
|
47
|
+
hasWebAuthFile,
|
|
48
|
+
noUserModelMessage
|
|
49
|
+
};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
const ORGANIZATION_MODEL = `model Organization {
|
|
2
|
+
id String @id @default(cuid())
|
|
3
|
+
name String
|
|
4
|
+
slug String @unique
|
|
5
|
+
createdAt DateTime @default(now())
|
|
6
|
+
updatedAt DateTime @updatedAt
|
|
7
|
+
memberships Membership[]
|
|
8
|
+
}`;
|
|
9
|
+
const MEMBERSHIP_MODEL = `model Membership {
|
|
10
|
+
id String @id @default(cuid())
|
|
11
|
+
role String
|
|
12
|
+
// Null while the membership is a pending invitation.
|
|
13
|
+
userId String?
|
|
14
|
+
organizationId String
|
|
15
|
+
invitedById String?
|
|
16
|
+
invitationToken String? @unique
|
|
17
|
+
createdAt DateTime @default(now())
|
|
18
|
+
updatedAt DateTime @updatedAt
|
|
19
|
+
user User? @relation(fields: [userId], references: [id])
|
|
20
|
+
organization Organization @relation(fields: [organizationId], references: [id])
|
|
21
|
+
invitedBy Membership? @relation("Inviter", fields: [invitedById], references: [id])
|
|
22
|
+
invitees Membership[] @relation("Inviter")
|
|
23
|
+
|
|
24
|
+
@@unique([userId, organizationId])
|
|
25
|
+
@@index([organizationId])
|
|
26
|
+
}`;
|
|
27
|
+
const RW_DATA_MIGRATION_MODEL = `model RW_DataMigration {
|
|
28
|
+
version String @id
|
|
29
|
+
name String
|
|
30
|
+
startedAt DateTime
|
|
31
|
+
finishedAt DateTime
|
|
32
|
+
}`;
|
|
33
|
+
function hasModel(schema, modelName) {
|
|
34
|
+
return new RegExp(`\\bmodel\\s+${modelName}\\s*\\{`).test(schema);
|
|
35
|
+
}
|
|
36
|
+
function addTenancyModels(schema) {
|
|
37
|
+
return [ORGANIZATION_MODEL, MEMBERSHIP_MODEL].reduce(appendModel, schema);
|
|
38
|
+
}
|
|
39
|
+
function appendModel(schema, model) {
|
|
40
|
+
if (schema.includes(model)) {
|
|
41
|
+
return schema;
|
|
42
|
+
}
|
|
43
|
+
return `${schema.trimEnd()}
|
|
44
|
+
|
|
45
|
+
${model}
|
|
46
|
+
`;
|
|
47
|
+
}
|
|
48
|
+
function addDataMigrationModel(schema) {
|
|
49
|
+
if (hasModel(schema, "RW_DataMigration")) {
|
|
50
|
+
return schema;
|
|
51
|
+
}
|
|
52
|
+
return `${schema.trimEnd()}
|
|
53
|
+
|
|
54
|
+
${RW_DATA_MIGRATION_MODEL}
|
|
55
|
+
`;
|
|
56
|
+
}
|
|
57
|
+
function editSchema(schema, options) {
|
|
58
|
+
if (!hasModel(schema, "User")) {
|
|
59
|
+
throw new Error("CEDAR_TENANCY_ERR_NO_USER_MODEL");
|
|
60
|
+
}
|
|
61
|
+
const organizationIsCustomized = hasModel(schema, "Organization") && !schema.includes(ORGANIZATION_MODEL);
|
|
62
|
+
const membershipIsCustomized = hasModel(schema, "Membership") && !schema.includes(MEMBERSHIP_MODEL);
|
|
63
|
+
const modelsCustomized = organizationIsCustomized || membershipIsCustomized;
|
|
64
|
+
if (modelsCustomized && !options.force) {
|
|
65
|
+
return { schema: addDataMigrationModel(schema), outcome: "skipped" };
|
|
66
|
+
}
|
|
67
|
+
const withTenancyModels = addTenancyModels(schema);
|
|
68
|
+
return {
|
|
69
|
+
schema: addDataMigrationModel(withTenancyModels),
|
|
70
|
+
outcome: modelsCustomized ? "forced" : "added"
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
export {
|
|
74
|
+
MEMBERSHIP_MODEL,
|
|
75
|
+
ORGANIZATION_MODEL,
|
|
76
|
+
RW_DATA_MIGRATION_MODEL,
|
|
77
|
+
addDataMigrationModel,
|
|
78
|
+
addTenancyModels,
|
|
79
|
+
editSchema,
|
|
80
|
+
hasModel
|
|
81
|
+
};
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import j from "jscodeshift";
|
|
2
|
+
function useShorthandUserAttributes(fn) {
|
|
3
|
+
const [firstParam] = fn.params;
|
|
4
|
+
if (!j.ObjectPattern.check(firstParam)) {
|
|
5
|
+
throw new Error("CEDAR_CODEMOD_ERR_SIGNUP_SHAPE_NOT_FOUND");
|
|
6
|
+
}
|
|
7
|
+
const userAttributesProperty = firstParam.properties.find(
|
|
8
|
+
(prop) => j.ObjectProperty.check(prop) && j.Identifier.check(prop.key) && prop.key.name === "userAttributes"
|
|
9
|
+
);
|
|
10
|
+
if (!userAttributesProperty || !j.ObjectProperty.check(userAttributesProperty)) {
|
|
11
|
+
throw new Error("CEDAR_CODEMOD_ERR_SIGNUP_SHAPE_NOT_FOUND");
|
|
12
|
+
}
|
|
13
|
+
userAttributesProperty.value = j.identifier("userAttributes");
|
|
14
|
+
userAttributesProperty.shorthand = true;
|
|
15
|
+
}
|
|
16
|
+
function addInvitationTokenToUserAttributes(root) {
|
|
17
|
+
root.find(j.TSInterfaceDeclaration, { id: { name: "UserAttributes" } }).forEach((path) => {
|
|
18
|
+
const alreadyHasField = path.node.body.body.some(
|
|
19
|
+
(member) => j.TSPropertySignature.check(member) && j.Identifier.check(member.key) && member.key.name === "invitationToken"
|
|
20
|
+
);
|
|
21
|
+
if (alreadyHasField) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
path.node.body.body.push(
|
|
25
|
+
j.tsPropertySignature(
|
|
26
|
+
j.identifier("invitationToken"),
|
|
27
|
+
j.tsTypeAnnotation(j.tsStringKeyword()),
|
|
28
|
+
true
|
|
29
|
+
// optional
|
|
30
|
+
)
|
|
31
|
+
);
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
function wrapCreateCall(fn) {
|
|
35
|
+
fn.async = true;
|
|
36
|
+
const ensureCallStatement = j.expressionStatement(
|
|
37
|
+
j.awaitExpression(
|
|
38
|
+
j.callExpression(j.identifier("ensureDefaultOrganization"), [
|
|
39
|
+
j.objectExpression([
|
|
40
|
+
j.objectProperty(
|
|
41
|
+
j.identifier("currentUser"),
|
|
42
|
+
j.objectExpression([
|
|
43
|
+
j.objectProperty(
|
|
44
|
+
j.identifier("id"),
|
|
45
|
+
j.memberExpression(j.identifier("user"), j.identifier("id"))
|
|
46
|
+
),
|
|
47
|
+
j.objectProperty(
|
|
48
|
+
j.identifier("memberships"),
|
|
49
|
+
j.arrayExpression([])
|
|
50
|
+
)
|
|
51
|
+
])
|
|
52
|
+
),
|
|
53
|
+
j.objectProperty(
|
|
54
|
+
j.identifier("invitationToken"),
|
|
55
|
+
j.memberExpression(
|
|
56
|
+
j.identifier("userAttributes"),
|
|
57
|
+
j.identifier("invitationToken")
|
|
58
|
+
)
|
|
59
|
+
)
|
|
60
|
+
])
|
|
61
|
+
])
|
|
62
|
+
)
|
|
63
|
+
);
|
|
64
|
+
const returnUserStatement = j.returnStatement(j.identifier("user"));
|
|
65
|
+
if (j.BlockStatement.check(fn.body)) {
|
|
66
|
+
const returnIndex = fn.body.body.findIndex(
|
|
67
|
+
(statement) => j.ReturnStatement.check(statement)
|
|
68
|
+
);
|
|
69
|
+
const returnStatement = fn.body.body[returnIndex];
|
|
70
|
+
if (returnIndex === -1 || !j.ReturnStatement.check(returnStatement) || !returnStatement.argument) {
|
|
71
|
+
throw new Error("CEDAR_CODEMOD_ERR_SIGNUP_SHAPE_NOT_FOUND");
|
|
72
|
+
}
|
|
73
|
+
const createUserStatement = j.variableDeclaration("const", [
|
|
74
|
+
j.variableDeclarator(
|
|
75
|
+
j.identifier("user"),
|
|
76
|
+
j.awaitExpression(returnStatement.argument)
|
|
77
|
+
)
|
|
78
|
+
]);
|
|
79
|
+
fn.body.body.splice(
|
|
80
|
+
returnIndex,
|
|
81
|
+
1,
|
|
82
|
+
createUserStatement,
|
|
83
|
+
ensureCallStatement,
|
|
84
|
+
returnUserStatement
|
|
85
|
+
);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (j.Expression.check(fn.body)) {
|
|
89
|
+
const createUserStatement = j.variableDeclaration("const", [
|
|
90
|
+
j.variableDeclarator(
|
|
91
|
+
j.identifier("user"),
|
|
92
|
+
j.awaitExpression(
|
|
93
|
+
// ast-types' `Expression` checker type isn't assignable to
|
|
94
|
+
// `awaitExpression`'s narrower `ExpressionKind` union (a gap in
|
|
95
|
+
// ast-types' generated types); the `check()` call above already
|
|
96
|
+
// guarantees `fn.body` is a valid expression node.
|
|
97
|
+
fn.body
|
|
98
|
+
)
|
|
99
|
+
)
|
|
100
|
+
]);
|
|
101
|
+
fn.body = j.blockStatement([
|
|
102
|
+
createUserStatement,
|
|
103
|
+
ensureCallStatement,
|
|
104
|
+
returnUserStatement
|
|
105
|
+
]);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
throw new Error("CEDAR_CODEMOD_ERR_SIGNUP_SHAPE_NOT_FOUND");
|
|
109
|
+
}
|
|
110
|
+
function transform(fileInfo) {
|
|
111
|
+
const root = j.withParser("tsx")(fileInfo.source);
|
|
112
|
+
const signupOptionsDeclarators = root.find(j.VariableDeclarator, {
|
|
113
|
+
id: { name: "signupOptions" }
|
|
114
|
+
});
|
|
115
|
+
if (signupOptionsDeclarators.length === 0) {
|
|
116
|
+
throw new Error("CEDAR_CODEMOD_ERR_SIGNUP_SHAPE_NOT_FOUND");
|
|
117
|
+
}
|
|
118
|
+
let handlerFound = false;
|
|
119
|
+
signupOptionsDeclarators.forEach((path) => {
|
|
120
|
+
const init = path.node.init;
|
|
121
|
+
if (!j.ObjectExpression.check(init)) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const handlerProperty = init.properties.find(
|
|
125
|
+
(prop) => j.ObjectProperty.check(prop) && j.Identifier.check(prop.key) && prop.key.name === "handler"
|
|
126
|
+
);
|
|
127
|
+
if (!handlerProperty || !j.ObjectProperty.check(handlerProperty)) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const handlerFn = handlerProperty.value;
|
|
131
|
+
if (!j.ArrowFunctionExpression.check(handlerFn) && !j.FunctionExpression.check(handlerFn)) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
useShorthandUserAttributes(handlerFn);
|
|
135
|
+
wrapCreateCall(handlerFn);
|
|
136
|
+
handlerFound = true;
|
|
137
|
+
});
|
|
138
|
+
if (!handlerFound) {
|
|
139
|
+
throw new Error("CEDAR_CODEMOD_ERR_SIGNUP_SHAPE_NOT_FOUND");
|
|
140
|
+
}
|
|
141
|
+
addInvitationTokenToUserAttributes(root);
|
|
142
|
+
const imports = root.find(j.ImportDeclaration);
|
|
143
|
+
imports.at(-1).insertAfter(
|
|
144
|
+
j.importDeclaration(
|
|
145
|
+
[j.importSpecifier(j.identifier("ensureDefaultOrganization"))],
|
|
146
|
+
j.literal("src/services/organizations/organizations")
|
|
147
|
+
)
|
|
148
|
+
);
|
|
149
|
+
return root.toSource();
|
|
150
|
+
}
|
|
151
|
+
export {
|
|
152
|
+
transform as default
|
|
153
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react'
|
|
2
|
+
|
|
3
|
+
import { navigate, routes, useParams } from '@cedarjs/router'
|
|
4
|
+
import { Metadata, useMutation } from '@cedarjs/web'
|
|
5
|
+
|
|
6
|
+
import { useAuth } from 'src/auth'
|
|
7
|
+
|
|
8
|
+
const ACCEPT_INVITATION_MUTATION = gql`
|
|
9
|
+
mutation AcceptInvitationMutation($token: String!) {
|
|
10
|
+
acceptInvitation(token: $token) {
|
|
11
|
+
id
|
|
12
|
+
slug
|
|
13
|
+
name
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
`
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Landing page for an invitation link (`/invite/{token}`). Outside
|
|
20
|
+
* `OrgScope`, since the visitor is not a member of the organization yet.
|
|
21
|
+
*/
|
|
22
|
+
const InvitePage = () => {
|
|
23
|
+
const { token } = useParams()
|
|
24
|
+
const { isAuthenticated, reauthenticate } = useAuth()
|
|
25
|
+
const [error, setError] = useState<string | null>(null)
|
|
26
|
+
|
|
27
|
+
const [acceptInvitation] = useMutation(ACCEPT_INVITATION_MUTATION, {
|
|
28
|
+
onCompleted: async (data) => {
|
|
29
|
+
await reauthenticate()
|
|
30
|
+
// TODO: point this at your app's organization home route, e.g.
|
|
31
|
+
// navigate(routes.orgHome({ orgSlug: data.acceptInvitation.slug }))
|
|
32
|
+
navigate(`/org/${data.acceptInvitation.slug}`)
|
|
33
|
+
},
|
|
34
|
+
onError: (mutationError) => setError(mutationError.message),
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
if (isAuthenticated && token) {
|
|
39
|
+
acceptInvitation({ variables: { token } })
|
|
40
|
+
}
|
|
41
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
42
|
+
}, [isAuthenticated, token])
|
|
43
|
+
|
|
44
|
+
if (!isAuthenticated) {
|
|
45
|
+
return (
|
|
46
|
+
<>
|
|
47
|
+
<Metadata title="Accept invitation" />
|
|
48
|
+
<p>Sign in or create an account to accept this invitation.</p>
|
|
49
|
+
<a href={`${routes.login()}?invitationToken=${token}`}>Log in</a>
|
|
50
|
+
<a href={`${routes.signup()}?invitationToken=${token}`}>Sign up</a>
|
|
51
|
+
</>
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<>
|
|
57
|
+
<Metadata title="Accept invitation" />
|
|
58
|
+
{error ? <p>{error}</p> : <p>Joining organization…</p>}
|
|
59
|
+
</>
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export default InvitePage
|
|
@@ -0,0 +1,40 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
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
|
+
fullName: 'Org Owner',
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
admin: {
|
|
35
|
+
data: {
|
|
36
|
+
role: 'admin',
|
|
37
|
+
organization: { connect: { slug: 'acme-inc' } },
|
|
38
|
+
user: {
|
|
39
|
+
create: {
|
|
40
|
+
email: 'admin@example.com',
|
|
41
|
+
hashedPassword: 'x',
|
|
42
|
+
salt: 'x',
|
|
43
|
+
fullName: 'Org Admin',
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
pending: {
|
|
49
|
+
data: {
|
|
50
|
+
role: 'admin',
|
|
51
|
+
invitationToken: 'invite-token',
|
|
52
|
+
organization: { connect: { slug: 'acme-inc' } },
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
export type StandardScenario = ScenarioData<Organization, 'organization', 'acme'> &
|
|
59
|
+
ScenarioData<Membership, 'membership', 'owner' | 'admin' | 'pending'>
|
|
@@ -0,0 +1,89 @@
|
|
|
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
|
+
`
|