@cedarjs/cli 7.1.0 → 7.1.1-next.468

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (194) hide show
  1. package/dist/commands/build/buildHandler.js +2 -1
  2. package/dist/commands/build.js +1 -1
  3. package/dist/commands/check.js +2 -1
  4. package/dist/commands/deploy/baremetal/baremetalHandler.js +364 -73
  5. package/dist/commands/deploy/baremetal/monitors.js +66 -0
  6. package/dist/commands/deploy/baremetal.js +7 -1
  7. package/dist/commands/deploy/flightcontrolHandler.js +1 -1
  8. package/dist/commands/deploy/helpers/deployHandler.js +1 -1
  9. package/dist/commands/deploy/netlify.js +1 -1
  10. package/dist/commands/deploy/renderHandler.js +1 -1
  11. package/dist/commands/deploy/serverlessHandler.js +2 -1
  12. package/dist/commands/deploy/vercel.js +1 -1
  13. package/dist/commands/destroy/handlerHelpers.js +1 -1
  14. package/dist/commands/destroy/page/pageHandler.js +2 -1
  15. package/dist/commands/destroy/scaffold/scaffoldHandler.js +65 -26
  16. package/dist/commands/destroy/sdl/sdlHandler.js +2 -1
  17. package/dist/commands/dev/devHandler.js +3 -4
  18. package/dist/commands/dev/packageWatchCommands.js +8 -3
  19. package/dist/commands/dev.js +1 -1
  20. package/dist/commands/execHandler.js +2 -1
  21. package/dist/commands/experimental/live-queries/liveQueries.js +1 -1
  22. package/dist/commands/experimental/live-queries/liveQueriesHandler.js +3 -3
  23. package/dist/commands/experimental/setupInngest.js +1 -1
  24. package/dist/commands/experimental/setupInngestHandler.js +1 -1
  25. package/dist/commands/experimental/setupOpentelemetry.js +1 -1
  26. package/dist/commands/experimental/setupOpentelemetryHandler.js +2 -1
  27. package/dist/commands/experimental/setupReactCompiler.js +1 -1
  28. package/dist/commands/experimental/setupReactCompilerHandler.js +4 -4
  29. package/dist/commands/experimental/setupRsc.js +1 -1
  30. package/dist/commands/experimental/setupRscHandler.js +2 -1
  31. package/dist/commands/experimental/setupStreamingSsr.js +1 -1
  32. package/dist/commands/experimental/setupStreamingSsrHandler.js +3 -2
  33. package/dist/commands/generate/cell/cell.js +20 -0
  34. package/dist/commands/generate/cell/cellHandler.js +68 -20
  35. package/dist/commands/generate/cell/templates/cell.tsx.template +21 -2
  36. package/dist/commands/generate/cell/templates/cellFragment.tsx.template +43 -0
  37. package/dist/commands/generate/cell/templates/cellList.tsx.template +21 -2
  38. package/dist/commands/generate/cell/templates/storiesFragment.tsx.template +23 -0
  39. package/dist/commands/generate/cell/templates/test.js.template +1 -1
  40. package/dist/commands/generate/cell/templates/testFragment.js.template +30 -0
  41. package/dist/commands/generate/dataMigration/dataMigration.js +2 -1
  42. package/dist/commands/generate/dbAuth/dbAuth.js +6 -2
  43. package/dist/commands/generate/dbAuth/dbAuthHandler.js +80 -17
  44. package/dist/commands/generate/dbAuth/templates/changePassword.tsx.template +131 -0
  45. package/dist/commands/generate/dbAuth/templates/login.oauth.tsx.template +164 -0
  46. package/dist/commands/generate/dbAuth/templates/login.webAuthn.tsx.template +20 -12
  47. package/dist/commands/generate/dbAuth/templates/signup.oauth.tsx.template +147 -0
  48. package/dist/commands/generate/directive/directiveHandler.js +2 -1
  49. package/dist/commands/generate/function/functionHandler.js +2 -1
  50. package/dist/commands/generate/helpers.js +26 -0
  51. package/dist/commands/generate/job/jobHandler.js +2 -1
  52. package/dist/commands/generate/model/modelHandler.js +2 -1
  53. package/dist/commands/generate/ogImage/ogImageHandler.js +2 -1
  54. package/dist/commands/generate/ogImage/templates/ogImage.og.tsx.template +1 -1
  55. package/dist/commands/generate/package/filesTask.js +9 -12
  56. package/dist/commands/generate/package/packageHandler.js +3 -4
  57. package/dist/commands/generate/package/templates/tsconfig.json.template +1 -0
  58. package/dist/commands/generate/page/pageHandler.js +2 -1
  59. package/dist/commands/generate/realtime/realtime.js +1 -1
  60. package/dist/commands/generate/realtime/realtimeHandler.js +2 -9
  61. package/dist/commands/generate/realtime/templates/liveQueries/blank/blank.service.ts.template +12 -4
  62. package/dist/commands/generate/realtime/templates/subscriptions/blank/blank.service.ts.template +2 -2
  63. package/dist/commands/generate/realtime/templates/subscriptions/blank/blank.ts.template +4 -4
  64. package/dist/commands/generate/scaffold/scaffold.js +1 -1
  65. package/dist/commands/generate/scaffold/scaffoldHandler.js +93 -49
  66. package/dist/commands/generate/scaffold/templates/assets/scaffold.tailwind.css.template +12 -7
  67. package/dist/commands/generate/scaffold/templates/components/EditNameCell.tsx.template +5 -7
  68. package/dist/commands/generate/scaffold/templates/components/NameForm.tsx.template +10 -9
  69. package/dist/commands/generate/scaffold/templates/layouts/NamesScaffoldLayout.tsx.template +18 -0
  70. package/dist/commands/generate/scaffold/templates/lib/formatters.test.tsx.template +11 -0
  71. package/dist/commands/generate/scaffold/templates/lib/formatters.tsx.template +16 -4
  72. package/dist/commands/generate/script/scriptHandler.js +2 -1
  73. package/dist/commands/generate/script/templates/tsconfig.json.template +1 -0
  74. package/dist/commands/generate/sdl/sdlHandler.js +10 -4
  75. package/dist/commands/generate/secret/secret.js +1 -1
  76. package/dist/commands/generate/service/serviceHandler.js +79 -49
  77. package/dist/commands/generate/service/templates/service.ts.template +40 -13
  78. package/dist/commands/generate/service/templates/test.ts.template +8 -6
  79. package/dist/commands/generate/yargsCommandHelpers.js +1 -1
  80. package/dist/commands/generate/yargsHandlerHelpers.js +2 -1
  81. package/dist/commands/generate.js +1 -1
  82. package/dist/commands/info.js +1 -1
  83. package/dist/commands/jobsHandler.js +5 -3
  84. package/dist/commands/lint.js +1 -1
  85. package/dist/commands/prerenderHandler.js +11 -4
  86. package/dist/commands/prismaHandler.js +7 -2
  87. package/dist/commands/record/init.js +1 -1
  88. package/dist/commands/serve.js +2 -1
  89. package/dist/commands/setup/auth/auth.js +2 -4
  90. package/dist/commands/setup/cache/cache.js +1 -1
  91. package/dist/commands/setup/cache/cacheHandler.js +2 -1
  92. package/dist/commands/setup/cache/templates/memcached.ts.template +14 -17
  93. package/dist/commands/setup/cache/templates/redis.ts.template +12 -20
  94. package/dist/commands/setup/database/postgres.js +1 -1
  95. package/dist/commands/setup/database/postgresHandler.js +3 -1
  96. package/dist/commands/setup/deploy/providers/baremetal.js +7 -0
  97. package/dist/commands/setup/deploy/providers/baremetalHandler.js +130 -21
  98. package/dist/commands/setup/deploy/providers/coherence.js +1 -1
  99. package/dist/commands/setup/deploy/providers/coherenceHandler.js +7 -6
  100. package/dist/commands/setup/deploy/providers/flightcontrolHandler.js +2 -1
  101. package/dist/commands/setup/deploy/providers/netlifyHandler.js +2 -1
  102. package/dist/commands/setup/deploy/providers/renderHandler.js +2 -1
  103. package/dist/commands/setup/deploy/providers/serverlessHandler.js +2 -1
  104. package/dist/commands/setup/deploy/providers/universalDeployHandler.js +4 -6
  105. package/dist/commands/setup/deploy/providers/vercelHandler.js +2 -1
  106. package/dist/commands/setup/deploy/templates/baremetal.js +119 -9
  107. package/dist/commands/setup/deploy/templates/netlifyUD.js +3 -0
  108. package/dist/commands/setup/docker/docker.js +1 -1
  109. package/dist/commands/setup/docker/dockerHandler.js +2 -1
  110. package/dist/commands/setup/generator/generator.js +1 -1
  111. package/dist/commands/setup/generator/generatorHandler.js +1 -1
  112. package/dist/commands/setup/graphql/features/fragments/fragments.js +1 -1
  113. package/dist/commands/setup/graphql/features/fragments/fragmentsHandler.js +3 -5
  114. package/dist/commands/setup/graphql/features/trustedDocuments/trustedDocuments.js +1 -1
  115. package/dist/commands/setup/graphql/features/trustedDocuments/trustedDocumentsHandler.js +2 -1
  116. package/dist/commands/setup/i18n/i18n.js +1 -1
  117. package/dist/commands/setup/i18n/i18nHandler.js +1 -1
  118. package/dist/commands/setup/jobs/jobs.js +1 -1
  119. package/dist/commands/setup/jobs/jobsHandler.js +4 -1
  120. package/dist/commands/setup/mailer/mailer.js +1 -1
  121. package/dist/commands/setup/mailer/mailerHandler.js +2 -1
  122. package/dist/commands/setup/middleware/ogImage/ogImage.js +1 -1
  123. package/dist/commands/setup/middleware/ogImage/ogImageHandler.js +2 -1
  124. package/dist/commands/setup/monitoring/sentry/sentry.js +1 -1
  125. package/dist/commands/setup/monitoring/sentry/sentryHandler.js +13 -9
  126. package/dist/commands/setup/monitoring/sentry/templates/sentryApi.ts.template +1 -4
  127. package/dist/commands/setup/monitoring/sentry/templates/sentryWeb.ts.template +7 -4
  128. package/dist/commands/setup/neon/neon.js +1 -1
  129. package/dist/commands/setup/neon/neonHandler.js +5 -9
  130. package/dist/commands/setup/package/package.js +1 -1
  131. package/dist/commands/setup/package/packageHandler.js +4 -4
  132. package/dist/commands/setup/realtime/addRealtimeToGraphql.js +1 -1
  133. package/dist/commands/setup/realtime/realtime.js +1 -1
  134. package/dist/commands/setup/realtime/realtimeHandler.js +4 -16
  135. package/dist/commands/setup/realtime/templates/defer/fastAndSlowFields/fastAndSlowFields.ts.template +4 -1
  136. package/dist/commands/setup/realtime/templates/liveQueries/auctions/auctions.ts.template +12 -4
  137. package/dist/commands/setup/realtime/templates/subscriptions/countdown/countdown.ts.template +1 -1
  138. package/dist/commands/setup/realtime/templates/subscriptions/newMessage/newMessage.ts.template +2 -2
  139. package/dist/commands/setup/server-file/serverFile.js +1 -1
  140. package/dist/commands/setup/server-file/serverFileHandler.js +2 -1
  141. package/dist/commands/setup/tenancy/authCodemod.js +66 -0
  142. package/dist/commands/setup/tenancy/dbCodemod.js +64 -0
  143. package/dist/commands/setup/tenancy/graphqlCodemod.js +88 -0
  144. package/dist/commands/setup/tenancy/preflight.js +49 -0
  145. package/dist/commands/setup/tenancy/schemaPrisma.js +81 -0
  146. package/dist/commands/setup/tenancy/signupCodemod.js +154 -0
  147. package/dist/commands/setup/tenancy/templates/InvitePage.tsx.template +72 -0
  148. package/dist/commands/setup/tenancy/templates/OrgScope.tsx.template +40 -0
  149. package/dist/commands/setup/tenancy/templates/ensureDefaultOrganizations.dataMigration.ts.template +39 -0
  150. package/dist/commands/setup/tenancy/templates/organizations.scenarios.ts.template +57 -0
  151. package/dist/commands/setup/tenancy/templates/organizations.sdl.ts.template +89 -0
  152. package/dist/commands/setup/tenancy/templates/organizations.test.ts.template +381 -0
  153. package/dist/commands/setup/tenancy/templates/organizations.ts.template +394 -0
  154. package/dist/commands/setup/tenancy/templates/requireMembership.test.ts.template +35 -0
  155. package/dist/commands/setup/tenancy/templates/requireMembership.ts.template +25 -0
  156. package/dist/commands/setup/tenancy/tenancy.js +30 -0
  157. package/dist/commands/setup/tenancy/tenancyHandler.js +425 -0
  158. package/dist/commands/setup/ui/libraries/chakra-uiHandler.js +2 -1
  159. package/dist/commands/setup/ui/libraries/mantineHandler.js +2 -1
  160. package/dist/commands/setup/ui/libraries/tailwindcssHandler.js +63 -82
  161. package/dist/commands/setup/uploads/gitignore.js +17 -0
  162. package/dist/commands/setup/uploads/schemaPrisma.js +41 -0
  163. package/dist/commands/setup/uploads/serverFile.js +217 -0
  164. package/dist/commands/setup/uploads/templates/requireUploadToken.ts.template +15 -0
  165. package/dist/commands/setup/uploads/templates/srcLibUploads.ts.template +23 -24
  166. package/dist/commands/setup/uploads/templates/uploads.sdl.ts.template +79 -0
  167. package/dist/commands/setup/uploads/templates/uploads.ts.template +98 -0
  168. package/dist/commands/setup/uploads/templates/withDataUri.ts.template +14 -0
  169. package/dist/commands/setup/uploads/templates/withSignedUrl.ts.template +16 -0
  170. package/dist/commands/setup/uploads/uploads.js +49 -7
  171. package/dist/commands/setup/uploads/uploadsHandler.js +277 -69
  172. package/dist/commands/setup.js +2 -1
  173. package/dist/commands/studio.js +1 -1
  174. package/dist/commands/test/testHandler.js +25 -71
  175. package/dist/commands/test/viteVersionCheck.js +75 -0
  176. package/dist/commands/test.js +10 -18
  177. package/dist/commands/type-checkHandler.js +1 -1
  178. package/dist/commands/upgrade/preUpgradeScripts.js +3 -3
  179. package/dist/commands/upgrade/upgrade.js +1 -1
  180. package/dist/commands/upgrade/upgradeHandler.js +2 -1
  181. package/dist/index.js +5 -8
  182. package/dist/lib/exit.js +1 -1
  183. package/dist/lib/index.js +16 -31
  184. package/dist/lib/loadEnvFiles.js +1 -1
  185. package/dist/lib/plugin.js +1 -1
  186. package/dist/lib/test.js +1 -1
  187. package/dist/lib/updateCheck.js +4 -4
  188. package/dist/middleware/checkNodeVersion.js +4 -4
  189. package/package.json +27 -27
  190. package/dist/commands/setup/ui/templates/postcss.config.cjs.template +0 -9
  191. package/dist/commands/setup/uploads/dbCodemod.js +0 -28
  192. package/dist/commands/setup/uploads/templates/signedUrl.ts.template +0 -21
  193. package/dist/commands/test/testHandlerEsm.js +0 -109
  194. package/dist/commands/testEsm.js +0 -41
