@cedarjs/cli 7.0.0-canary.3113 → 7.0.0-canary.3115
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/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 +48 -6
- package/dist/commands/setup/uploads/uploadsHandler.js +256 -67
- package/package.json +13 -13
- package/dist/commands/setup/uploads/dbCodemod.js +0 -28
- package/dist/commands/setup/uploads/templates/signedUrl.ts.template +0 -21
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const UPLOAD_MODEL = `model Upload {
|
|
2
|
+
id String @id @default(cuid())
|
|
3
|
+
/// Name of the storage target, as configured in api/src/lib/uploads
|
|
4
|
+
target String
|
|
5
|
+
/// "pending" | "completed" | "failed"
|
|
6
|
+
status String @default("pending")
|
|
7
|
+
filename String
|
|
8
|
+
mimeType String
|
|
9
|
+
/// Bytes. BigInt because Int caps at 2 GB.
|
|
10
|
+
size BigInt
|
|
11
|
+
/// Provider-specific reference: S3 object key, filename on disk, or null
|
|
12
|
+
/// for files stored inline
|
|
13
|
+
storageKey String?
|
|
14
|
+
/// Inline bytes, only for DB targets
|
|
15
|
+
data Bytes?
|
|
16
|
+
/// The user the file belongs to, when it belongs to one
|
|
17
|
+
userId String?
|
|
18
|
+
/// Id of the upload token that created this row
|
|
19
|
+
tokenId String?
|
|
20
|
+
/// The organization the file belongs to, in multi-tenant apps
|
|
21
|
+
organizationId String?
|
|
22
|
+
createdAt DateTime @default(now())
|
|
23
|
+
updatedAt DateTime @updatedAt
|
|
24
|
+
}`;
|
|
25
|
+
function hasModel(schema, modelName) {
|
|
26
|
+
return new RegExp(`\\bmodel\\s+${modelName}\\s*\\{`).test(schema);
|
|
27
|
+
}
|
|
28
|
+
function addUploadModel(schema) {
|
|
29
|
+
if (hasModel(schema, "Upload")) {
|
|
30
|
+
return schema;
|
|
31
|
+
}
|
|
32
|
+
return `${schema.trimEnd()}
|
|
33
|
+
|
|
34
|
+
${UPLOAD_MODEL}
|
|
35
|
+
`;
|
|
36
|
+
}
|
|
37
|
+
export {
|
|
38
|
+
UPLOAD_MODEL,
|
|
39
|
+
addUploadModel,
|
|
40
|
+
hasModel
|
|
41
|
+
};
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
const START_LINE = /^([ \t]*)await server\.start\(\)/m;
|
|
2
|
+
const FACTORY_IMPORT = /import\s*\{[^}]*\bcreateAuthDecoder\b[^}]*\}\s*from\s*['"](@cedarjs\/auth-[a-z0-9-]+-api)['"]/;
|
|
3
|
+
const DECODER_IMPORT = /import\s*\{[^}]*\bauthDecoder\b[^}]*\}\s*from\s*['"](@cedarjs\/auth-[a-z0-9-]+-api)['"]/;
|
|
4
|
+
function detectServerAuth(graphqlSource) {
|
|
5
|
+
const factory = FACTORY_IMPORT.exec(graphqlSource);
|
|
6
|
+
if (factory) {
|
|
7
|
+
return { decoderPackage: factory[1], usesFactory: true };
|
|
8
|
+
}
|
|
9
|
+
const decoder = DECODER_IMPORT.exec(graphqlSource);
|
|
10
|
+
if (decoder) {
|
|
11
|
+
return { decoderPackage: decoder[1], usesFactory: false };
|
|
12
|
+
}
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
function importedBindings(source) {
|
|
16
|
+
const names = /* @__PURE__ */ new Set();
|
|
17
|
+
const statements = source.matchAll(
|
|
18
|
+
/^import\s+(type\s+)?([^;]*?)\s+from\s+['"][^'"]+['"]/gm
|
|
19
|
+
);
|
|
20
|
+
for (const [, typeOnly, clause] of statements) {
|
|
21
|
+
if (typeOnly) {
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
const braces = /\{([^}]*)\}/.exec(clause);
|
|
25
|
+
const defaultOrNamespace = clause.replace(/\{[^}]*\}/, "");
|
|
26
|
+
for (const part of defaultOrNamespace.split(",")) {
|
|
27
|
+
const local = part.replace(/^\s*\*\s+as\s+/, "").trim();
|
|
28
|
+
if (local) {
|
|
29
|
+
names.add(local);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
for (const specifier of braces?.[1].split(",") ?? []) {
|
|
33
|
+
const trimmed = specifier.trim();
|
|
34
|
+
if (!trimmed || trimmed.startsWith("type ")) {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
const alias = / as (\w+)$/.exec(trimmed);
|
|
38
|
+
names.add(alias ? alias[1] : trimmed);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return names;
|
|
42
|
+
}
|
|
43
|
+
function hasBinding(source, name) {
|
|
44
|
+
const declared = new RegExp(
|
|
45
|
+
`^(?:export\\s+)?(?:const|let|var|function)\\s+${name}\\b`,
|
|
46
|
+
"m"
|
|
47
|
+
);
|
|
48
|
+
return importedBindings(source).has(name) || declared.test(source);
|
|
49
|
+
}
|
|
50
|
+
function uploadsServerImports(source, auth) {
|
|
51
|
+
const packageSpecs = [];
|
|
52
|
+
const srcSpecs = [];
|
|
53
|
+
if (auth) {
|
|
54
|
+
if (auth.usesFactory) {
|
|
55
|
+
if (!hasBinding(source, "authDecoder")) {
|
|
56
|
+
packageSpecs.push({
|
|
57
|
+
names: ["createAuthDecoder"],
|
|
58
|
+
from: auth.decoderPackage
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
} else {
|
|
62
|
+
packageSpecs.push({ names: ["authDecoder"], from: auth.decoderPackage });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
packageSpecs.push({
|
|
66
|
+
names: auth ? ["cedarUploadsPlugin", "createUploadAuthenticator"] : ["cedarUploadsPlugin"],
|
|
67
|
+
from: "@cedarjs/uploads"
|
|
68
|
+
});
|
|
69
|
+
if (auth) {
|
|
70
|
+
const authNames = ["getCurrentUser"];
|
|
71
|
+
if (auth.usesFactory && !hasBinding(source, "authDecoder")) {
|
|
72
|
+
authNames.unshift("cookieName");
|
|
73
|
+
}
|
|
74
|
+
srcSpecs.push({ names: authNames, from: "src/lib/auth" });
|
|
75
|
+
}
|
|
76
|
+
srcSpecs.push({ names: ["db"], from: "src/lib/db" });
|
|
77
|
+
srcSpecs.push({ names: ["targets"], from: "src/lib/uploads" });
|
|
78
|
+
const render = (specs) => specs.map((spec) => ({
|
|
79
|
+
...spec,
|
|
80
|
+
names: spec.names.filter((name) => !hasBinding(source, name))
|
|
81
|
+
})).filter((spec) => spec.names.length > 0).map((spec) => `import { ${spec.names.join(", ")} } from '${spec.from}'`);
|
|
82
|
+
const packageLines = render(packageSpecs);
|
|
83
|
+
const srcLines = render(srcSpecs);
|
|
84
|
+
if (packageLines.length > 0 && srcLines.length > 0) {
|
|
85
|
+
return [...packageLines, "", ...srcLines];
|
|
86
|
+
}
|
|
87
|
+
return [...packageLines, ...srcLines];
|
|
88
|
+
}
|
|
89
|
+
function uploadsServerRegistration(source, auth) {
|
|
90
|
+
if (!auth) {
|
|
91
|
+
return ` await server.register(cedarUploadsPlugin, {
|
|
92
|
+
tokenSecret: process.env.UPLOAD_TOKEN_SECRET,
|
|
93
|
+
targets,
|
|
94
|
+
db,
|
|
95
|
+
// Once the app has auth, bind upload tokens to the logged-in user:
|
|
96
|
+
//
|
|
97
|
+
// authenticate: createUploadAuthenticator({ authDecoder, getCurrentUser }),
|
|
98
|
+
})
|
|
99
|
+
`;
|
|
100
|
+
}
|
|
101
|
+
const decoder = auth.usesFactory && !hasBinding(source, "authDecoder") ? " const authDecoder = createAuthDecoder(cookieName)\n\n" : "";
|
|
102
|
+
return `${decoder} await server.register(cedarUploadsPlugin, {
|
|
103
|
+
tokenSecret: process.env.UPLOAD_TOKEN_SECRET,
|
|
104
|
+
targets,
|
|
105
|
+
db,
|
|
106
|
+
// Binds upload tokens to the logged-in user through the same auth
|
|
107
|
+
// pipeline the GraphQL server uses
|
|
108
|
+
authenticate: createUploadAuthenticator({ authDecoder, getCurrentUser }),
|
|
109
|
+
})
|
|
110
|
+
`;
|
|
111
|
+
}
|
|
112
|
+
function hasUploadsPlugin(source) {
|
|
113
|
+
return /^[ \t]*(?:(?:await|void)\s+|(?:const|let|var)\s+\w+\s*=\s*)?[\w.]+\.register\(\s*cedarUploadsPlugin\b/m.test(
|
|
114
|
+
source
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
function addUploadsPlugin(source, { auth = null } = {}) {
|
|
118
|
+
if (hasUploadsPlugin(source)) {
|
|
119
|
+
return source;
|
|
120
|
+
}
|
|
121
|
+
const start = START_LINE.exec(source);
|
|
122
|
+
if (!start) {
|
|
123
|
+
throw new Error("CEDAR_UPLOADS_ERR_NO_START");
|
|
124
|
+
}
|
|
125
|
+
const lines = source.split("\n");
|
|
126
|
+
let lastImport = -1;
|
|
127
|
+
for (const [index, line] of lines.entries()) {
|
|
128
|
+
if (/^import\s/.test(line)) {
|
|
129
|
+
lastImport = index;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const withImports = [
|
|
133
|
+
...lines.slice(0, lastImport + 1),
|
|
134
|
+
...uploadsServerImports(source, auth),
|
|
135
|
+
...lines.slice(lastImport + 1)
|
|
136
|
+
].join("\n");
|
|
137
|
+
return withImports.replace(
|
|
138
|
+
START_LINE,
|
|
139
|
+
(line) => `${uploadsServerRegistration(source, auth)}
|
|
140
|
+
${line}`
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
export {
|
|
144
|
+
addUploadsPlugin,
|
|
145
|
+
detectServerAuth,
|
|
146
|
+
hasBinding,
|
|
147
|
+
hasUploadsPlugin,
|
|
148
|
+
importedBindings,
|
|
149
|
+
uploadsServerImports,
|
|
150
|
+
uploadsServerRegistration
|
|
151
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { createRequireUploadTokenDirective } from '@cedarjs/uploads'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Requires a valid upload token in the x-upload-token request header and
|
|
5
|
+
* makes its claims available to the resolver through
|
|
6
|
+
* `getUploadTokenPayload(context)`. The token must have been issued to the
|
|
7
|
+
* requesting user.
|
|
8
|
+
*/
|
|
9
|
+
const requireUploadToken = createRequireUploadTokenDirective({
|
|
10
|
+
secret: process.env.UPLOAD_TOKEN_SECRET ?? '',
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
export const schema = requireUploadToken.schema
|
|
14
|
+
|
|
15
|
+
export default requireUploadToken
|
|
@@ -1,25 +1,24 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
__PATH_IMPORT____S3_IMPORTS__import {
|
|
2
|
+
__FS_IMPORT__
|
|
3
|
+
__DB_IMPORT__
|
|
4
|
+
__DB_MAX_IMPORT__
|
|
5
|
+
defineStorageTargets,
|
|
6
|
+
defineUploadProfiles,
|
|
7
|
+
} from '@cedarjs/uploads'
|
|
8
|
+
__GET_PATHS_IMPORT__
|
|
9
|
+
__S3_CLIENT__
|
|
10
|
+
/**
|
|
11
|
+
* Where files live. Each target is a provider instance; the Upload row
|
|
12
|
+
* records which target holds a file so it can be read, served, and deleted
|
|
13
|
+
* later. Look one up by name with `resolveTarget(targets, upload.target)`.
|
|
14
|
+
*/
|
|
15
|
+
export const targets = defineStorageTargets({
|
|
16
|
+
__TARGETS__})
|
|
4
17
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
export const
|
|
11
|
-
|
|
12
|
-
})
|
|
13
|
-
|
|
14
|
-
export const urlSigner = new UrlSigner({
|
|
15
|
-
secret: process.env.UPLOADS_SECRET,
|
|
16
|
-
endpoint: '/signedUrl',
|
|
17
|
-
})
|
|
18
|
-
|
|
19
|
-
const { saveFiles, storagePrismaExtension } = setupStorage({
|
|
20
|
-
uploadsConfig,
|
|
21
|
-
storageAdapter: fsStorage,
|
|
22
|
-
urlSigner,
|
|
23
|
-
})
|
|
24
|
-
|
|
25
|
-
export { saveFiles, storagePrismaExtension }
|
|
18
|
+
/**
|
|
19
|
+
* What clients may upload. A profile names a target and the constraints the
|
|
20
|
+
* server signs into every upload token issued for it. The client never
|
|
21
|
+
* supplies constraints; it names a profile.
|
|
22
|
+
*/
|
|
23
|
+
export const profiles = defineUploadProfiles({
|
|
24
|
+
__PROFILES__})
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
export const schema = gql`
|
|
2
|
+
type Upload {
|
|
3
|
+
id: String!
|
|
4
|
+
target: String!
|
|
5
|
+
status: String!
|
|
6
|
+
filename: String!
|
|
7
|
+
mimeType: String!
|
|
8
|
+
size: BigInt!
|
|
9
|
+
storageKey: String
|
|
10
|
+
userId: String
|
|
11
|
+
organizationId: String
|
|
12
|
+
createdAt: DateTime!
|
|
13
|
+
updatedAt: DateTime!
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
type UploadToken {
|
|
17
|
+
token: String!
|
|
18
|
+
"""
|
|
19
|
+
The profile's constraints, echoed for client-side UX (file-picker
|
|
20
|
+
filters, pre-upload validation). The authoritative copy is inside the
|
|
21
|
+
signed token.
|
|
22
|
+
"""
|
|
23
|
+
allowedMimeTypes: [String!]!
|
|
24
|
+
maxFileSize: BigInt!
|
|
25
|
+
maxFiles: Int!
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
type PresignedUploadUrl {
|
|
29
|
+
uploadId: String!
|
|
30
|
+
url: String!
|
|
31
|
+
method: String!
|
|
32
|
+
headers: JSON!
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
input CreatePresignedUploadUrlInput {
|
|
36
|
+
filename: String!
|
|
37
|
+
contentType: String!
|
|
38
|
+
size: BigInt!
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
input UploadFileInput {
|
|
42
|
+
filename: String!
|
|
43
|
+
mimeType: String!
|
|
44
|
+
"""
|
|
45
|
+
The file's bytes, base64-encoded. Only for profiles whose target stores
|
|
46
|
+
files inline in the database.
|
|
47
|
+
"""
|
|
48
|
+
data: String!
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
type Query {
|
|
52
|
+
"""
|
|
53
|
+
Issues an upload token for a server-defined profile. The profile name
|
|
54
|
+
must match a key of \`profiles\` in api/src/lib/uploads.
|
|
55
|
+
"""
|
|
56
|
+
requestUploadToken(profile: String!): UploadToken! @requireAuth
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
type Mutation {
|
|
60
|
+
"""
|
|
61
|
+
Direct-to-storage uploads: returns a presigned URL for one file. Send the
|
|
62
|
+
upload token in the x-upload-token header.
|
|
63
|
+
"""
|
|
64
|
+
createPresignedUploadUrl(
|
|
65
|
+
input: CreatePresignedUploadUrlInput!
|
|
66
|
+
): PresignedUploadUrl! @requireAuth @requireUploadToken
|
|
67
|
+
|
|
68
|
+
"""
|
|
69
|
+
Confirms a direct-to-storage upload once the client has PUT the bytes.
|
|
70
|
+
"""
|
|
71
|
+
confirmUpload(uploadId: String!): Upload! @requireAuth
|
|
72
|
+
|
|
73
|
+
"""
|
|
74
|
+
Stores a small file sent as base64, for profiles whose target keeps files
|
|
75
|
+
inline in the database.
|
|
76
|
+
"""
|
|
77
|
+
uploadFile(profile: String!, input: UploadFileInput!): Upload! @requireAuth
|
|
78
|
+
}
|
|
79
|
+
`
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import {
|
|
2
|
+
confirmUpload as confirmUploadRecord,
|
|
3
|
+
createPresignedUpload,
|
|
4
|
+
getUploadTokenPayload,
|
|
5
|
+
isMimeTypeAllowed,
|
|
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
|
+
if (!isMimeTypeAllowed(profile.allowedMimeTypes, input.mimeType)) {
|
|
75
|
+
throw new UploadError(
|
|
76
|
+
'MIME_TYPE_NOT_ALLOWED',
|
|
77
|
+
`File type '${input.mimeType}' is not allowed for upload profile '${profile.name}'.`,
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return storeFile(target, {
|
|
82
|
+
db,
|
|
83
|
+
filename: input.filename,
|
|
84
|
+
mimeType: input.mimeType,
|
|
85
|
+
data: Buffer.from(input.data, 'base64'),
|
|
86
|
+
maxSize: profile.maxFileSize,
|
|
87
|
+
userId: context.currentUser ? String(context.currentUser.id) : undefined,
|
|
88
|
+
})
|
|
89
|
+
}
|
|
@@ -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 prompts from "prompts";
|
|
1
2
|
import { recordTelemetryAttributes } from "@cedarjs/cli-helpers/telemetry";
|
|
3
|
+
import { requireTTYOrExit } from "@cedarjs/cli-helpers/tty";
|
|
2
4
|
const command = "uploads";
|
|
3
|
-
const description = "
|
|
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("
|
|
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
|
-
|
|
16
|
-
|
|
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,91 +1,265 @@
|
|
|
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";
|
|
6
7
|
import { colors as c } from "@cedarjs/cli-helpers/colors";
|
|
7
8
|
import { getPrettierOptions } from "@cedarjs/cli-helpers/fileHelpers";
|
|
8
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
addApiPackages,
|
|
11
|
+
addWebPackages
|
|
12
|
+
} from "@cedarjs/cli-helpers/installHelpers";
|
|
9
13
|
import { formatCedarCommand } from "@cedarjs/cli-helpers/packageManager/display";
|
|
14
|
+
import { addEnvVar } from "@cedarjs/cli-helpers/project";
|
|
15
|
+
import { getSchemaPath } from "@cedarjs/project-config";
|
|
10
16
|
import { errorTelemetry } from "@cedarjs/telemetry";
|
|
11
17
|
import { getPaths, transformTSToJS, writeFile } from "../../../lib/index.js";
|
|
12
18
|
import { isTypeScriptProject } from "../../../lib/project.js";
|
|
13
|
-
import {
|
|
14
|
-
|
|
19
|
+
import { generateSecret } from "../../generate/secret/secret.js";
|
|
20
|
+
import { setupServerFileTasks } from "../server-file/serverFileHandler.js";
|
|
21
|
+
import { addUploadModel } from "./schemaPrisma.js";
|
|
22
|
+
import {
|
|
23
|
+
addUploadsPlugin,
|
|
24
|
+
detectServerAuth,
|
|
25
|
+
hasUploadsPlugin
|
|
26
|
+
} from "./serverFile.js";
|
|
27
|
+
const UPPY_VERSION = "^6.0.0";
|
|
28
|
+
const S3_IMPORTS = `import { S3Client } from '@aws-sdk/client-s3'
|
|
29
|
+
|
|
30
|
+
import { createS3Provider } from '@cedarjs/uploads/s3'
|
|
31
|
+
`;
|
|
32
|
+
const S3_CLIENT = `
|
|
33
|
+
// Your own S3 client. It stays available for S3-specific work (lifecycle
|
|
34
|
+
// rules, CORS, and so on) alongside the storage target below. Credentials
|
|
35
|
+
// come from the SDK's default chain: AWS_ACCESS_KEY_ID and
|
|
36
|
+
// AWS_SECRET_ACCESS_KEY in .env, or an IAM role, SSO, or container
|
|
37
|
+
// credentials in production. For an S3-compatible service, add \`endpoint\`
|
|
38
|
+
// and \`forcePathStyle: true\`.
|
|
39
|
+
export const s3Client = new S3Client({
|
|
40
|
+
region: process.env.AWS_REGION,
|
|
41
|
+
})
|
|
42
|
+
`;
|
|
43
|
+
const TARGET_BLOCKS = {
|
|
44
|
+
fs: ` // Files uploaded through the api server and stored on local disk. Good for
|
|
45
|
+
// development and single-server deploys.
|
|
46
|
+
local: createFsProvider({
|
|
47
|
+
uploadDir: path.join(getPaths().api.base, '.uploads'),
|
|
48
|
+
serveBaseUrl: process.env.UPLOAD_SERVE_BASE_URL,
|
|
49
|
+
signSecret: process.env.UPLOAD_TOKEN_SECRET,
|
|
50
|
+
}),
|
|
51
|
+
`,
|
|
52
|
+
db: ` // Small files stored inline in the Upload row (avatars, thumbnails).
|
|
53
|
+
thumbnails: createDbProvider(),
|
|
54
|
+
`,
|
|
55
|
+
s3: ` // Files uploaded straight from the browser to S3 with presigned URLs.
|
|
56
|
+
files: createS3Provider({
|
|
57
|
+
client: s3Client,
|
|
58
|
+
bucket: process.env.S3_BUCKET_UPLOADS,
|
|
59
|
+
region: process.env.AWS_REGION,
|
|
60
|
+
keyPrefix: 'uploads/',
|
|
61
|
+
}),
|
|
62
|
+
`
|
|
63
|
+
};
|
|
64
|
+
const PROFILE_BLOCKS = {
|
|
65
|
+
fs: ` attachment: {
|
|
66
|
+
target: 'local',
|
|
67
|
+
allowedMimeTypes: ['application/pdf', 'image/png', 'image/jpeg'],
|
|
68
|
+
maxFileSize: 25 * 1024 * 1024,
|
|
69
|
+
maxFiles: 10,
|
|
70
|
+
},
|
|
71
|
+
`,
|
|
72
|
+
db: ` avatar: {
|
|
73
|
+
target: 'thumbnails',
|
|
74
|
+
allowedMimeTypes: ['image/png', 'image/jpeg'],
|
|
75
|
+
maxFileSize: DB_MAX_FILE_SIZE,
|
|
76
|
+
maxFiles: 1,
|
|
77
|
+
},
|
|
78
|
+
`,
|
|
79
|
+
s3: ` document: {
|
|
80
|
+
target: 'files',
|
|
81
|
+
allowedMimeTypes: ['application/pdf', 'image/*'],
|
|
82
|
+
maxFileSize: 100 * 1024 * 1024,
|
|
83
|
+
maxFiles: 5,
|
|
84
|
+
},
|
|
85
|
+
`
|
|
86
|
+
};
|
|
87
|
+
const handler = async ({ targets, force }) => {
|
|
88
|
+
const paths = getPaths();
|
|
15
89
|
const projectIsTypescript = isTypeScriptProject();
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
90
|
+
const ext = projectIsTypescript ? "ts" : "js";
|
|
91
|
+
const wantsFs = targets.includes("fs");
|
|
92
|
+
const wantsDb = targets.includes("db");
|
|
93
|
+
const wantsS3 = targets.includes("s3");
|
|
94
|
+
const wantsUppy = wantsFs || wantsS3;
|
|
95
|
+
const packageJson = await import(pathToFileURL(path.join(paths.base, "package.json")).href, { with: { type: "json" } });
|
|
19
96
|
const cedarVersion = packageJson.default.devDependencies["@cedarjs/core"] ?? "latest";
|
|
97
|
+
const serverFilePath = path.join(paths.api.src, `server.${ext}`);
|
|
98
|
+
const writeTemplateFile = async (templateFile, outputPath, replacements = {}) => {
|
|
99
|
+
const templatePath = path.resolve(
|
|
100
|
+
import.meta.dirname,
|
|
101
|
+
"templates",
|
|
102
|
+
templateFile
|
|
103
|
+
);
|
|
104
|
+
let templateContent = fs.readFileSync(templatePath, "utf-8");
|
|
105
|
+
for (const [token, value] of Object.entries(replacements)) {
|
|
106
|
+
templateContent = templateContent.replaceAll(token, value);
|
|
107
|
+
}
|
|
108
|
+
const content = projectIsTypescript ? templateContent : await transformTSToJS(outputPath, templateContent);
|
|
109
|
+
return writeFile(outputPath, content, { overwriteExisting: force });
|
|
110
|
+
};
|
|
111
|
+
const apiPackages = [`@cedarjs/uploads@${cedarVersion}`];
|
|
112
|
+
const webPackages = [`@cedarjs/uploads@${cedarVersion}`];
|
|
113
|
+
if (wantsS3) {
|
|
114
|
+
apiPackages.push("@aws-sdk/client-s3", "@aws-sdk/s3-request-presigner");
|
|
115
|
+
}
|
|
116
|
+
if (wantsUppy) {
|
|
117
|
+
webPackages.push(
|
|
118
|
+
`@uppy/core@${UPPY_VERSION}`,
|
|
119
|
+
`@uppy/react@${UPPY_VERSION}`,
|
|
120
|
+
`@uppy/dashboard@${UPPY_VERSION}`
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
if (wantsFs) {
|
|
124
|
+
webPackages.push(`@uppy/xhr-upload@${UPPY_VERSION}`);
|
|
125
|
+
}
|
|
126
|
+
if (wantsS3) {
|
|
127
|
+
webPackages.push(`@uppy/aws-s3@${UPPY_VERSION}`);
|
|
128
|
+
}
|
|
20
129
|
const tasks = new Listr(
|
|
21
130
|
[
|
|
22
131
|
{
|
|
23
|
-
|
|
132
|
+
...addApiPackages(apiPackages),
|
|
133
|
+
title: "Adding @cedarjs/uploads to your api side..."
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
...addWebPackages(webPackages),
|
|
137
|
+
title: "Adding @cedarjs/uploads to your web side..."
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
title: "Adding the Upload model to schema.prisma...",
|
|
24
141
|
task: async () => {
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
142
|
+
const schemaPath = await getSchemaPath(paths.api.prismaConfig);
|
|
143
|
+
const schema = fs.readFileSync(schemaPath, "utf-8");
|
|
144
|
+
fs.writeFileSync(schemaPath, addUploadModel(schema));
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
title: `Adding api/src/lib/uploads.${ext}...`,
|
|
149
|
+
task: async () => {
|
|
150
|
+
await writeTemplateFile(
|
|
151
|
+
"srcLibUploads.ts.template",
|
|
152
|
+
path.join(paths.api.lib, `uploads.${ext}`),
|
|
153
|
+
{
|
|
154
|
+
__S3_IMPORTS__: wantsS3 ? S3_IMPORTS : "",
|
|
155
|
+
__S3_CLIENT__: wantsS3 ? S3_CLIENT : "",
|
|
156
|
+
__FS_IMPORT__: wantsFs ? "createFsProvider," : "",
|
|
157
|
+
__DB_IMPORT__: wantsDb ? "createDbProvider," : "",
|
|
158
|
+
__DB_MAX_IMPORT__: wantsDb ? "DB_MAX_FILE_SIZE," : "",
|
|
159
|
+
__PATH_IMPORT__: wantsFs ? "import path from 'node:path'\n\n" : "",
|
|
160
|
+
__GET_PATHS_IMPORT__: wantsFs ? "import { getPaths } from '@cedarjs/project-config'\n" : "",
|
|
161
|
+
__TARGETS__: targets.map((t) => TARGET_BLOCKS[t]).join("\n"),
|
|
162
|
+
__PROFILES__: targets.map((t) => PROFILE_BLOCKS[t]).join("\n")
|
|
163
|
+
}
|
|
37
164
|
);
|
|
38
|
-
const uploadsContent = projectIsTypescript ? templateContent : await transformTSToJS(uploadsPath, templateContent);
|
|
39
|
-
return writeFile(uploadsPath, uploadsContent, {
|
|
40
|
-
overwriteExisting: force
|
|
41
|
-
});
|
|
42
165
|
}
|
|
43
166
|
},
|
|
44
167
|
{
|
|
45
|
-
title:
|
|
168
|
+
title: "Adding the uploads SDL and service...",
|
|
46
169
|
task: async () => {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
"signedUrl.ts.template"
|
|
170
|
+
await writeTemplateFile(
|
|
171
|
+
"uploads.sdl.ts.template",
|
|
172
|
+
path.join(paths.api.graphql, `uploads.sdl.${ext}`)
|
|
51
173
|
);
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
});
|
|
56
|
-
const uploadsPath = path.join(
|
|
57
|
-
getPaths().api.functions,
|
|
58
|
-
`signedUrl.${projectIsTypescript ? "ts" : "js"}`
|
|
174
|
+
await writeTemplateFile(
|
|
175
|
+
"uploads.ts.template",
|
|
176
|
+
path.join(paths.api.services, "uploads", `uploads.${ext}`)
|
|
59
177
|
);
|
|
60
|
-
const uploadsContent = projectIsTypescript ? templateContent : await transformTSToJS(uploadsPath, templateContent);
|
|
61
|
-
return writeFile(uploadsPath, uploadsContent, {
|
|
62
|
-
overwriteExisting: force
|
|
63
|
-
});
|
|
64
178
|
}
|
|
65
179
|
},
|
|
66
180
|
{
|
|
67
|
-
...
|
|
68
|
-
|
|
181
|
+
title: "Adding the upload directives...",
|
|
182
|
+
task: async () => {
|
|
183
|
+
const directives = ["requireUploadToken", "withSignedUrl"];
|
|
184
|
+
if (wantsDb) {
|
|
185
|
+
directives.push("withDataUri");
|
|
186
|
+
}
|
|
187
|
+
for (const name of directives) {
|
|
188
|
+
await writeTemplateFile(
|
|
189
|
+
`${name}.ts.template`,
|
|
190
|
+
path.join(paths.api.directives, name, `${name}.${ext}`)
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
title: "Adding the server file...",
|
|
197
|
+
task: (_ctx, task) => task.newListr(setupServerFileTasks({ force: false })),
|
|
198
|
+
skip: () => fs.existsSync(serverFilePath) ? "api/src/server already exists; skipping." : false
|
|
69
199
|
},
|
|
70
200
|
{
|
|
71
|
-
title: "
|
|
72
|
-
task: async () => {
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
);
|
|
86
|
-
}
|
|
201
|
+
title: "Registering the upload plugin in api/src/server...",
|
|
202
|
+
task: async (_ctx, task) => {
|
|
203
|
+
const source = fs.readFileSync(serverFilePath, "utf-8");
|
|
204
|
+
if (hasUploadsPlugin(source)) {
|
|
205
|
+
task.skip("The upload plugin is already registered; skipping.");
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
const graphqlPath = path.join(paths.api.functions, `graphql.${ext}`);
|
|
209
|
+
const authPath = path.join(paths.api.lib, `auth.${ext}`);
|
|
210
|
+
const auth = fs.existsSync(graphqlPath) && fs.existsSync(authPath) && fs.readFileSync(authPath, "utf-8").includes("getCurrentUser") ? detectServerAuth(fs.readFileSync(graphqlPath, "utf-8")) : null;
|
|
211
|
+
let updated;
|
|
212
|
+
try {
|
|
213
|
+
updated = addUploadsPlugin(source, { auth });
|
|
214
|
+
} catch {
|
|
87
215
|
throw new Error(
|
|
88
|
-
|
|
216
|
+
`Could not find \`await server.start()\` in ${path.relative(
|
|
217
|
+
paths.base,
|
|
218
|
+
serverFilePath
|
|
219
|
+
)}. Register the plugin by hand before the server starts:
|
|
220
|
+
|
|
221
|
+
import { cedarUploadsPlugin } from '@cedarjs/uploads'
|
|
222
|
+
import { db } from 'src/lib/db'
|
|
223
|
+
import { targets } from 'src/lib/uploads'
|
|
224
|
+
|
|
225
|
+
await server.register(cedarUploadsPlugin, {
|
|
226
|
+
tokenSecret: process.env.UPLOAD_TOKEN_SECRET,
|
|
227
|
+
targets,
|
|
228
|
+
db,
|
|
229
|
+
})
|
|
230
|
+
`
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
fs.writeFileSync(serverFilePath, updated);
|
|
234
|
+
}
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
title: "Adding environment variables to .env...",
|
|
238
|
+
task: () => {
|
|
239
|
+
addEnvVar(
|
|
240
|
+
"UPLOAD_TOKEN_SECRET",
|
|
241
|
+
generateSecret(),
|
|
242
|
+
"Signs upload tokens and file-serving URLs. Keep it secret and use a different value per environment."
|
|
243
|
+
);
|
|
244
|
+
if (wantsFs) {
|
|
245
|
+
addEnvVar(
|
|
246
|
+
"UPLOAD_SERVE_BASE_URL",
|
|
247
|
+
"http://localhost:8911",
|
|
248
|
+
"Origin the api server is reachable at, used to build signed URLs for locally stored files."
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
if (wantsS3) {
|
|
252
|
+
addEnvVar(
|
|
253
|
+
"AWS_REGION",
|
|
254
|
+
"us-east-1",
|
|
255
|
+
"Region of the uploads bucket."
|
|
256
|
+
);
|
|
257
|
+
addEnvVar("AWS_ACCESS_KEY_ID", "replace-me", "");
|
|
258
|
+
addEnvVar("AWS_SECRET_ACCESS_KEY", "replace-me", "");
|
|
259
|
+
addEnvVar(
|
|
260
|
+
"S3_BUCKET_UPLOADS",
|
|
261
|
+
"replace-me",
|
|
262
|
+
"Bucket direct uploads land in."
|
|
89
263
|
);
|
|
90
264
|
}
|
|
91
265
|
}
|
|
@@ -94,10 +268,10 @@ const handler = async ({ force }) => {
|
|
|
94
268
|
title: "Prettifying changed files",
|
|
95
269
|
task: async (_ctx, task) => {
|
|
96
270
|
const prettifyPaths = [
|
|
97
|
-
|
|
98
|
-
path.join(
|
|
99
|
-
path.join(
|
|
100
|
-
path.join(
|
|
271
|
+
serverFilePath,
|
|
272
|
+
path.join(paths.api.lib, `uploads.${ext}`),
|
|
273
|
+
path.join(paths.api.graphql, `uploads.sdl.${ext}`),
|
|
274
|
+
path.join(paths.api.services, "uploads", `uploads.${ext}`)
|
|
101
275
|
];
|
|
102
276
|
for (const prettifyPath of prettifyPaths) {
|
|
103
277
|
try {
|
|
@@ -106,11 +280,11 @@ const handler = async ({ force }) => {
|
|
|
106
280
|
}
|
|
107
281
|
const source = fs.readFileSync(prettifyPath, "utf-8");
|
|
108
282
|
const prettierOptions = await getPrettierOptions();
|
|
109
|
-
const
|
|
283
|
+
const prettified = await format(source, {
|
|
110
284
|
...prettierOptions,
|
|
111
285
|
parser: "babel-ts"
|
|
112
286
|
});
|
|
113
|
-
fs.writeFileSync(prettifyPath,
|
|
287
|
+
fs.writeFileSync(prettifyPath, prettified, "utf-8");
|
|
114
288
|
} catch {
|
|
115
289
|
task.output = "Couldn't prettify the changes. Please reformat the files manually if needed.";
|
|
116
290
|
}
|
|
@@ -120,15 +294,30 @@ const handler = async ({ force }) => {
|
|
|
120
294
|
{
|
|
121
295
|
title: "One more thing...",
|
|
122
296
|
task: (_ctx, task) => {
|
|
297
|
+
const migrate = c.highlight(
|
|
298
|
+
formatCedarCommand(["prisma", "migrate", "dev"])
|
|
299
|
+
);
|
|
300
|
+
const types = c.highlight(formatCedarCommand(["generate", "types"]));
|
|
123
301
|
task.title = `One more thing...
|
|
124
302
|
|
|
125
|
-
${c.success("
|
|
303
|
+
${c.success("Uploads configured!")}
|
|
304
|
+
|
|
305
|
+
Next steps:
|
|
306
|
+
|
|
307
|
+
1. Apply the schema change: ${migrate}
|
|
308
|
+
2. Regenerate types: ${types}
|
|
309
|
+
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" : ""}
|
|
126
310
|
|
|
127
|
-
|
|
311
|
+
Upload tokens are bound to the logged-in user when the app has auth.
|
|
312
|
+
If you add auth later, pass
|
|
313
|
+
\`authenticate: createUploadAuthenticator({ authDecoder, getCurrentUser })\`
|
|
314
|
+
to the plugin in api/src/server.${ext}.
|
|
315
|
+
|
|
316
|
+
Schedule cleanupStaleUploads() from a recurring job to sweep uploads
|
|
317
|
+
that never completed.
|
|
128
318
|
|
|
129
319
|
Check out the docs for more info:
|
|
130
320
|
${terminalLink("", "https://cedarjs.com/docs/uploads")}
|
|
131
|
-
|
|
132
321
|
`;
|
|
133
322
|
}
|
|
134
323
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cedarjs/cli",
|
|
3
|
-
"version": "7.0.0-canary.
|
|
3
|
+
"version": "7.0.0-canary.3115",
|
|
4
4
|
"description": "The CedarJS Command Line",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -36,17 +36,17 @@
|
|
|
36
36
|
"@babel/preset-typescript": "7.29.7",
|
|
37
37
|
"@babel/traverse": "7.29.8",
|
|
38
38
|
"@babel/types": "7.29.8",
|
|
39
|
-
"@cedarjs/api-server": "7.0.0-canary.
|
|
40
|
-
"@cedarjs/babel-config": "7.0.0-canary.
|
|
41
|
-
"@cedarjs/cli-helpers": "7.0.0-canary.
|
|
42
|
-
"@cedarjs/internal": "7.0.0-canary.
|
|
43
|
-
"@cedarjs/prerender": "7.0.0-canary.
|
|
44
|
-
"@cedarjs/project-config": "7.0.0-canary.
|
|
45
|
-
"@cedarjs/structure": "7.0.0-canary.
|
|
46
|
-
"@cedarjs/telemetry": "7.0.0-canary.
|
|
47
|
-
"@cedarjs/utils": "7.0.0-canary.
|
|
48
|
-
"@cedarjs/vite": "7.0.0-canary.
|
|
49
|
-
"@cedarjs/web-server": "7.0.0-canary.
|
|
39
|
+
"@cedarjs/api-server": "7.0.0-canary.3115",
|
|
40
|
+
"@cedarjs/babel-config": "7.0.0-canary.3115",
|
|
41
|
+
"@cedarjs/cli-helpers": "7.0.0-canary.3115",
|
|
42
|
+
"@cedarjs/internal": "7.0.0-canary.3115",
|
|
43
|
+
"@cedarjs/prerender": "7.0.0-canary.3115",
|
|
44
|
+
"@cedarjs/project-config": "7.0.0-canary.3115",
|
|
45
|
+
"@cedarjs/structure": "7.0.0-canary.3115",
|
|
46
|
+
"@cedarjs/telemetry": "7.0.0-canary.3115",
|
|
47
|
+
"@cedarjs/utils": "7.0.0-canary.3115",
|
|
48
|
+
"@cedarjs/vite": "7.0.0-canary.3115",
|
|
49
|
+
"@cedarjs/web-server": "7.0.0-canary.3115",
|
|
50
50
|
"@listr2/prompt-adapter-enquirer": "4.3.2",
|
|
51
51
|
"@opentelemetry/api": "1.9.1",
|
|
52
52
|
"@opentelemetry/core": "1.30.1",
|
|
@@ -94,7 +94,7 @@
|
|
|
94
94
|
"yargs": "17.7.3"
|
|
95
95
|
},
|
|
96
96
|
"devDependencies": {
|
|
97
|
-
"@cedarjs/framework-tools": "7.0.0-canary.
|
|
97
|
+
"@cedarjs/framework-tools": "7.0.0-canary.3115",
|
|
98
98
|
"@prisma/dmmf": "7.10.0",
|
|
99
99
|
"@types/archiver": "^7.0.0",
|
|
100
100
|
"memfs": "4.68.2",
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import j from "jscodeshift";
|
|
2
|
-
function transform(fileInfo) {
|
|
3
|
-
const root = j(fileInfo.source);
|
|
4
|
-
const imports = root.find(j.ImportDeclaration);
|
|
5
|
-
imports.at(-1).insertAfter(
|
|
6
|
-
j.importDeclaration(
|
|
7
|
-
[j.importSpecifier(j.identifier("storagePrismaExtension"))],
|
|
8
|
-
j.literal("./uploads.js")
|
|
9
|
-
)
|
|
10
|
-
);
|
|
11
|
-
root.find(j.VariableDeclaration, { declarations: [{ id: { name: "db" } }] }).forEach((path) => {
|
|
12
|
-
const dbDeclaration = path.node.declarations[0];
|
|
13
|
-
if (j.VariableDeclarator.check(dbDeclaration) && j.NewExpression.check(dbDeclaration.init)) {
|
|
14
|
-
throw new Error("RW_CODEMOD_ERR_OLD_FORMAT");
|
|
15
|
-
}
|
|
16
|
-
if (j.VariableDeclarator.check(dbDeclaration) && j.Expression.check(dbDeclaration.init)) {
|
|
17
|
-
const newInit = j.callExpression(
|
|
18
|
-
j.memberExpression(dbDeclaration.init, j.identifier("$extends")),
|
|
19
|
-
[j.identifier("storagePrismaExtension")]
|
|
20
|
-
);
|
|
21
|
-
dbDeclaration.init = newInit;
|
|
22
|
-
}
|
|
23
|
-
});
|
|
24
|
-
return root.toSource();
|
|
25
|
-
}
|
|
26
|
-
export {
|
|
27
|
-
transform as default
|
|
28
|
-
};
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import type { APIGatewayEvent, Context } from 'aws-lambda'
|
|
2
|
-
|
|
3
|
-
import type { SignatureValidationArgs } from '@cedarjs/storage/UrlSigner'
|
|
4
|
-
|
|
5
|
-
import { urlSigner, fsStorage } from 'src/lib/uploads'
|
|
6
|
-
|
|
7
|
-
export const handler = async (event: APIGatewayEvent, _context: Context) => {
|
|
8
|
-
const fileToReturn = urlSigner.validateSignature(
|
|
9
|
-
event.queryStringParameters as SignatureValidationArgs
|
|
10
|
-
)
|
|
11
|
-
|
|
12
|
-
const { contents, type } = await fsStorage.read(fileToReturn)
|
|
13
|
-
|
|
14
|
-
return {
|
|
15
|
-
statusCode: 200,
|
|
16
|
-
headers: {
|
|
17
|
-
'Content-Type': type,
|
|
18
|
-
},
|
|
19
|
-
body: contents,
|
|
20
|
-
}
|
|
21
|
-
}
|