@cedarjs/cli 6.0.0-rc.189 → 6.0.0-rc.241
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/dev.js +1 -4
- package/dist/commands/experimental/setupOpentelemetryHandler.js +1 -1
- package/dist/commands/generate/helpers.js +16 -0
- package/dist/commands/generate/scaffold/scaffoldHandler.js +86 -8
- package/dist/commands/generate/sdl/sdlHandler.js +104 -11
- package/dist/commands/generate/sdl/stubFiles.js +132 -0
- package/dist/commands/generate/sdl/templates/sdl.js.template +2 -1
- package/dist/commands/generate/sdl/templates/sdl.ts.template +2 -1
- package/dist/commands/generate/service/serviceHandler.js +17 -6
- package/dist/commands/prismaHandler.js +3 -3
- package/dist/commands/serve.js +19 -9
- package/dist/commands/serveBothHandler.js +2 -2
- package/dist/commands/setup/database/database.js +15 -0
- package/dist/commands/setup/database/postgres.js +19 -0
- package/dist/commands/setup/database/postgresHandler.js +194 -0
- package/dist/commands/setup/deploy/providers/renderHandler.js +4 -20
- package/dist/commands/setup/deploy/templates/render.js +24 -14
- package/dist/commands/setup/docker/templates/Dockerfile.yarn +2 -5
- package/dist/commands/setup/docker/templates/docker-compose.dev.yml +1 -1
- package/dist/commands/setup/docker/templates/docker-compose.prod.yml +2 -5
- package/dist/commands/setup/neon/neonHandler.js +96 -254
- package/dist/commands/setup/uploads/uploadsHandler.js +5 -4
- package/dist/commands/setup.js +2 -1
- package/dist/lib/index.js +22 -6
- package/dist/telemetry/resource.js +3 -6
- package/package.json +16 -16
- /package/dist/commands/setup/{neon → database}/templates/db.ts.template +0 -0
package/dist/commands/dev.js
CHANGED
|
@@ -13,10 +13,7 @@ const builder = (yargs) => {
|
|
|
13
13
|
}).option("forward", {
|
|
14
14
|
alias: "fwd",
|
|
15
15
|
description: 'String of one or more vite dev server config options, for example: `--fwd="--port=1234 --open=false"`',
|
|
16
|
-
type: "string"
|
|
17
|
-
// The reason `forward` is hidden is that it's been broken with Vite and
|
|
18
|
-
// it's not clear how to fix it.
|
|
19
|
-
hidden: true
|
|
16
|
+
type: "string"
|
|
20
17
|
}).option("generate", {
|
|
21
18
|
type: "boolean",
|
|
22
19
|
default: true,
|
|
@@ -114,7 +114,7 @@ const handler = async ({
|
|
|
114
114
|
},
|
|
115
115
|
task: (_ctx, task) => {
|
|
116
116
|
task.output = [
|
|
117
|
-
"Please add the following to your '
|
|
117
|
+
"Please add the following to your 'cedarFastifyGraphQLServer' plugin options to enable OTel for your graphql",
|
|
118
118
|
"openTelemetryOptions: {",
|
|
119
119
|
" resolvers: true,",
|
|
120
120
|
" result: true,",
|
|
@@ -23,6 +23,20 @@ const validateName = (name) => {
|
|
|
23
23
|
);
|
|
24
24
|
}
|
|
25
25
|
};
|
|
26
|
+
const SENSITIVE_FIELDS = [
|
|
27
|
+
"hashedPassword",
|
|
28
|
+
"salt",
|
|
29
|
+
"resetToken",
|
|
30
|
+
"resetTokenExpiresAt",
|
|
31
|
+
"webAuthnChallenge"
|
|
32
|
+
];
|
|
33
|
+
const redactedModelFields = (fieldNames) => {
|
|
34
|
+
const sensitive = fieldNames.filter((name) => SENSITIVE_FIELDS.includes(name));
|
|
35
|
+
if (sensitive.length === 1 && sensitive[0] === "salt") {
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
return sensitive;
|
|
39
|
+
};
|
|
26
40
|
const relationsForModel = (model) => {
|
|
27
41
|
return model?.fields.filter((f) => f.relationName).map((field) => {
|
|
28
42
|
return field.name;
|
|
@@ -60,11 +74,13 @@ const mapPrismaScalarToPagePropTsType = (scalarType) => {
|
|
|
60
74
|
return prismaScalarToTsType[scalarType] || "unknown";
|
|
61
75
|
};
|
|
62
76
|
export {
|
|
77
|
+
SENSITIVE_FIELDS,
|
|
63
78
|
forcePluralizeWord,
|
|
64
79
|
intForeignKeysForModel,
|
|
65
80
|
mapPrismaScalarToPagePropTsType,
|
|
66
81
|
mapRouteParamTypeToTsType,
|
|
67
82
|
pathName,
|
|
83
|
+
redactedModelFields,
|
|
68
84
|
relationsForModel,
|
|
69
85
|
removeGeneratorName,
|
|
70
86
|
validateName
|
|
@@ -19,7 +19,6 @@ import {
|
|
|
19
19
|
writeFile,
|
|
20
20
|
getDefaultArgs,
|
|
21
21
|
getPaths,
|
|
22
|
-
writeFilesTask,
|
|
23
22
|
addRoutesToRouterTask,
|
|
24
23
|
addScaffoldImport,
|
|
25
24
|
transformTSToJS,
|
|
@@ -31,17 +30,24 @@ import {
|
|
|
31
30
|
} from "../../../lib/rollback.js";
|
|
32
31
|
import { getSchema, verifyModelName } from "../../../lib/schemaHelpers.js";
|
|
33
32
|
import {
|
|
33
|
+
redactedModelFields,
|
|
34
34
|
relationsForModel,
|
|
35
35
|
intForeignKeysForModel,
|
|
36
36
|
mapPrismaScalarToPagePropTsType
|
|
37
37
|
} from "../helpers.js";
|
|
38
38
|
import { builder as sdlBuilder } from "../sdl/sdl.js";
|
|
39
|
-
import {
|
|
39
|
+
import {
|
|
40
|
+
files as sdlFiles,
|
|
41
|
+
printRedactedFieldsNote,
|
|
42
|
+
redactedSensitiveFields
|
|
43
|
+
} from "../sdl/sdlHandler.js";
|
|
44
|
+
import { writeFilesWithStubsTask } from "../sdl/stubFiles.js";
|
|
40
45
|
import { builder as serviceBuilder } from "../service/service.js";
|
|
41
46
|
import { files as serviceFiles } from "../service/serviceHandler.js";
|
|
42
47
|
import { customOrDefaultTemplatePath } from "../yargsHandlerHelpers.js";
|
|
43
48
|
const SKIPPABLE_ASSETS = ["scaffold.css"];
|
|
44
49
|
const PACKAGE_SET = "Set";
|
|
50
|
+
const PACKAGE_PRIVATE_SET = "PrivateSet";
|
|
45
51
|
const getIdType = (model) => {
|
|
46
52
|
return model.fields.find((field) => field.isId)?.type;
|
|
47
53
|
};
|
|
@@ -272,7 +278,10 @@ const modelRelatedVariables = (model) => {
|
|
|
272
278
|
}
|
|
273
279
|
};
|
|
274
280
|
const relations = relationsForModel(model)?.map((relation) => relation);
|
|
275
|
-
const
|
|
281
|
+
const redactedFields = redactedModelFields(
|
|
282
|
+
model.fields.map((field) => field.name)
|
|
283
|
+
);
|
|
284
|
+
const columns = model.fields.filter((field) => field.kind !== "object").filter((field) => !redactedFields.includes(field.name)).map((column) => {
|
|
276
285
|
let validation;
|
|
277
286
|
const meta = componentMetadata[column.type];
|
|
278
287
|
const validationDef = meta?.validation;
|
|
@@ -512,6 +521,65 @@ const addHelperPackages = async (task) => {
|
|
|
512
521
|
await removeWorkspacePackages("web", ["humanize-string"]);
|
|
513
522
|
});
|
|
514
523
|
};
|
|
524
|
+
const isAuthSetup = () => {
|
|
525
|
+
const extensions = ["ts", "js", "tsx", "jsx"];
|
|
526
|
+
return extensions.some(
|
|
527
|
+
(ext) => fs.existsSync(path.join(getPaths().web.src, "auth." + ext))
|
|
528
|
+
);
|
|
529
|
+
};
|
|
530
|
+
const ROUTE_TAG_RE = /<Route\s+([^>]*?)\/?>/g;
|
|
531
|
+
const extractRouteAttr = (tagAttrs, attrName) => tagAttrs.match(new RegExp(`\\b${attrName}=["']([^"']+)["']`))?.[1];
|
|
532
|
+
const getRoutesFileContent = () => {
|
|
533
|
+
const routesPath = getPaths().web.routes;
|
|
534
|
+
if (!fs.existsSync(routesPath)) {
|
|
535
|
+
return void 0;
|
|
536
|
+
}
|
|
537
|
+
return readFile(routesPath).toString();
|
|
538
|
+
};
|
|
539
|
+
const hasLoginRoute = () => {
|
|
540
|
+
const content = getRoutesFileContent();
|
|
541
|
+
if (!content) {
|
|
542
|
+
return false;
|
|
543
|
+
}
|
|
544
|
+
return Array.from(content.matchAll(ROUTE_TAG_RE)).some(
|
|
545
|
+
([, attrs]) => extractRouteAttr(attrs, "name") === "login"
|
|
546
|
+
);
|
|
547
|
+
};
|
|
548
|
+
const findUnprotectedLandingPageRouteName = () => {
|
|
549
|
+
const content = getRoutesFileContent();
|
|
550
|
+
if (!content) {
|
|
551
|
+
return void 0;
|
|
552
|
+
}
|
|
553
|
+
const privateSetRanges = Array.from(
|
|
554
|
+
content.matchAll(/<PrivateSet\b[^>]*>([\s\S]*?)<\/PrivateSet>/g)
|
|
555
|
+
).map((match) => ({
|
|
556
|
+
start: match.index ?? 0,
|
|
557
|
+
end: (match.index ?? 0) + match[0].length
|
|
558
|
+
}));
|
|
559
|
+
for (const match of content.matchAll(ROUTE_TAG_RE)) {
|
|
560
|
+
const [, attrs] = match;
|
|
561
|
+
if (extractRouteAttr(attrs, "path") !== "/") {
|
|
562
|
+
continue;
|
|
563
|
+
}
|
|
564
|
+
const tagStart = match.index ?? 0;
|
|
565
|
+
const isProtected = privateSetRanges.some(
|
|
566
|
+
(range) => tagStart >= range.start && tagStart < range.end
|
|
567
|
+
);
|
|
568
|
+
if (!isProtected) {
|
|
569
|
+
return extractRouteAttr(attrs, "name");
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
return void 0;
|
|
573
|
+
};
|
|
574
|
+
const getUnauthenticatedRedirectRoute = () => {
|
|
575
|
+
if (!isAuthSetup()) {
|
|
576
|
+
return void 0;
|
|
577
|
+
}
|
|
578
|
+
if (hasLoginRoute()) {
|
|
579
|
+
return "login";
|
|
580
|
+
}
|
|
581
|
+
return findUnprotectedLandingPageRouteName();
|
|
582
|
+
};
|
|
515
583
|
const addSetImport = (task) => {
|
|
516
584
|
const routesPath = getPaths().web.routes;
|
|
517
585
|
const routesContent = readFile(routesPath).toString();
|
|
@@ -525,15 +593,19 @@ const addSetImport = (task) => {
|
|
|
525
593
|
return void 0;
|
|
526
594
|
}
|
|
527
595
|
const routerImports = importContent.replace(/\s/g, "").split(",");
|
|
528
|
-
|
|
596
|
+
const namesToImport = [
|
|
597
|
+
PACKAGE_SET,
|
|
598
|
+
...getUnauthenticatedRedirectRoute() ? [PACKAGE_PRIVATE_SET] : []
|
|
599
|
+
].filter((name) => !routerImports.includes(name));
|
|
600
|
+
if (!namesToImport.length) {
|
|
529
601
|
return "Skipping Set import";
|
|
530
602
|
}
|
|
531
603
|
const newRoutesContent = routesContent.replace(
|
|
532
604
|
cedarRouterImport,
|
|
533
|
-
importStart + spacing +
|
|
605
|
+
importStart + spacing + namesToImport.join("," + spacing) + `,` + spacing + importContent + importEnd
|
|
534
606
|
);
|
|
535
607
|
writeFile(routesPath, newRoutesContent, { overwriteExisting: true });
|
|
536
|
-
return
|
|
608
|
+
return `Added ${namesToImport.join(", ")} import to Routes.{jsx,tsx}`;
|
|
537
609
|
};
|
|
538
610
|
const addScaffoldSetToRouter = async (model, scaffoldPath) => {
|
|
539
611
|
const templateNames = getTemplateStrings(model, scaffoldPath);
|
|
@@ -542,10 +614,12 @@ const addScaffoldSetToRouter = async (model, scaffoldPath) => {
|
|
|
542
614
|
const titleTo = templateNames.pluralRouteName;
|
|
543
615
|
const buttonLabel = `New ${nameVars.singularPascalName}`;
|
|
544
616
|
const buttonTo = templateNames.newRouteName;
|
|
617
|
+
const unauthenticatedRoute = getUnauthenticatedRedirectRoute();
|
|
545
618
|
return addRoutesToRouterTask(
|
|
546
619
|
await routes({ model, path: scaffoldPath }),
|
|
547
620
|
"ScaffoldLayout",
|
|
548
|
-
{ title, titleTo, buttonLabel, buttonTo }
|
|
621
|
+
{ title, titleTo, buttonLabel, buttonTo },
|
|
622
|
+
unauthenticatedRoute ? { unauthenticated: unauthenticatedRoute } : void 0
|
|
549
623
|
);
|
|
550
624
|
};
|
|
551
625
|
const tasks = ({
|
|
@@ -572,7 +646,7 @@ const tasks = ({
|
|
|
572
646
|
tailwind,
|
|
573
647
|
force
|
|
574
648
|
});
|
|
575
|
-
return
|
|
649
|
+
return writeFilesWithStubsTask(f, { overwriteExisting: force });
|
|
576
650
|
}
|
|
577
651
|
},
|
|
578
652
|
{
|
|
@@ -650,6 +724,7 @@ const handler = async ({
|
|
|
650
724
|
prepareForRollback(t);
|
|
651
725
|
}
|
|
652
726
|
await t.run();
|
|
727
|
+
printRedactedFieldsNote(await redactedSensitiveFields([name]));
|
|
653
728
|
} catch (e) {
|
|
654
729
|
const message = e instanceof Error ? e.message : String(e);
|
|
655
730
|
const exitCode = e instanceof Error && "exitCode" in e ? e.exitCode ?? 1 : 1;
|
|
@@ -664,7 +739,10 @@ const splitPathAndModel = (pathSlashModel) => {
|
|
|
664
739
|
};
|
|
665
740
|
export {
|
|
666
741
|
files,
|
|
742
|
+
getUnauthenticatedRedirectRoute,
|
|
667
743
|
handler,
|
|
744
|
+
hasLoginRoute,
|
|
745
|
+
isAuthSetup,
|
|
668
746
|
routes,
|
|
669
747
|
shouldUseTailwindCSS,
|
|
670
748
|
splitPathAndModel,
|
|
@@ -3,11 +3,12 @@ import boxen from "boxen";
|
|
|
3
3
|
import camelcase from "camelcase";
|
|
4
4
|
import { Listr } from "listr2";
|
|
5
5
|
import { recordTelemetryAttributes, colors as c } from "@cedarjs/cli-helpers";
|
|
6
|
+
import { formatCedarCommand } from "@cedarjs/cli-helpers/packageManager/display";
|
|
6
7
|
import { generate as generateTypes } from "@cedarjs/internal/dist/generate/generate";
|
|
7
8
|
import { getConfig } from "@cedarjs/project-config";
|
|
8
9
|
import { errorTelemetry } from "@cedarjs/telemetry";
|
|
9
10
|
import { pluralize } from "@cedarjs/utils/cedarPluralize";
|
|
10
|
-
import { transformTSToJS
|
|
11
|
+
import { transformTSToJS } from "../../../lib/index.js";
|
|
11
12
|
import {
|
|
12
13
|
prepareForRollback,
|
|
13
14
|
addFunctionToRollback
|
|
@@ -17,9 +18,14 @@ import {
|
|
|
17
18
|
getEnum,
|
|
18
19
|
verifyModelName
|
|
19
20
|
} from "../../../lib/schemaHelpers.js";
|
|
20
|
-
import { relationsForModel } from "../helpers.js";
|
|
21
|
+
import { redactedModelFields, relationsForModel } from "../helpers.js";
|
|
21
22
|
import { files as serviceFiles } from "../service/serviceHandler.js";
|
|
22
23
|
import { templateForFile } from "../yargsHandlerHelpers.js";
|
|
24
|
+
import {
|
|
25
|
+
addStubHeader,
|
|
26
|
+
missingRelatedModels,
|
|
27
|
+
writeFilesWithStubsTask
|
|
28
|
+
} from "./stubFiles.js";
|
|
23
29
|
const DEFAULT_IGNORE_FIELDS_FOR_INPUT = ["createdAt", "updatedAt"];
|
|
24
30
|
const missingIdConsoleMessage = () => {
|
|
25
31
|
const line1 = ansis.bold.yellow("WARNING") + ": Cannot generate CRUD SDL without an `@id` database column.";
|
|
@@ -63,16 +69,23 @@ const modelFieldToSDL = ({
|
|
|
63
69
|
return fieldContent;
|
|
64
70
|
}
|
|
65
71
|
};
|
|
72
|
+
const modelRedactedFields = (model) => {
|
|
73
|
+
return redactedModelFields(model.fields.map((field) => field.name));
|
|
74
|
+
};
|
|
66
75
|
const querySDL = (model, docs = false) => {
|
|
67
|
-
|
|
76
|
+
const redactedFields = modelRedactedFields(model);
|
|
77
|
+
return model.fields.filter((field) => !redactedFields.includes(field.name)).map((field) => modelFieldToSDL({ field, docs }));
|
|
68
78
|
};
|
|
69
79
|
const inputSDL = (model, required, docs = false) => {
|
|
70
|
-
const ignoredFields =
|
|
80
|
+
const ignoredFields = [
|
|
81
|
+
...DEFAULT_IGNORE_FIELDS_FOR_INPUT,
|
|
82
|
+
...modelRedactedFields(model)
|
|
83
|
+
];
|
|
84
|
+
const idField = model.fields.find((field) => field.isId);
|
|
85
|
+
if (idField?.default !== void 0) {
|
|
86
|
+
ignoredFields.push(idField.name);
|
|
87
|
+
}
|
|
71
88
|
return model.fields.filter((field) => {
|
|
72
|
-
const idField = model.fields.find((field2) => field2.isId);
|
|
73
|
-
if (idField?.default) {
|
|
74
|
-
ignoredFields.push(idField.name);
|
|
75
|
-
}
|
|
76
89
|
return !ignoredFields.includes(field.name) && field.kind !== "object";
|
|
77
90
|
}).map((field) => modelFieldToSDL({ field, required, docs }));
|
|
78
91
|
};
|
|
@@ -174,6 +187,55 @@ const files = async ({
|
|
|
174
187
|
})
|
|
175
188
|
};
|
|
176
189
|
};
|
|
190
|
+
const stubFiles = async (models, generatedFor, { docs = false, typescript }) => {
|
|
191
|
+
const generatedFiles = {};
|
|
192
|
+
for (const stubModel of models) {
|
|
193
|
+
const stubModelFiles = await files({
|
|
194
|
+
name: stubModel,
|
|
195
|
+
crud: false,
|
|
196
|
+
docs,
|
|
197
|
+
tests: false,
|
|
198
|
+
typescript
|
|
199
|
+
});
|
|
200
|
+
for (const [stubPath, stubContent] of Object.entries(stubModelFiles)) {
|
|
201
|
+
generatedFiles[stubPath] = addStubHeader({
|
|
202
|
+
content: stubContent,
|
|
203
|
+
stubModel,
|
|
204
|
+
generatedFor
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return generatedFiles;
|
|
209
|
+
};
|
|
210
|
+
const redactedSensitiveFields = async (modelNames) => {
|
|
211
|
+
const redacted = [];
|
|
212
|
+
for (const modelName of modelNames) {
|
|
213
|
+
const model = await getSchema(modelName);
|
|
214
|
+
if (!model || !("fields" in model)) {
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
for (const fieldName of modelRedactedFields(model)) {
|
|
218
|
+
redacted.push(`${model.name}.${fieldName}`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return redacted;
|
|
222
|
+
};
|
|
223
|
+
const printRedactedFieldsNote = (redactedFields) => {
|
|
224
|
+
if (redactedFields.length === 0) {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
console.log();
|
|
228
|
+
console.log(
|
|
229
|
+
c.warning(
|
|
230
|
+
`The following fields were excluded from the generated SDL because they may contain sensitive data: ${redactedFields.join(", ")}`
|
|
231
|
+
)
|
|
232
|
+
);
|
|
233
|
+
console.log(
|
|
234
|
+
c.warning(
|
|
235
|
+
"If you want to expose any of them through your GraphQL API you can add them to the SDL file manually."
|
|
236
|
+
)
|
|
237
|
+
);
|
|
238
|
+
};
|
|
177
239
|
const handler = async ({
|
|
178
240
|
model,
|
|
179
241
|
crud,
|
|
@@ -197,13 +259,21 @@ const handler = async ({
|
|
|
197
259
|
});
|
|
198
260
|
try {
|
|
199
261
|
const { name } = await verifyModelName({ name: model });
|
|
262
|
+
const missingModels = await missingRelatedModels(name);
|
|
263
|
+
const redactedFields = await redactedSensitiveFields([
|
|
264
|
+
name,
|
|
265
|
+
...missingModels
|
|
266
|
+
]);
|
|
200
267
|
const tasks = new Listr(
|
|
201
268
|
[
|
|
202
269
|
{
|
|
203
270
|
title: "Generating SDL files...",
|
|
204
271
|
task: async () => {
|
|
205
|
-
const f =
|
|
206
|
-
|
|
272
|
+
const f = {
|
|
273
|
+
...await files({ name, tests, crud, typescript, docs }),
|
|
274
|
+
...await stubFiles(missingModels, name, { typescript, docs })
|
|
275
|
+
};
|
|
276
|
+
return writeFilesWithStubsTask(f, { overwriteExisting: force });
|
|
207
277
|
}
|
|
208
278
|
},
|
|
209
279
|
{
|
|
@@ -230,6 +300,26 @@ const handler = async ({
|
|
|
230
300
|
prepareForRollback(tasks);
|
|
231
301
|
}
|
|
232
302
|
await tasks.run();
|
|
303
|
+
if (missingModels.length > 0) {
|
|
304
|
+
console.log();
|
|
305
|
+
console.log(
|
|
306
|
+
c.info(
|
|
307
|
+
`${name} has relations to models that don't have SDL files of their own yet: ${missingModels.join(", ")}`
|
|
308
|
+
)
|
|
309
|
+
);
|
|
310
|
+
console.log(
|
|
311
|
+
c.info(
|
|
312
|
+
"Read-only SDL stubs were generated for them, since GraphQL type generation fails otherwise."
|
|
313
|
+
)
|
|
314
|
+
);
|
|
315
|
+
console.log(c.info("To replace a stub with a full SDL and service, run"));
|
|
316
|
+
for (const stubModel of missingModels) {
|
|
317
|
+
console.log(
|
|
318
|
+
c.info(` ${formatCedarCommand(["generate", "sdl", stubModel])}`)
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
printRedactedFieldsNote(redactedFields);
|
|
233
323
|
} catch (e) {
|
|
234
324
|
const message = e instanceof Error ? e.message : String(e);
|
|
235
325
|
const exitCode = e instanceof Error && "exitCode" in e && typeof e.exitCode === "number" ? e.exitCode : 1;
|
|
@@ -240,5 +330,8 @@ const handler = async ({
|
|
|
240
330
|
};
|
|
241
331
|
export {
|
|
242
332
|
files,
|
|
243
|
-
handler
|
|
333
|
+
handler,
|
|
334
|
+
printRedactedFieldsNote,
|
|
335
|
+
redactedSensitiveFields,
|
|
336
|
+
stubFiles
|
|
244
337
|
};
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { Listr } from "listr2";
|
|
5
|
+
import { getPaths } from "@cedarjs/project-config";
|
|
6
|
+
import { writeFile } from "../../../lib/index.js";
|
|
7
|
+
import { getSchema } from "../../../lib/schemaHelpers.js";
|
|
8
|
+
const STUB_HASH_MARKER = "@cedar-generator-stub-hash";
|
|
9
|
+
const STUB_HASH_MARKER_REGEX = new RegExp(
|
|
10
|
+
`^// ${STUB_HASH_MARKER} ([0-9a-f]+)$`,
|
|
11
|
+
"m"
|
|
12
|
+
);
|
|
13
|
+
function stubHash(contents) {
|
|
14
|
+
return crypto.createHash("sha256").update(contents).digest("hex").slice(0, 16);
|
|
15
|
+
}
|
|
16
|
+
function addStubHeader({
|
|
17
|
+
content,
|
|
18
|
+
stubModel,
|
|
19
|
+
generatedFor
|
|
20
|
+
}) {
|
|
21
|
+
const body = "\n\n" + content;
|
|
22
|
+
const marker = `// ${STUB_HASH_MARKER} PLACEHOLDER`;
|
|
23
|
+
const header = [
|
|
24
|
+
`// Generated as a read-only stub by \`cedar generate sdl ${generatedFor}\`,`,
|
|
25
|
+
`// because ${generatedFor} has a relation to ${stubModel}, which had no SDL yet.`,
|
|
26
|
+
`// Run \`cedar generate sdl ${stubModel}\` to replace this stub with the real thing.`,
|
|
27
|
+
`// If you edit this file, the hash below will stop matching and you'll`,
|
|
28
|
+
`// need to pass \`--force\` to overwrite it.`,
|
|
29
|
+
marker
|
|
30
|
+
].join("\n");
|
|
31
|
+
const contentToHash = header + body;
|
|
32
|
+
const hash = stubHash(contentToHash);
|
|
33
|
+
const finalMarker = `// ${STUB_HASH_MARKER} ${hash}`;
|
|
34
|
+
return (header + body).replace(marker, finalMarker);
|
|
35
|
+
}
|
|
36
|
+
function isPristineStub(contents) {
|
|
37
|
+
const match = STUB_HASH_MARKER_REGEX.exec(contents);
|
|
38
|
+
if (!match) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
const contentToHash = contents.replace(
|
|
42
|
+
match[0],
|
|
43
|
+
`// ${STUB_HASH_MARKER} PLACEHOLDER`
|
|
44
|
+
);
|
|
45
|
+
return stubHash(contentToHash) === match[1];
|
|
46
|
+
}
|
|
47
|
+
function readExistingSdlFiles() {
|
|
48
|
+
const graphqlDir = getPaths().api.graphql;
|
|
49
|
+
if (!fs.existsSync(graphqlDir)) {
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
const contents = [];
|
|
53
|
+
const dirsToWalk = [graphqlDir];
|
|
54
|
+
while (dirsToWalk.length > 0) {
|
|
55
|
+
const dir = dirsToWalk.shift();
|
|
56
|
+
if (!dir) {
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
60
|
+
const entryPath = path.join(dir, entry.name);
|
|
61
|
+
if (entry.isDirectory()) {
|
|
62
|
+
dirsToWalk.push(entryPath);
|
|
63
|
+
} else if (/\.sdl\.(js|ts)$/.test(entry.name)) {
|
|
64
|
+
contents.push(fs.readFileSync(entryPath, "utf-8"));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return contents;
|
|
69
|
+
}
|
|
70
|
+
async function missingRelatedModels(modelName) {
|
|
71
|
+
const existingSdls = readExistingSdlFiles();
|
|
72
|
+
const isDefined = (typeName) => existingSdls.some(
|
|
73
|
+
(sdl) => new RegExp(`\\btype\\s+${typeName}\\b`).test(sdl)
|
|
74
|
+
);
|
|
75
|
+
const seen = /* @__PURE__ */ new Set([modelName]);
|
|
76
|
+
const missing = [];
|
|
77
|
+
const queue = [modelName];
|
|
78
|
+
while (queue.length > 0) {
|
|
79
|
+
const current = queue.shift();
|
|
80
|
+
if (!current) {
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
const model = await getSchema(current);
|
|
84
|
+
if (!model || !("fields" in model)) {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
for (const field of model.fields) {
|
|
88
|
+
if (!field.relationName || seen.has(field.type)) {
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
seen.add(field.type);
|
|
92
|
+
queue.push(field.type);
|
|
93
|
+
if (!isDefined(field.type)) {
|
|
94
|
+
missing.push(field.type);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return missing;
|
|
99
|
+
}
|
|
100
|
+
function writeFilesWithStubsTask(files, { overwriteExisting = false } = {}) {
|
|
101
|
+
const { base } = getPaths();
|
|
102
|
+
return new Listr(
|
|
103
|
+
Object.entries(files).map(([file, contents]) => ({
|
|
104
|
+
title: `...waiting to write file \`./${path.relative(base, file)}\`...`,
|
|
105
|
+
task: (_ctx, task) => {
|
|
106
|
+
let canOverwrite = overwriteExisting;
|
|
107
|
+
if (!canOverwrite && fs.existsSync(file)) {
|
|
108
|
+
const existingContents = fs.readFileSync(file, "utf-8");
|
|
109
|
+
if (isPristineStub(existingContents)) {
|
|
110
|
+
canOverwrite = true;
|
|
111
|
+
} else if (STUB_HASH_MARKER_REGEX.test(existingContents)) {
|
|
112
|
+
throw new Error(
|
|
113
|
+
`${file} started out as a generated stub, but has since been edited. Use \`--force\` to overwrite it.`
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return writeFile(
|
|
118
|
+
file,
|
|
119
|
+
contents,
|
|
120
|
+
{ overwriteExisting: canOverwrite },
|
|
121
|
+
task
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
}))
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
export {
|
|
128
|
+
addStubHeader,
|
|
129
|
+
isPristineStub,
|
|
130
|
+
missingRelatedModels,
|
|
131
|
+
writeFilesWithStubsTask
|
|
132
|
+
};
|
|
@@ -30,6 +30,7 @@ export const schema = gql`
|
|
|
30
30
|
<% } %>${singularCamelName}(${idName}: ${idType}!): ${singularPascalName} @requireAuth<% } %>
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
<% if (crud) { %>
|
|
33
34
|
<% if (docs) { %>
|
|
34
35
|
"""Autogenerated input type of Input${singularPascalName}."""
|
|
35
36
|
<% } %>
|
|
@@ -51,7 +52,7 @@ export const schema = gql`
|
|
|
51
52
|
<% } %>
|
|
52
53
|
input Update${singularPascalName}Input {
|
|
53
54
|
${updateInput}
|
|
54
|
-
}
|
|
55
|
+
}
|
|
55
56
|
|
|
56
57
|
<% if (docs) { %>
|
|
57
58
|
"""About mutations"""
|
|
@@ -30,6 +30,7 @@ export const schema = gql`
|
|
|
30
30
|
<% } %>${singularCamelName}(${idName}: ${idType}!): ${singularPascalName} @requireAuth<% } %>
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
<% if (crud) { %>
|
|
33
34
|
<% if (docs) { %>
|
|
34
35
|
"""Autogenerated input type of Input${singularPascalName}."""
|
|
35
36
|
<% } %>
|
|
@@ -51,7 +52,7 @@ export const schema = gql`
|
|
|
51
52
|
<% } %>
|
|
52
53
|
input Update${singularPascalName}Input {
|
|
53
54
|
${updateInput}
|
|
54
|
-
}
|
|
55
|
+
}
|
|
55
56
|
|
|
56
57
|
<% if (docs) { %>
|
|
57
58
|
"""About mutations"""
|
|
@@ -3,7 +3,7 @@ import camelcase from "camelcase";
|
|
|
3
3
|
import { pluralize, singularize } from "@cedarjs/utils/cedarPluralize";
|
|
4
4
|
import { transformTSToJS } from "../../../lib/index.js";
|
|
5
5
|
import { getSchema, verifyModelName } from "../../../lib/schemaHelpers.js";
|
|
6
|
-
import { relationsForModel } from "../helpers.js";
|
|
6
|
+
import { redactedModelFields, relationsForModel } from "../helpers.js";
|
|
7
7
|
import { createHandler, templateForFile } from "../yargsHandlerHelpers.js";
|
|
8
8
|
function isServiceModel(schema) {
|
|
9
9
|
return typeof schema === "object" && schema !== null && "fields" in schema && "name" in schema;
|
|
@@ -32,7 +32,10 @@ const parseSchema = async (model) => {
|
|
|
32
32
|
return field.isRequired && !field.hasDefaultValue && // don't include fields that the database will default
|
|
33
33
|
!field.relationName;
|
|
34
34
|
});
|
|
35
|
-
|
|
35
|
+
const redactedFields = redactedModelFields(
|
|
36
|
+
schema.fields.map((field) => field.name)
|
|
37
|
+
);
|
|
38
|
+
return { scalarFields, relations, foreignKeys, redactedFields };
|
|
36
39
|
};
|
|
37
40
|
function scenarioFieldValue(field) {
|
|
38
41
|
const randFloat = Math.random() * 1e7;
|
|
@@ -145,9 +148,14 @@ const fieldTypes = async (model) => {
|
|
|
145
148
|
);
|
|
146
149
|
};
|
|
147
150
|
const fieldsToInput = async (model) => {
|
|
148
|
-
const { scalarFields, foreignKeys } = await parseSchema(model);
|
|
151
|
+
const { scalarFields, foreignKeys, redactedFields } = await parseSchema(model);
|
|
149
152
|
const modelName = camelcase(singularize(model));
|
|
150
153
|
const inputObj = {};
|
|
154
|
+
if (scalarFields.some(
|
|
155
|
+
(field) => redactedFields.includes(field.name)
|
|
156
|
+
)) {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
151
159
|
scalarFields.forEach((field) => {
|
|
152
160
|
if (foreignKeys.includes(field.name)) {
|
|
153
161
|
inputObj[field.name] = `scenario.${modelName}.two.${field.name}`;
|
|
@@ -162,14 +170,17 @@ const fieldsToInput = async (model) => {
|
|
|
162
170
|
}
|
|
163
171
|
};
|
|
164
172
|
const fieldsToUpdate = async (model) => {
|
|
165
|
-
const { scalarFields, relations, foreignKeys } = await parseSchema(model);
|
|
173
|
+
const { scalarFields, relations, foreignKeys, redactedFields } = await parseSchema(model);
|
|
166
174
|
const modelName = camelcase(singularize(model));
|
|
167
175
|
let field, newValue, fieldName;
|
|
168
|
-
|
|
176
|
+
const updatableFields = scalarFields.filter(
|
|
177
|
+
(scalar) => !redactedFields.includes(scalar.name)
|
|
178
|
+
);
|
|
179
|
+
field = updatableFields.find(
|
|
169
180
|
(scalar) => !foreignKeys.includes(scalar.name)
|
|
170
181
|
);
|
|
171
182
|
if (!field) {
|
|
172
|
-
field =
|
|
183
|
+
field = updatableFields[0];
|
|
173
184
|
}
|
|
174
185
|
if (!field) {
|
|
175
186
|
return false;
|
|
@@ -3,7 +3,8 @@ import boxen from "boxen";
|
|
|
3
3
|
import { recordTelemetryAttributes, colors as c } from "@cedarjs/cli-helpers";
|
|
4
4
|
import {
|
|
5
5
|
formatCedarCommand,
|
|
6
|
-
formatRunBinCommand
|
|
6
|
+
formatRunBinCommand,
|
|
7
|
+
formatRunTransitiveBinCommand
|
|
7
8
|
} from "@cedarjs/cli-helpers/packageManager/display";
|
|
8
9
|
import { runTransitiveBinSync } from "@cedarjs/cli-helpers/packageManager/exec";
|
|
9
10
|
import { errorTelemetry } from "@cedarjs/telemetry";
|
|
@@ -54,10 +55,9 @@ const handler = async ({
|
|
|
54
55
|
args.push(String(value));
|
|
55
56
|
}
|
|
56
57
|
}
|
|
57
|
-
const displayCommand = args.map((arg) => arg.includes(" ") ? `"${arg}"` : arg).join(" ");
|
|
58
58
|
console.log();
|
|
59
59
|
console.log(c.note("Running Prisma CLI..."));
|
|
60
|
-
console.log(c.underline(`$
|
|
60
|
+
console.log(c.underline(`$ ${formatRunTransitiveBinCommand("prisma", args)}`));
|
|
61
61
|
console.log();
|
|
62
62
|
try {
|
|
63
63
|
runTransitiveBinSync("prisma", args, {
|