@@ -0,0 +1,98 @@
1
+ import {
2
+ acceptedContentType,
3
+ confirmUpload as confirmUploadRecord,
4
+ createPresignedUpload,
5
+ getUploadTokenPayload,
6
+ issueUploadToken,
7
+ resolveProfile,
8
+ resolveTarget,
9
+ storeFile,
10
+ UploadError,
11
+ } from '@cedarjs/uploads'
12
+
13
+ import { db } from 'src/lib/db'
14
+ import { profiles, targets } from 'src/lib/uploads'
15
+
16
+ const secret = () => {
17
+ if (!process.env.UPLOAD_TOKEN_SECRET) {
18
+ throw new Error(
19
+ 'UPLOAD_TOKEN_SECRET is not set. Generate one with `yarn cedar generate secret` and add it to .env.',
20
+ )
21
+ }
22
+
23
+ return process.env.UPLOAD_TOKEN_SECRET
24
+ }
25
+
26
+ export const requestUploadToken = ({ profile }: { profile: string }) => {
27
+ return issueUploadToken({
28
+ profiles,
29
+ profile,
30
+ secret: secret(),
31
+ currentUser: context.currentUser,
32
+ })
33
+ }
34
+
35
+ export const createPresignedUploadUrl = ({
36
+ input,
37
+ }: {
38
+ input: { filename: string; contentType: string; size: bigint | number }
39
+ }) => {
40
+ return createPresignedUpload({
41
+ db,
42
+ targets,
43
+ tokenPayload: getUploadTokenPayload(context),
44
+ input,
45
+ })
46
+ }
47
+
48
+ export const confirmUpload = ({ uploadId }: { uploadId: string }) => {
49
+ return confirmUploadRecord({
50
+ db,
51
+ targets,
52
+ uploadId,
53
+ currentUser: context.currentUser,
54
+ })
55
+ }
56
+
57
+ export const uploadFile = async ({
58
+ profile: profileName,
59
+ input,
60
+ }: {
61
+ profile: string
62
+ input: { filename: string; mimeType: string; data: string }
63
+ }) => {
64
+ const profile = resolveProfile(profiles, profileName)
65
+ const target = resolveTarget(targets, profile.target)
66
+
67
+ if (target.providerType !== 'db') {
68
+ throw new UploadError(
69
+ 'NOT_SUPPORTED',
70
+ `Upload profile '${profile.name}' does not store files in the database. Use an upload token instead.`,
71
+ )
72
+ }
73
+
74
+ // Checked by what the bytes are, not the type the client claimed, and
75
+ // stored under the type returned
76
+ const data = Buffer.from(input.data, 'base64')
77
+ const mimeType = acceptedContentType(
78
+ data,
79
+ profile.allowedMimeTypes,
80
+ input.mimeType,
81
+ )
82
+
83
+ if (!mimeType) {
84
+ throw new UploadError(
85
+ 'MIME_TYPE_NOT_ALLOWED',
86
+ `That file is not a type allowed for upload profile '${profile.name}'.`,
87
+ )
88
+ }
89
+
90
+ return storeFile(target, {
91
+ db,
92
+ filename: input.filename,
93
+ mimeType,
94
+ data,
95
+ maxSize: profile.maxFileSize,
96
+ userId: context.currentUser ? String(context.currentUser.id) : undefined,
97
+ })
98
+ }
@@ -0,0 +1,14 @@
1
+ import { createWithDataUriDirective } from '@cedarjs/uploads'
2
+
3
+ import { db } from 'src/lib/db'
4
+ import { targets } from 'src/lib/uploads'
5
+
6
+ /**
7
+ * Resolves a field holding an Upload id to a base64 data: URI of the file.
8
+ * Every resolved field embeds the whole file, so use it for small files only.
9
+ */
10
+ const withDataUri = createWithDataUriDirective({ db, targets })
11
+
12
+ export const schema = withDataUri.schema
13
+
14
+ export default withDataUri
@@ -0,0 +1,16 @@
1
+ import { createWithSignedUrlDirective } from '@cedarjs/uploads'
2
+
3
+ import { db } from 'src/lib/db'
4
+ import { targets } from 'src/lib/uploads'
5
+
6
+ /**
7
+ * Resolves a field holding an Upload id to a time-limited URL for the file,
8
+ * from whichever storage target holds it. Files download by default; pass
9
+ * `disposition: 'inline'` to render them in the browser instead, and only
10
+ * for MIME types you trust.
11
+ */
12
+ const withSignedUrl = createWithSignedUrlDirective({ db, targets })
13
+
14
+ export const schema = withSignedUrl.schema
15
+
16
+ export default withSignedUrl
@@ -1,26 +1,68 @@
1
- import { recordTelemetryAttributes } from "@cedarjs/cli-helpers";
1
+ import prompts from "prompts";
2
+ import { recordTelemetryAttributes } from "@cedarjs/cli-helpers/telemetry";
3
+ import { requireTTYOrExit } from "@cedarjs/cli-helpers/tty";
2
4
  const command = "uploads";
3
- const description = "Setup uploads and storage. This will install the required packages and add the required initial configuration to your redwood app.";
5
+ const description = "Set up file uploads and storage: storage targets, upload profiles, the upload routes, and the GraphQL API";
6
+ const TARGET_CHOICES = ["fs", "db", "s3"];
7
+ const TARGET_DESCRIPTIONS = {
8
+ fs: "Local filesystem, uploaded through the api server (development, single-server deploys)",
9
+ db: "Inline in the database, for small files like avatars and thumbnails",
10
+ s3: "S3 or S3-compatible storage, uploaded straight from the browser with presigned URLs"
11
+ };
4
12
  const builder = (yargs) => {
5
- yargs.option("force", {
13
+ return yargs.option("targets", {
14
+ choices: TARGET_CHOICES,
15
+ description: "Storage targets to configure: fs (local filesystem), db (inline in the database), s3 (direct-to-S3 presigned uploads). Prompts when omitted.",
16
+ type: "array"
17
+ }).option("force", {
6
18
  alias: "f",
7
19
  default: false,
8
20
  description: "Overwrite existing configuration",
9
21
  type: "boolean"
10
22
  });
11
23
  };
24
+ function toTargetChoices(values) {
25
+ const choices = TARGET_CHOICES;
26
+ return [...new Set(values)].filter(
27
+ (t) => choices.includes(t)
28
+ );
29
+ }
30
+ async function promptForTargets() {
31
+ requireTTYOrExit("--targets");
32
+ const response = await prompts({
33
+ type: "multiselect",
34
+ name: "targets",
35
+ message: "Which storage targets do you want to configure?",
36
+ instructions: false,
37
+ hint: "Space to toggle, enter to confirm",
38
+ min: 1,
39
+ choices: TARGET_CHOICES.map((value) => ({
40
+ title: value,
41
+ description: TARGET_DESCRIPTIONS[value],
42
+ value,
43
+ selected: value !== "s3"
44
+ }))
45
+ });
46
+ if (response.targets === void 0) {
47
+ process.exit(0);
48
+ }
49
+ return toTargetChoices(response.targets);
50
+ }
12
51
  const handler = async (options) => {
52
+ const targets = options.targets?.length ? toTargetChoices(options.targets) : await promptForTargets();
13
53
  recordTelemetryAttributes({
14
54
  command: "setup uploads",
15
- force: options.force,
16
- skipExamples: options.skipExamples
55
+ targets: targets.join(","),
56
+ force: options.force
17
57
  });
18
58
  const { handler: handler2 } = await import("./uploadsHandler.js");
19
- return handler2(options);
59
+ return handler2({ targets, force: options.force });
20
60
  };
21
61
  export {
62
+ TARGET_CHOICES,
22
63
  builder,
23
64
  command,
24
65
  description,
25
- handler
66
+ handler,
67
+ toTargetChoices
26
68
  };
@@ -1,105 +1,298 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
+ import { pathToFileURL } from "node:url";
3
4
  import { Listr } from "listr2";
4
5
  import { format } from "prettier";
5
6
  import { terminalLink } from "termi-link";
7
+ import { colors as c } from "@cedarjs/cli-helpers/colors";
8
+ import { getPrettierOptions } from "@cedarjs/cli-helpers/fileHelpers";
6
9
  import {
7
10
  addApiPackages,
8
- getPrettierOptions,
9
- colors as c
10
- } from "@cedarjs/cli-helpers";
11
+ addWebPackages
12
+ } from "@cedarjs/cli-helpers/installHelpers";
11
13
  import { formatCedarCommand } from "@cedarjs/cli-helpers/packageManager/display";
14
+ import { addEnvVar } from "@cedarjs/cli-helpers/project";
15
+ import { getSchemaPath } from "@cedarjs/project-config";
12
16
  import { errorTelemetry } from "@cedarjs/telemetry";
13
17
  import { getPaths, transformTSToJS, writeFile } from "../../../lib/index.js";
14
18
  import { isTypeScriptProject } from "../../../lib/project.js";
15
- import { runTransform } from "../../../lib/runTransform.js";
16
- const handler = async ({ force }) => {
19
+ import { generateSecret } from "../../generate/secret/secret.js";
20
+ import { setupServerFileTasks } from "../server-file/serverFileHandler.js";
21
+ import { transformGitignore } from "./gitignore.js";
22
+ import { addUploadModel } from "./schemaPrisma.js";
23
+ import {
24
+ addUploadsPlugin,
25
+ detectServerAuth,
26
+ hasUploadsPlugin
27
+ } from "./serverFile.js";
28
+ const UPPY_VERSION = "^6.0.0";
29
+ const S3_IMPORTS = `import { S3Client } from '@aws-sdk/client-s3'
30
+
31
+ import { createS3Provider } from '@cedarjs/uploads/s3'
32
+ `;
33
+ const S3_CLIENT = `
34
+ // Your own S3 client. It stays available for S3-specific work (lifecycle
35
+ // rules, CORS, and so on) alongside the storage target below. Credentials
36
+ // come from the SDK's default chain: AWS_ACCESS_KEY_ID and
37
+ // AWS_SECRET_ACCESS_KEY in .env, or an IAM role, SSO, or container
38
+ // credentials in production. For an S3-compatible service, add \`endpoint\`
39
+ // and \`forcePathStyle: true\`.
40
+ export const s3Client = new S3Client({
41
+ region: process.env.AWS_REGION,
42
+ })
43
+
44
+ const s3Bucket = process.env.S3_BUCKET_UPLOADS
45
+
46
+ if (!s3Bucket) {
47
+ throw new Error('Set S3_BUCKET_UPLOADS in .env to use the S3 storage target')
48
+ }
49
+ `;
50
+ const TARGET_BLOCKS = {
51
+ fs: ` // Files uploaded through the api server and stored on local disk. Good for
52
+ // development and single-server deploys.
53
+ local: createFsProvider({
54
+ uploadDir: path.join(getPaths().api.base, '.uploads'),
55
+ serveBaseUrl: process.env.UPLOAD_SERVE_BASE_URL,
56
+ signSecret: process.env.UPLOAD_TOKEN_SECRET,
57
+ }),
58
+ `,
59
+ db: ` // Small files stored inline in the Upload row (avatars, thumbnails).
60
+ thumbnails: createDbProvider(),
61
+ `,
62
+ s3: ` // Files uploaded straight from the browser to S3 with presigned URLs.
63
+ files: createS3Provider({
64
+ client: s3Client,
65
+ bucket: s3Bucket,
66
+ region: process.env.AWS_REGION,
67
+ keyPrefix: 'uploads/',
68
+ }),
69
+ `
70
+ };
71
+ const PROFILE_BLOCKS = {
72
+ fs: ` attachment: {
73
+ target: 'local',
74
+ allowedMimeTypes: ['application/pdf', 'image/png', 'image/jpeg'],
75
+ maxFileSize: 25 * 1024 * 1024,
76
+ maxFiles: 10,
77
+ },
78
+ `,
79
+ db: ` avatar: {
80
+ target: 'thumbnails',
81
+ allowedMimeTypes: ['image/png', 'image/jpeg'],
82
+ maxFileSize: DB_MAX_FILE_SIZE,
83
+ maxFiles: 1,
84
+ },
85
+ `,
86
+ s3: ` document: {
87
+ target: 'files',
88
+ allowedMimeTypes: ['application/pdf', 'image/*'],
89
+ maxFileSize: 100 * 1024 * 1024,
90
+ maxFiles: 5,
91
+ },
92
+ `
93
+ };
94
+ const handler = async ({ targets, force }) => {
95
+ const paths = getPaths();
17
96
  const projectIsTypescript = isTypeScriptProject();
18
- const packageJson = await import(path.join(getPaths().base, "package.json"), {
19
- with: { type: "json" }
20
- });
97
+ const ext = projectIsTypescript ? "ts" : "js";
98
+ const wantsFs = targets.includes("fs");
99
+ const wantsDb = targets.includes("db");
100
+ const wantsS3 = targets.includes("s3");
101
+ const wantsUppy = wantsFs || wantsS3;
102
+ const packageJson = await import(pathToFileURL(path.join(paths.base, "package.json")).href, { with: { type: "json" } });
21
103
  const cedarVersion = packageJson.default.devDependencies["@cedarjs/core"] ?? "latest";
104
+ const serverFilePath = path.join(paths.api.src, `server.${ext}`);
105
+ const writeTemplateFile = async (templateFile, outputPath, replacements = {}) => {
106
+ const templatePath = path.resolve(
107
+ import.meta.dirname,
108
+ "templates",
109
+ templateFile
110
+ );
111
+ let templateContent = fs.readFileSync(templatePath, "utf-8");
112
+ for (const [token, value] of Object.entries(replacements)) {
113
+ templateContent = templateContent.replaceAll(token, value);
114
+ }
115
+ const content = projectIsTypescript ? templateContent : await transformTSToJS(outputPath, templateContent);
116
+ return writeFile(outputPath, content, { overwriteExisting: force });
117
+ };
118
+ const apiPackages = [`@cedarjs/uploads@${cedarVersion}`];
119
+ const webPackages = [`@cedarjs/uploads@${cedarVersion}`];
120
+ if (wantsS3) {
121
+ apiPackages.push("@aws-sdk/client-s3", "@aws-sdk/s3-request-presigner");
122
+ }
123
+ if (wantsUppy) {
124
+ webPackages.push(
125
+ `@uppy/core@${UPPY_VERSION}`,
126
+ `@uppy/react@${UPPY_VERSION}`,
127
+ `@uppy/dashboard@${UPPY_VERSION}`
128
+ );
129
+ }
130
+ if (wantsFs) {
131
+ webPackages.push(`@uppy/xhr-upload@${UPPY_VERSION}`);
132
+ }
133
+ if (wantsS3) {
134
+ webPackages.push(`@uppy/aws-s3@${UPPY_VERSION}`);
135
+ }
22
136
  const tasks = new Listr(
23
137
  [
24
138
  {
25
- title: `Adding api/src/lib/uploads.${projectIsTypescript ? "ts" : "js"}...`,
139
+ ...addApiPackages(apiPackages),
140
+ title: "Adding @cedarjs/uploads to your api side..."
141
+ },
142
+ {
143
+ ...addWebPackages(webPackages),
144
+ title: "Adding @cedarjs/uploads to your web side..."
145
+ },
146
+ {
147
+ title: "Adding the Upload model to schema.prisma...",
26
148
  task: async () => {
27
- const templatePath = path.resolve(
28
- import.meta.dirname,
29
- "templates",
30
- "srcLibUploads.ts.template"
31
- );
32
- const templateContent = fs.readFileSync(templatePath, {
33
- encoding: "utf8",
34
- flag: "r"
35
- });
36
- const uploadsPath = path.join(
37
- getPaths().api.lib,
38
- `uploads.${projectIsTypescript ? "ts" : "js"}`
149
+ const schemaPath = await getSchemaPath(paths.api.prismaConfig);
150
+ const schema = fs.readFileSync(schemaPath, "utf-8");
151
+ fs.writeFileSync(schemaPath, addUploadModel(schema));
152
+ }
153
+ },
154
+ {
155
+ title: `Adding api/src/lib/uploads.${ext}...`,
156
+ task: async () => {
157
+ await writeTemplateFile(
158
+ "srcLibUploads.ts.template",
159
+ path.join(paths.api.lib, `uploads.${ext}`),
160
+ {
161
+ __S3_IMPORTS__: wantsS3 ? S3_IMPORTS : "",
162
+ __S3_CLIENT__: wantsS3 ? S3_CLIENT : "",
163
+ __FS_IMPORT__: wantsFs ? "createFsProvider," : "",
164
+ __DB_IMPORT__: wantsDb ? "createDbProvider," : "",
165
+ __DB_MAX_IMPORT__: wantsDb ? "DB_MAX_FILE_SIZE," : "",
166
+ __PATH_IMPORT__: wantsFs ? "import path from 'node:path'\n\n" : "",
167
+ __GET_PATHS_IMPORT__: wantsFs ? "import { getPaths } from '@cedarjs/project-config'\n" : "",
168
+ __TARGETS__: targets.map((t) => TARGET_BLOCKS[t]).join("\n"),
169
+ __PROFILES__: targets.map((t) => PROFILE_BLOCKS[t]).join("\n")
170
+ }
39
171
  );
40
- const uploadsContent = projectIsTypescript ? templateContent : await transformTSToJS(uploadsPath, templateContent);
41
- return writeFile(uploadsPath, uploadsContent, {
42
- overwriteExisting: force
43
- });
44
172
  }
45
173
  },
46
174
  {
47
- title: `Adding signedUrl function...`,
175
+ title: "Adding the uploads SDL and service...",
48
176
  task: async () => {
49
- const templatePath = path.resolve(
50
- import.meta.dirname,
51
- "templates",
52
- "signedUrl.ts.template"
177
+ await writeTemplateFile(
178
+ "uploads.sdl.ts.template",
179
+ path.join(paths.api.graphql, `uploads.sdl.${ext}`)
53
180
  );
54
- const templateContent = fs.readFileSync(templatePath, {
55
- encoding: "utf8",
56
- flag: "r"
57
- });
58
- const uploadsPath = path.join(
59
- getPaths().api.functions,
60
- `signedUrl.${projectIsTypescript ? "ts" : "js"}`
181
+ await writeTemplateFile(
182
+ "uploads.ts.template",
183
+ path.join(paths.api.services, "uploads", `uploads.${ext}`)
61
184
  );
62
- const uploadsContent = projectIsTypescript ? templateContent : await transformTSToJS(uploadsPath, templateContent);
63
- return writeFile(uploadsPath, uploadsContent, {
64
- overwriteExisting: force
65
- });
66
185
  }
67
186
  },
68
187
  {
69
- ...addApiPackages([`@cedarjs/storage@${cedarVersion}`]),
70
- title: "Adding dependencies to your api side..."
188
+ title: "Adding the upload directives...",
189
+ task: async () => {
190
+ const directives = ["requireUploadToken", "withSignedUrl"];
191
+ if (wantsDb) {
192
+ directives.push("withDataUri");
193
+ }
194
+ for (const name of directives) {
195
+ await writeTemplateFile(
196
+ `${name}.ts.template`,
197
+ path.join(paths.api.directives, name, `${name}.${ext}`)
198
+ );
199
+ }
200
+ }
71
201
  },
72
202
  {
73
- title: "Modifying api/src/lib/db to add uploads prisma extension..",
74
- task: async () => {
75
- const dbPath = path.join(
76
- getPaths().api.lib,
77
- `db.${projectIsTypescript ? "ts" : "js"}`
78
- );
79
- const transformResult = await runTransform({
80
- transformPath: path.join(import.meta.dirname, "dbCodemod.js"),
81
- targetPaths: [dbPath]
82
- });
83
- if (transformResult.error) {
84
- if (transformResult.error === "RW_CODEMOD_ERR_OLD_FORMAT") {
85
- throw new Error(
86
- "It looks like your src/lib/db file is using the old format. Please update it as per the v8 upgrade guide: https://cedarjs.com/docs/8.x/upgrade-guides/v8#database-file-structure-change. And run again. \n\nYou can also manually modify your api/src/lib/db to include the prisma extension: https://cedarjs.com/docs/uploads/#attaching-the-prisma-extension"
87
- );
88
- }
203
+ title: "Adding the server file...",
204
+ task: (_ctx, task) => task.newListr(setupServerFileTasks({ force: false })),
205
+ skip: () => fs.existsSync(serverFilePath) ? "api/src/server already exists; skipping." : false
206
+ },
207
+ {
208
+ title: "Registering the upload plugin in api/src/server...",
209
+ task: async (_ctx, task) => {
210
+ const source = fs.readFileSync(serverFilePath, "utf-8");
211
+ if (hasUploadsPlugin(source)) {
212
+ task.skip("The upload plugin is already registered; skipping.");
213
+ return;
214
+ }
215
+ const graphqlPath = path.join(paths.api.functions, `graphql.${ext}`);
216
+ const authPath = path.join(paths.api.lib, `auth.${ext}`);
217
+ const auth = fs.existsSync(graphqlPath) && fs.existsSync(authPath) && fs.readFileSync(authPath, "utf-8").includes("getCurrentUser") ? detectServerAuth(fs.readFileSync(graphqlPath, "utf-8")) : null;
218
+ let updated;
219
+ try {
220
+ updated = addUploadsPlugin(source, { auth });
221
+ } catch {
89
222
  throw new Error(
90
- "Could not add the prisma extension. \n Please modify your api/src/lib/db to include the prisma extension: https://cedarjs.com/docs/uploads/#attaching-the-prisma-extension"
223
+ `Could not find \`await server.start()\` in ${path.relative(
224
+ paths.base,
225
+ serverFilePath
226
+ )}. Register the plugin by hand before the server starts:
227
+
228
+ import { cedarUploadsPlugin } from '@cedarjs/uploads'
229
+ import { db } from 'src/lib/db'
230
+ import { targets } from 'src/lib/uploads'
231
+
232
+ await server.register(cedarUploadsPlugin, {
233
+ tokenSecret: process.env.UPLOAD_TOKEN_SECRET,
234
+ targets,
235
+ db,
236
+ })
237
+ `
91
238
  );
92
239
  }
240
+ fs.writeFileSync(serverFilePath, updated);
241
+ }
242
+ },
243
+ {
244
+ title: "Adding environment variables to .env...",
245
+ task: () => {
246
+ addEnvVar(
247
+ "UPLOAD_TOKEN_SECRET",
248
+ generateSecret(),
249
+ "Signs upload tokens and file-serving URLs. Keep it secret and use a different value per environment."
250
+ );
251
+ if (wantsFs) {
252
+ addEnvVar(
253
+ "UPLOAD_SERVE_BASE_URL",
254
+ "http://localhost:8911",
255
+ "Origin the api server is reachable at, used to build signed URLs for locally stored files."
256
+ );
257
+ }
258
+ if (wantsS3) {
259
+ addEnvVar(
260
+ "AWS_REGION",
261
+ "us-east-1",
262
+ "Region of the uploads bucket."
263
+ );
264
+ addEnvVar("AWS_ACCESS_KEY_ID", "replace-me", "");
265
+ addEnvVar("AWS_SECRET_ACCESS_KEY", "replace-me", "");
266
+ addEnvVar(
267
+ "S3_BUCKET_UPLOADS",
268
+ "replace-me",
269
+ "Bucket direct uploads land in."
270
+ );
271
+ }
272
+ }
273
+ },
274
+ {
275
+ title: "Adding the fs upload directory to .gitignore...",
276
+ skip: () => wantsFs ? false : "No fs target selected; skipping.",
277
+ task: (_ctx, task) => {
278
+ const gitignorePath = path.join(paths.base, ".gitignore");
279
+ const source = fs.existsSync(gitignorePath) ? fs.readFileSync(gitignorePath, "utf-8") : "";
280
+ const updated = transformGitignore(source);
281
+ if (updated === source) {
282
+ task.skip(".gitignore already up to date");
283
+ return;
284
+ }
285
+ fs.writeFileSync(gitignorePath, updated, "utf-8");
93
286
  }
94
287
  },
95
288
  {
96
289
  title: "Prettifying changed files",
97
290
  task: async (_ctx, task) => {
98
291
  const prettifyPaths = [
99
- path.join(getPaths().api.lib, "db.js"),
100
- path.join(getPaths().api.lib, "db.ts"),
101
- path.join(getPaths().api.lib, "uploads.js"),
102
- path.join(getPaths().api.lib, "uploads.ts")
292
+ serverFilePath,
293
+ path.join(paths.api.lib, `uploads.${ext}`),
294
+ path.join(paths.api.graphql, `uploads.sdl.${ext}`),
295
+ path.join(paths.api.services, "uploads", `uploads.${ext}`)
103
296
  ];
104
297
  for (const prettifyPath of prettifyPaths) {
105
298
  try {
@@ -108,11 +301,11 @@ const handler = async ({ force }) => {
108
301
  }
109
302
  const source = fs.readFileSync(prettifyPath, "utf-8");
110
303
  const prettierOptions = await getPrettierOptions();
111
- const prettifiedApp = await format(source, {
304
+ const prettified = await format(source, {
112
305
  ...prettierOptions,
113
306
  parser: "babel-ts"
114
307
  });
115
- fs.writeFileSync(prettifyPath, prettifiedApp, "utf-8");
308
+ fs.writeFileSync(prettifyPath, prettified, "utf-8");
116
309
  } catch {
117
310
  task.output = "Couldn't prettify the changes. Please reformat the files manually if needed.";
118
311
  }
@@ -122,15 +315,30 @@ const handler = async ({ force }) => {
122
315
  {
123
316
  title: "One more thing...",
124
317
  task: (_ctx, task) => {
318
+ const migrate = c.highlight(
319
+ formatCedarCommand(["prisma", "migrate", "dev"])
320
+ );
321
+ const types = c.highlight(formatCedarCommand(["generate", "types"]));
125
322
  task.title = `One more thing...
126
323
 
127
- ${c.success("\nUploads and storage configured!\n")}
324
+ ${c.success("Uploads configured!")}
325
+
326
+ Next steps:
128
327
 
129
- Remember to add UPLOADS_SECRET to your .env file. You can generate one with ${c.highlight(formatCedarCommand(["generate", "secret"]))}
328
+ 1. Apply the schema change: ${migrate}
329
+ 2. Regenerate types: ${types}
330
+ 3. Review the storage targets and upload profiles in api/src/lib/uploads.${ext}${wantsS3 ? "\n 4. Fill in the AWS_* and S3_BUCKET_UPLOADS values in .env" : ""}
331
+
332
+ Upload tokens are bound to the logged-in user when the app has auth.
333
+ If you add auth later, pass
334
+ \`authenticate: createUploadAuthenticator({ authDecoder, getCurrentUser })\`
335
+ to the plugin in api/src/server.${ext}.
336
+
337
+ Schedule cleanupStaleUploads() from a recurring job to sweep uploads
338
+ that never completed.
130
339
 
131
340
  Check out the docs for more info:
132
341
  ${terminalLink("", "https://cedarjs.com/docs/uploads")}
133
-
134
342
  `;
135
343
  }
136
344
  }
@@ -16,11 +16,12 @@ import * as setupNeon from "./setup/neon/neon.js";
16
16
  import * as setupPackage from "./setup/package/package.js";
17
17
  import * as setupRealtime from "./setup/realtime/realtime.js";
18
18
  import * as setupServerFile from "./setup/server-file/serverFile.js";
19
+ import * as setupTenancy from "./setup/tenancy/tenancy.js";
19
20
  import * as setupUi from "./setup/ui/ui.js";
20
21
  import * as setupUploads from "./setup/uploads/uploads.js";
21
22
  const command = "setup <command>";
22
23
  const description = "Initialize project config and install packages";
23
- const builder = (yargs) => yargs.command(setupAuth).command(setupCache).command(setupDatabase).command(setupDeploy).command(setupDocker).command(setupGenerator).command(setupGraphql).command(setupI18n).command(setupJobs).command(setupMailer).command(setupMiddleware).command(setupMonitoring).command(setupNeon).command(setupPackage).command(setupRealtime).command(setupServerFile).command(setupUi).command(setupUploads).demandCommand().middleware(detectCedarVersion).epilogue(
24
+ const builder = (yargs) => yargs.command(setupAuth).command(setupCache).command(setupDatabase).command(setupDeploy).command(setupDocker).command(setupGenerator).command(setupGraphql).command(setupI18n).command(setupJobs).command(setupMailer).command(setupMiddleware).command(setupMonitoring).command(setupNeon).command(setupPackage).command(setupRealtime).command(setupServerFile).command(setupTenancy).command(setupUi).command(setupUploads).demandCommand().middleware(detectCedarVersion).epilogue(
24
25
  `Also see the ${terminalLink(
25
26
  "CedarJS CLI Reference",
26
27
  "https://cedarjs.com/docs/cli-commands#setup"
@@ -1,4 +1,4 @@
1
- import { recordTelemetryAttributes } from "@cedarjs/cli-helpers";
1
+ import { recordTelemetryAttributes } from "@cedarjs/cli-helpers/telemetry";
2
2
  const command = "studio";
3
3
  const description = "Run the CedarJS development studio";
4
4
  function builder(yargs) {