@cedarjs/cli 7.1.0 → 7.1.1-next.469
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/build/buildHandler.js +2 -1
- package/dist/commands/build.js +1 -1
- package/dist/commands/check.js +2 -1
- package/dist/commands/deploy/baremetal/baremetalHandler.js +364 -73
- package/dist/commands/deploy/baremetal/monitors.js +66 -0
- package/dist/commands/deploy/baremetal.js +7 -1
- package/dist/commands/deploy/flightcontrolHandler.js +1 -1
- package/dist/commands/deploy/helpers/deployHandler.js +1 -1
- package/dist/commands/deploy/netlify.js +1 -1
- package/dist/commands/deploy/renderHandler.js +1 -1
- package/dist/commands/deploy/serverlessHandler.js +2 -1
- package/dist/commands/deploy/vercel.js +1 -1
- package/dist/commands/destroy/handlerHelpers.js +1 -1
- package/dist/commands/destroy/page/pageHandler.js +2 -1
- package/dist/commands/destroy/scaffold/scaffoldHandler.js +65 -26
- package/dist/commands/destroy/sdl/sdlHandler.js +2 -1
- package/dist/commands/dev/devHandler.js +3 -4
- package/dist/commands/dev/packageWatchCommands.js +8 -3
- package/dist/commands/dev.js +1 -1
- package/dist/commands/execHandler.js +2 -1
- package/dist/commands/experimental/live-queries/liveQueries.js +1 -1
- package/dist/commands/experimental/live-queries/liveQueriesHandler.js +3 -3
- package/dist/commands/experimental/setupInngest.js +1 -1
- package/dist/commands/experimental/setupInngestHandler.js +1 -1
- package/dist/commands/experimental/setupOpentelemetry.js +1 -1
- package/dist/commands/experimental/setupOpentelemetryHandler.js +2 -1
- package/dist/commands/experimental/setupReactCompiler.js +1 -1
- package/dist/commands/experimental/setupReactCompilerHandler.js +4 -4
- package/dist/commands/experimental/setupRsc.js +1 -1
- package/dist/commands/experimental/setupRscHandler.js +2 -1
- package/dist/commands/experimental/setupStreamingSsr.js +1 -1
- package/dist/commands/experimental/setupStreamingSsrHandler.js +3 -2
- package/dist/commands/generate/cell/cell.js +20 -0
- package/dist/commands/generate/cell/cellHandler.js +68 -20
- package/dist/commands/generate/cell/templates/cell.tsx.template +21 -2
- package/dist/commands/generate/cell/templates/cellFragment.tsx.template +43 -0
- package/dist/commands/generate/cell/templates/cellList.tsx.template +21 -2
- package/dist/commands/generate/cell/templates/storiesFragment.tsx.template +23 -0
- package/dist/commands/generate/cell/templates/test.js.template +1 -1
- package/dist/commands/generate/cell/templates/testFragment.js.template +30 -0
- package/dist/commands/generate/dataMigration/dataMigration.js +2 -1
- package/dist/commands/generate/dbAuth/dbAuth.js +6 -2
- package/dist/commands/generate/dbAuth/dbAuthHandler.js +80 -17
- package/dist/commands/generate/dbAuth/templates/changePassword.tsx.template +131 -0
- package/dist/commands/generate/dbAuth/templates/login.oauth.tsx.template +164 -0
- package/dist/commands/generate/dbAuth/templates/login.webAuthn.tsx.template +20 -12
- package/dist/commands/generate/dbAuth/templates/signup.oauth.tsx.template +147 -0
- package/dist/commands/generate/directive/directiveHandler.js +2 -1
- package/dist/commands/generate/function/functionHandler.js +2 -1
- package/dist/commands/generate/helpers.js +26 -0
- package/dist/commands/generate/job/jobHandler.js +2 -1
- package/dist/commands/generate/model/modelHandler.js +2 -1
- package/dist/commands/generate/ogImage/ogImageHandler.js +2 -1
- package/dist/commands/generate/ogImage/templates/ogImage.og.tsx.template +1 -1
- package/dist/commands/generate/package/filesTask.js +9 -12
- package/dist/commands/generate/package/packageHandler.js +3 -4
- package/dist/commands/generate/package/templates/tsconfig.json.template +1 -0
- package/dist/commands/generate/page/pageHandler.js +2 -1
- package/dist/commands/generate/realtime/realtime.js +1 -1
- package/dist/commands/generate/realtime/realtimeHandler.js +2 -9
- package/dist/commands/generate/realtime/templates/liveQueries/blank/blank.service.ts.template +12 -4
- package/dist/commands/generate/realtime/templates/subscriptions/blank/blank.service.ts.template +2 -2
- package/dist/commands/generate/realtime/templates/subscriptions/blank/blank.ts.template +4 -4
- package/dist/commands/generate/scaffold/scaffold.js +1 -1
- package/dist/commands/generate/scaffold/scaffoldHandler.js +93 -49
- package/dist/commands/generate/scaffold/templates/assets/scaffold.tailwind.css.template +12 -7
- package/dist/commands/generate/scaffold/templates/components/EditNameCell.tsx.template +5 -7
- package/dist/commands/generate/scaffold/templates/components/NameForm.tsx.template +10 -9
- package/dist/commands/generate/scaffold/templates/layouts/NamesScaffoldLayout.tsx.template +18 -0
- package/dist/commands/generate/scaffold/templates/lib/formatters.test.tsx.template +11 -0
- package/dist/commands/generate/scaffold/templates/lib/formatters.tsx.template +16 -4
- package/dist/commands/generate/script/scriptHandler.js +2 -1
- package/dist/commands/generate/script/templates/tsconfig.json.template +1 -0
- package/dist/commands/generate/sdl/sdlHandler.js +10 -4
- package/dist/commands/generate/secret/secret.js +1 -1
- package/dist/commands/generate/service/serviceHandler.js +79 -49
- package/dist/commands/generate/service/templates/service.ts.template +40 -13
- package/dist/commands/generate/service/templates/test.ts.template +8 -6
- package/dist/commands/generate/yargsCommandHelpers.js +1 -1
- package/dist/commands/generate/yargsHandlerHelpers.js +2 -1
- package/dist/commands/generate.js +1 -1
- package/dist/commands/info.js +1 -1
- package/dist/commands/jobsHandler.js +5 -3
- package/dist/commands/lint.js +1 -1
- package/dist/commands/prerenderHandler.js +11 -4
- package/dist/commands/prismaHandler.js +7 -2
- package/dist/commands/record/init.js +1 -1
- package/dist/commands/serve.js +2 -1
- package/dist/commands/setup/auth/auth.js +2 -4
- package/dist/commands/setup/cache/cache.js +1 -1
- package/dist/commands/setup/cache/cacheHandler.js +2 -1
- package/dist/commands/setup/cache/templates/memcached.ts.template +14 -17
- package/dist/commands/setup/cache/templates/redis.ts.template +12 -20
- package/dist/commands/setup/database/postgres.js +1 -1
- package/dist/commands/setup/database/postgresHandler.js +3 -1
- package/dist/commands/setup/deploy/providers/baremetal.js +7 -0
- package/dist/commands/setup/deploy/providers/baremetalHandler.js +130 -21
- package/dist/commands/setup/deploy/providers/coherence.js +1 -1
- package/dist/commands/setup/deploy/providers/coherenceHandler.js +7 -6
- package/dist/commands/setup/deploy/providers/flightcontrolHandler.js +2 -1
- package/dist/commands/setup/deploy/providers/netlifyHandler.js +2 -1
- package/dist/commands/setup/deploy/providers/renderHandler.js +2 -1
- package/dist/commands/setup/deploy/providers/serverlessHandler.js +2 -1
- package/dist/commands/setup/deploy/providers/universalDeployHandler.js +4 -6
- package/dist/commands/setup/deploy/providers/vercelHandler.js +2 -1
- package/dist/commands/setup/deploy/templates/baremetal.js +119 -9
- package/dist/commands/setup/deploy/templates/netlifyUD.js +3 -0
- package/dist/commands/setup/docker/docker.js +1 -1
- package/dist/commands/setup/docker/dockerHandler.js +2 -1
- package/dist/commands/setup/generator/generator.js +1 -1
- package/dist/commands/setup/generator/generatorHandler.js +1 -1
- package/dist/commands/setup/graphql/features/fragments/fragments.js +1 -1
- package/dist/commands/setup/graphql/features/fragments/fragmentsHandler.js +3 -5
- package/dist/commands/setup/graphql/features/trustedDocuments/trustedDocuments.js +1 -1
- package/dist/commands/setup/graphql/features/trustedDocuments/trustedDocumentsHandler.js +2 -1
- package/dist/commands/setup/i18n/i18n.js +1 -1
- package/dist/commands/setup/i18n/i18nHandler.js +1 -1
- package/dist/commands/setup/jobs/jobs.js +1 -1
- package/dist/commands/setup/jobs/jobsHandler.js +4 -1
- package/dist/commands/setup/mailer/mailer.js +1 -1
- package/dist/commands/setup/mailer/mailerHandler.js +2 -1
- package/dist/commands/setup/middleware/ogImage/ogImage.js +1 -1
- package/dist/commands/setup/middleware/ogImage/ogImageHandler.js +2 -1
- package/dist/commands/setup/monitoring/sentry/sentry.js +1 -1
- package/dist/commands/setup/monitoring/sentry/sentryHandler.js +13 -9
- package/dist/commands/setup/monitoring/sentry/templates/sentryApi.ts.template +1 -4
- package/dist/commands/setup/monitoring/sentry/templates/sentryWeb.ts.template +7 -4
- package/dist/commands/setup/neon/neon.js +1 -1
- package/dist/commands/setup/neon/neonHandler.js +5 -9
- package/dist/commands/setup/package/package.js +1 -1
- package/dist/commands/setup/package/packageHandler.js +4 -4
- package/dist/commands/setup/realtime/addRealtimeToGraphql.js +1 -1
- package/dist/commands/setup/realtime/realtime.js +1 -1
- package/dist/commands/setup/realtime/realtimeHandler.js +4 -16
- package/dist/commands/setup/realtime/templates/defer/fastAndSlowFields/fastAndSlowFields.ts.template +4 -1
- package/dist/commands/setup/realtime/templates/liveQueries/auctions/auctions.ts.template +12 -4
- package/dist/commands/setup/realtime/templates/subscriptions/countdown/countdown.ts.template +1 -1
- package/dist/commands/setup/realtime/templates/subscriptions/newMessage/newMessage.ts.template +2 -2
- package/dist/commands/setup/server-file/serverFile.js +1 -1
- package/dist/commands/setup/server-file/serverFileHandler.js +2 -1
- package/dist/commands/setup/tenancy/authCodemod.js +66 -0
- package/dist/commands/setup/tenancy/dbCodemod.js +64 -0
- package/dist/commands/setup/tenancy/graphqlCodemod.js +88 -0
- package/dist/commands/setup/tenancy/preflight.js +49 -0
- package/dist/commands/setup/tenancy/schemaPrisma.js +81 -0
- package/dist/commands/setup/tenancy/signupCodemod.js +154 -0
- package/dist/commands/setup/tenancy/templates/InvitePage.tsx.template +72 -0
- package/dist/commands/setup/tenancy/templates/OrgScope.tsx.template +40 -0
- package/dist/commands/setup/tenancy/templates/ensureDefaultOrganizations.dataMigration.ts.template +39 -0
- package/dist/commands/setup/tenancy/templates/organizations.scenarios.ts.template +57 -0
- package/dist/commands/setup/tenancy/templates/organizations.sdl.ts.template +89 -0
- package/dist/commands/setup/tenancy/templates/organizations.test.ts.template +381 -0
- package/dist/commands/setup/tenancy/templates/organizations.ts.template +394 -0
- package/dist/commands/setup/tenancy/templates/requireMembership.test.ts.template +35 -0
- package/dist/commands/setup/tenancy/templates/requireMembership.ts.template +25 -0
- package/dist/commands/setup/tenancy/tenancy.js +30 -0
- package/dist/commands/setup/tenancy/tenancyHandler.js +425 -0
- package/dist/commands/setup/ui/libraries/chakra-uiHandler.js +2 -1
- package/dist/commands/setup/ui/libraries/mantineHandler.js +2 -1
- package/dist/commands/setup/ui/libraries/tailwindcssHandler.js +63 -82
- package/dist/commands/setup/uploads/gitignore.js +17 -0
- package/dist/commands/setup/uploads/schemaPrisma.js +41 -0
- package/dist/commands/setup/uploads/serverFile.js +217 -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 +98 -0
- package/dist/commands/setup/uploads/templates/withDataUri.ts.template +14 -0
- package/dist/commands/setup/uploads/templates/withSignedUrl.ts.template +16 -0
- package/dist/commands/setup/uploads/uploads.js +49 -7
- package/dist/commands/setup/uploads/uploadsHandler.js +277 -69
- package/dist/commands/setup.js +2 -1
- package/dist/commands/studio.js +1 -1
- package/dist/commands/test/testHandler.js +25 -71
- package/dist/commands/test/viteVersionCheck.js +75 -0
- package/dist/commands/test.js +10 -18
- package/dist/commands/type-checkHandler.js +1 -1
- package/dist/commands/upgrade/preUpgradeScripts.js +3 -3
- package/dist/commands/upgrade/upgrade.js +1 -1
- package/dist/commands/upgrade/upgradeHandler.js +2 -1
- package/dist/index.js +5 -8
- package/dist/lib/exit.js +1 -1
- package/dist/lib/index.js +16 -31
- package/dist/lib/loadEnvFiles.js +1 -1
- package/dist/lib/plugin.js +1 -1
- package/dist/lib/test.js +1 -1
- package/dist/lib/updateCheck.js +4 -4
- package/dist/middleware/checkNodeVersion.js +4 -4
- package/package.json +27 -27
- package/dist/commands/setup/ui/templates/postcss.config.cjs.template +0 -9
- package/dist/commands/setup/uploads/dbCodemod.js +0 -28
- package/dist/commands/setup/uploads/templates/signedUrl.ts.template +0 -21
- package/dist/commands/test/testHandlerEsm.js +0 -109
- package/dist/commands/testEsm.js +0 -41
|
@@ -5,14 +5,15 @@ import { paramCase } from "change-case";
|
|
|
5
5
|
import humanize from "humanize-string";
|
|
6
6
|
import { Listr } from "listr2";
|
|
7
7
|
import pascalcase from "pascalcase";
|
|
8
|
-
import {
|
|
8
|
+
import { colors as c } from "@cedarjs/cli-helpers/colors";
|
|
9
9
|
import { formatCedarCommand } from "@cedarjs/cli-helpers/packageManager/display";
|
|
10
10
|
import {
|
|
11
11
|
addWorkspacePackages,
|
|
12
12
|
removeWorkspacePackages
|
|
13
13
|
} from "@cedarjs/cli-helpers/packageManager/packages";
|
|
14
|
+
import { recordTelemetryAttributes } from "@cedarjs/cli-helpers/telemetry";
|
|
14
15
|
import { generate as generateTypes } from "@cedarjs/internal/dist/generate/generate";
|
|
15
|
-
import { getConfig } from "@cedarjs/project-config";
|
|
16
|
+
import { getConfig, getParsedScalars } from "@cedarjs/project-config";
|
|
16
17
|
import { pluralize, singularize } from "@cedarjs/utils/cedarPluralize";
|
|
17
18
|
import {
|
|
18
19
|
generateTemplate,
|
|
@@ -85,6 +86,18 @@ const getImportComponentNames = (name, scaffoldPath, nestScaffoldByModel = true)
|
|
|
85
86
|
importLayoutNames: `src/layouts/ScaffoldLayout`
|
|
86
87
|
};
|
|
87
88
|
};
|
|
89
|
+
const getScaffoldLayoutNames = (name, scaffoldPath = "") => {
|
|
90
|
+
const pluralPascalName = pascalcase(pluralize(name));
|
|
91
|
+
const directoryName = `${pluralPascalName}ScaffoldLayout`;
|
|
92
|
+
const pathParts = scaffoldPath === "" ? [] : scaffoldPath.split("/").map(pascalcase);
|
|
93
|
+
return {
|
|
94
|
+
directoryName,
|
|
95
|
+
pathParts,
|
|
96
|
+
// The name to import the layout as in Routes.{jsx,tsx}
|
|
97
|
+
importName: pascalcase(scaffoldPath) + directoryName,
|
|
98
|
+
importPath: ["src/layouts", ...pathParts, directoryName].join("/")
|
|
99
|
+
};
|
|
100
|
+
};
|
|
88
101
|
const getTemplateStrings = (name, scaffoldPath, nestScaffoldByModel = true) => {
|
|
89
102
|
const nameVars = nameVariants(name);
|
|
90
103
|
const camelScaffoldPath = camelcase(pascalcase(scaffoldPath));
|
|
@@ -96,11 +109,21 @@ const getTemplateStrings = (name, scaffoldPath, nestScaffoldByModel = true) => {
|
|
|
96
109
|
...getImportComponentNames(name, scaffoldPath, nestScaffoldByModel)
|
|
97
110
|
};
|
|
98
111
|
};
|
|
112
|
+
const webDependsOnTailwindCSS = () => {
|
|
113
|
+
const webPackageJsonPath = path.join(getPaths().web.base, "package.json");
|
|
114
|
+
if (!fs.existsSync(webPackageJsonPath)) {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
const { dependencies, devDependencies } = JSON.parse(
|
|
118
|
+
fs.readFileSync(webPackageJsonPath, "utf-8")
|
|
119
|
+
);
|
|
120
|
+
return Boolean(
|
|
121
|
+
dependencies?.["tailwindcss"] || devDependencies?.["tailwindcss"]
|
|
122
|
+
);
|
|
123
|
+
};
|
|
99
124
|
const shouldUseTailwindCSS = (flag) => {
|
|
100
125
|
if (flag === void 0) {
|
|
101
|
-
return
|
|
102
|
-
path.join(getPaths().web.config, "tailwind.config.cjs")
|
|
103
|
-
);
|
|
126
|
+
return webDependsOnTailwindCSS();
|
|
104
127
|
} else {
|
|
105
128
|
return flag;
|
|
106
129
|
}
|
|
@@ -149,7 +172,13 @@ const files = async ({
|
|
|
149
172
|
}),
|
|
150
173
|
...await assetFiles(name, tailwind),
|
|
151
174
|
...await formatters(name, typescript),
|
|
152
|
-
...await layoutFiles(
|
|
175
|
+
...await layoutFiles(
|
|
176
|
+
name,
|
|
177
|
+
scaffoldPath,
|
|
178
|
+
force,
|
|
179
|
+
typescript,
|
|
180
|
+
templateStrings
|
|
181
|
+
),
|
|
153
182
|
...await pageFiles(
|
|
154
183
|
name,
|
|
155
184
|
pascalScaffoldPath,
|
|
@@ -189,6 +218,7 @@ const assetFiles = async (name, tailwind) => {
|
|
|
189
218
|
}
|
|
190
219
|
return fileList;
|
|
191
220
|
};
|
|
221
|
+
const isDateTimeParsed = () => getParsedScalars().DateTime === "Date";
|
|
192
222
|
const formatters = async (name, isTypescript) => {
|
|
193
223
|
const outputPath = path.join(
|
|
194
224
|
getPaths().web.src,
|
|
@@ -210,7 +240,8 @@ const formatters = async (name, isTypescript) => {
|
|
|
210
240
|
templatePath: path.join("lib", "formatters.tsx.template")
|
|
211
241
|
}),
|
|
212
242
|
{
|
|
213
|
-
name
|
|
243
|
+
name,
|
|
244
|
+
parsedDateTime: isDateTimeParsed()
|
|
214
245
|
}
|
|
215
246
|
);
|
|
216
247
|
const templateTest = await generateTemplate(
|
|
@@ -220,7 +251,8 @@ const formatters = async (name, isTypescript) => {
|
|
|
220
251
|
templatePath: path.join("lib", "formatters.test.tsx.template")
|
|
221
252
|
}),
|
|
222
253
|
{
|
|
223
|
-
name
|
|
254
|
+
name,
|
|
255
|
+
parsedDateTime: isDateTimeParsed()
|
|
224
256
|
}
|
|
225
257
|
);
|
|
226
258
|
return {
|
|
@@ -340,44 +372,61 @@ const modelRelatedVariables = (model) => {
|
|
|
340
372
|
fieldsToImport,
|
|
341
373
|
editableColumns,
|
|
342
374
|
listFormattersImports,
|
|
343
|
-
formattersImports
|
|
375
|
+
formattersImports,
|
|
376
|
+
parsedDateTime: isDateTimeParsed()
|
|
344
377
|
};
|
|
345
378
|
};
|
|
346
|
-
const layoutFiles = async (name, force, generateTypescript, templateStrings) => {
|
|
379
|
+
const layoutFiles = async (name, scaffoldPath, force, generateTypescript, templateStrings) => {
|
|
347
380
|
const fileList = {};
|
|
348
|
-
const
|
|
381
|
+
const extension = generateTypescript ? ".tsx" : ".jsx";
|
|
382
|
+
const renderLayout = async (templateName, outputPath) => {
|
|
383
|
+
const template = await generateTemplate(
|
|
384
|
+
customOrDefaultTemplatePath({
|
|
385
|
+
side: "web",
|
|
386
|
+
generator: "scaffold",
|
|
387
|
+
templatePath: path.join("layouts", templateName)
|
|
388
|
+
}),
|
|
389
|
+
{
|
|
390
|
+
name,
|
|
391
|
+
pascalScaffoldPath: "",
|
|
392
|
+
...templateStrings
|
|
393
|
+
}
|
|
394
|
+
);
|
|
395
|
+
fileList[outputPath] = generateTypescript || !outputPath.endsWith(".jsx") ? template : await transformTSToJS(outputPath, template);
|
|
396
|
+
};
|
|
397
|
+
const sharedLayoutDir = path.join(getPaths().web.layouts, "ScaffoldLayout");
|
|
398
|
+
const sharedLayoutTemplates = fs.readdirSync(
|
|
349
399
|
customOrDefaultTemplatePath({
|
|
350
400
|
side: "web",
|
|
351
401
|
generator: "scaffold",
|
|
352
402
|
templatePath: "layouts"
|
|
353
403
|
})
|
|
354
|
-
);
|
|
355
|
-
for (const
|
|
356
|
-
const
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
if (!fs.existsSync(outputPath) || force) {
|
|
366
|
-
const template = await generateTemplate(
|
|
367
|
-
customOrDefaultTemplatePath({
|
|
368
|
-
side: "web",
|
|
369
|
-
generator: "scaffold",
|
|
370
|
-
templatePath: path.join("layouts", layout)
|
|
371
|
-
}),
|
|
372
|
-
{
|
|
373
|
-
name,
|
|
374
|
-
pascalScaffoldPath: "",
|
|
375
|
-
...templateStrings
|
|
376
|
-
}
|
|
377
|
-
);
|
|
378
|
-
fileList[outputPath] = generateTypescript ? template : await transformTSToJS(outputPath, template);
|
|
404
|
+
).filter((template) => template !== "NamesScaffoldLayout.tsx.template");
|
|
405
|
+
for (const template of sharedLayoutTemplates) {
|
|
406
|
+
const isComponent = template.endsWith(".tsx.template");
|
|
407
|
+
const outputName = isComponent ? template.replace(/\.tsx\.template$/, extension) : template.replace(/\.template$/, "");
|
|
408
|
+
const exists = isComponent ? [".tsx", ".jsx", ".ts", ".js"].some(
|
|
409
|
+
(ext) => fs.existsSync(
|
|
410
|
+
path.join(sharedLayoutDir, outputName.replace(extension, ext))
|
|
411
|
+
)
|
|
412
|
+
) : fs.existsSync(path.join(sharedLayoutDir, outputName));
|
|
413
|
+
if (!exists || force) {
|
|
414
|
+
await renderLayout(template, path.join(sharedLayoutDir, outputName));
|
|
379
415
|
}
|
|
380
416
|
}
|
|
417
|
+
const { directoryName, pathParts } = getScaffoldLayoutNames(
|
|
418
|
+
name,
|
|
419
|
+
scaffoldPath
|
|
420
|
+
);
|
|
421
|
+
await renderLayout(
|
|
422
|
+
"NamesScaffoldLayout.tsx.template",
|
|
423
|
+
path.join(
|
|
424
|
+
getPaths().web.layouts,
|
|
425
|
+
...pathParts,
|
|
426
|
+
directoryName,
|
|
427
|
+
directoryName + extension
|
|
428
|
+
)
|
|
429
|
+
);
|
|
381
430
|
return fileList;
|
|
382
431
|
};
|
|
383
432
|
const pageFiles = async (name, pascalScaffoldPath = "", generateTypescript, nestScaffoldByModel = true, templateStrings) => {
|
|
@@ -419,7 +468,7 @@ const pageFiles = async (name, pascalScaffoldPath = "", generateTypescript, nest
|
|
|
419
468
|
...modelRelatedVariables(model)
|
|
420
469
|
}
|
|
421
470
|
);
|
|
422
|
-
fileList[outputPath] = generateTypescript ? template : await transformTSToJS(outputPath, template);
|
|
471
|
+
fileList[outputPath] = generateTypescript || !outputPath.endsWith(".jsx") ? template : await transformTSToJS(outputPath, template);
|
|
423
472
|
}
|
|
424
473
|
return fileList;
|
|
425
474
|
};
|
|
@@ -467,7 +516,7 @@ const componentFiles = async (name, pascalScaffoldPath = "", generateTypescript,
|
|
|
467
516
|
...modelRelatedVariables(model)
|
|
468
517
|
}
|
|
469
518
|
);
|
|
470
|
-
fileList[outputPath] = generateTypescript ? template : await transformTSToJS(outputPath, template);
|
|
519
|
+
fileList[outputPath] = generateTypescript || !outputPath.endsWith(".jsx") ? template : await transformTSToJS(outputPath, template);
|
|
471
520
|
}
|
|
472
521
|
return fileList;
|
|
473
522
|
};
|
|
@@ -498,8 +547,9 @@ const routes = async ({
|
|
|
498
547
|
`<Route path="/${paramScaffoldPath}${nameVars.pluralParamName}" page={${pageRoot}${nameVars.pluralPascalName}Page} name="${templateNames.pluralRouteName}" />`
|
|
499
548
|
];
|
|
500
549
|
};
|
|
501
|
-
const addLayoutImport = () => {
|
|
502
|
-
const
|
|
550
|
+
const addLayoutImport = (model, scaffoldPath) => {
|
|
551
|
+
const { importName, importPath } = getScaffoldLayoutNames(model, scaffoldPath);
|
|
552
|
+
const importLayout = `import ${importName} from '${importPath}'`;
|
|
503
553
|
const routesPath = getPaths().web.routes;
|
|
504
554
|
const routesContent = readFile(routesPath).toString();
|
|
505
555
|
if (!routesContent.match(importLayout)) {
|
|
@@ -613,17 +663,10 @@ const addSetImport = (task) => {
|
|
|
613
663
|
return `Added ${namesToImport.join(", ")} import to Routes.{jsx,tsx}`;
|
|
614
664
|
};
|
|
615
665
|
const addScaffoldSetToRouter = async (model, scaffoldPath) => {
|
|
616
|
-
const templateNames = getTemplateStrings(model, scaffoldPath);
|
|
617
|
-
const nameVars = nameVariants(model);
|
|
618
|
-
const title = nameVars.pluralPascalName;
|
|
619
|
-
const titleTo = templateNames.pluralRouteName;
|
|
620
|
-
const buttonLabel = `New ${nameVars.singularPascalName}`;
|
|
621
|
-
const buttonTo = templateNames.newRouteName;
|
|
622
666
|
const unauthenticatedRoute = getUnauthenticatedRedirectRoute();
|
|
623
667
|
return addRoutesToRouterTask(
|
|
624
668
|
await routes({ model, path: scaffoldPath }),
|
|
625
|
-
|
|
626
|
-
{ title, titleTo, buttonLabel, buttonTo },
|
|
669
|
+
getScaffoldLayoutNames(model, scaffoldPath).importName,
|
|
627
670
|
unauthenticatedRoute ? { unauthenticated: unauthenticatedRoute } : void 0
|
|
628
671
|
);
|
|
629
672
|
};
|
|
@@ -664,7 +707,7 @@ const tasks = ({
|
|
|
664
707
|
},
|
|
665
708
|
{
|
|
666
709
|
title: "Adding layout import...",
|
|
667
|
-
task: async () => addLayoutImport()
|
|
710
|
+
task: async () => addLayoutImport(model, path2)
|
|
668
711
|
},
|
|
669
712
|
{
|
|
670
713
|
title: "Adding set import...",
|
|
@@ -780,6 +823,7 @@ const splitPathAndModel = (pathSlashModel) => {
|
|
|
780
823
|
};
|
|
781
824
|
export {
|
|
782
825
|
files,
|
|
826
|
+
getScaffoldLayoutNames,
|
|
783
827
|
getUnauthenticatedRedirectRoute,
|
|
784
828
|
handler,
|
|
785
829
|
hasLoginRoute,
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Makes your app's Tailwind theme and utilities available to `@apply`
|
|
3
|
+
* without emitting any of the styles in index.css a second time
|
|
4
|
+
*/
|
|
5
|
+
@reference './index.css';
|
|
6
|
+
|
|
1
7
|
.rw-scaffold {
|
|
2
8
|
@apply bg-white text-gray-600;
|
|
3
9
|
}
|
|
@@ -27,8 +33,7 @@
|
|
|
27
33
|
@apply mx-4 pb-4;
|
|
28
34
|
}
|
|
29
35
|
.rw-segment {
|
|
30
|
-
@apply rounded-lg overflow-hidden w-full border border-gray-200;
|
|
31
|
-
scrollbar-color: theme('colors.zinc.400') transparent;
|
|
36
|
+
@apply rounded-lg overflow-hidden w-full border border-gray-200 scrollbar-thumb-zinc-400 scrollbar-track-transparent;
|
|
32
37
|
}
|
|
33
38
|
.rw-segment::-webkit-scrollbar {
|
|
34
39
|
height: initial;
|
|
@@ -80,7 +85,7 @@
|
|
|
80
85
|
}
|
|
81
86
|
.rw-cell-error,
|
|
82
87
|
.rw-form-error-wrapper {
|
|
83
|
-
@apply p-4 bg-red-50 text-red-600 border border-red-100 rounded my-4;
|
|
88
|
+
@apply p-4 bg-red-50 text-red-600 border border-red-100 rounded-sm my-4;
|
|
84
89
|
}
|
|
85
90
|
.rw-form-error-title {
|
|
86
91
|
@apply m-0 font-semibold;
|
|
@@ -89,13 +94,13 @@
|
|
|
89
94
|
@apply mt-2 list-disc list-inside;
|
|
90
95
|
}
|
|
91
96
|
.rw-button {
|
|
92
|
-
@apply flex justify-center py-1 px-4 border-0 rounded bg-gray-200 text-gray-500 text-xs font-semibold uppercase tracking-wide leading-loose no-underline cursor-pointer transition duration-100;
|
|
97
|
+
@apply flex justify-center py-1 px-4 border-0 rounded-sm bg-gray-200 text-gray-500 text-xs font-semibold uppercase tracking-wide leading-loose no-underline cursor-pointer transition duration-100;
|
|
93
98
|
}
|
|
94
99
|
.rw-button:hover {
|
|
95
100
|
@apply bg-gray-500 text-white;
|
|
96
101
|
}
|
|
97
102
|
.rw-button.rw-button-small {
|
|
98
|
-
@apply text-xs rounded-
|
|
103
|
+
@apply text-xs rounded-xs py-1 px-2;
|
|
99
104
|
}
|
|
100
105
|
.rw-button.rw-button-green {
|
|
101
106
|
@apply bg-green-500 text-white;
|
|
@@ -134,7 +139,7 @@
|
|
|
134
139
|
@apply text-red-600;
|
|
135
140
|
}
|
|
136
141
|
.rw-input {
|
|
137
|
-
@apply block mt-2 w-full p-2 bg-white border border-gray-200 rounded outline-
|
|
142
|
+
@apply block mt-2 w-full p-2 bg-white border border-gray-200 rounded-sm outline-hidden;
|
|
138
143
|
}
|
|
139
144
|
.rw-check-radio-items {
|
|
140
145
|
@apply flex justify-items-center;
|
|
@@ -153,7 +158,7 @@
|
|
|
153
158
|
@apply border-red-600 text-red-600;
|
|
154
159
|
}
|
|
155
160
|
.rw-input-error:focus {
|
|
156
|
-
@apply border-red-600 outline-
|
|
161
|
+
@apply border-red-600 outline-hidden;
|
|
157
162
|
box-shadow: 0 0 5px #c53030;
|
|
158
163
|
}
|
|
159
164
|
.rw-field-error {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
${useClientDirective}import type {
|
|
2
2
|
Edit${singularPascalName}By${pascalIdName},
|
|
3
3
|
Update${singularPascalName}Input,
|
|
4
|
+
Update${singularPascalName}Mutation,
|
|
4
5
|
Update${singularPascalName}MutationVariables
|
|
5
6
|
} from 'types/graphql'
|
|
6
7
|
|
|
@@ -24,7 +25,7 @@ export const QUERY: TypedDocumentNode<Edit${singularPascalName}By${pascalIdName}
|
|
|
24
25
|
`
|
|
25
26
|
|
|
26
27
|
const UPDATE_${singularConstantName}_MUTATION: TypedDocumentNode<
|
|
27
|
-
|
|
28
|
+
Update${singularPascalName}Mutation,
|
|
28
29
|
Update${singularPascalName}MutationVariables
|
|
29
30
|
> = gql`
|
|
30
31
|
mutation Update${singularPascalName}Mutation($${idName}: ${idType}!, $input: Update${singularPascalName}Input!) {
|
|
@@ -56,17 +57,14 @@ export const Success = ({ ${singularCamelName} }: CellSuccessProps<Edit${singula
|
|
|
56
57
|
}
|
|
57
58
|
)
|
|
58
59
|
|
|
59
|
-
const onSave = (
|
|
60
|
-
|
|
61
|
-
id: Edit${singularPascalName}By${pascalIdName}['${singularCamelName}']['id']
|
|
62
|
-
) => {
|
|
63
|
-
update${singularPascalName}({ variables: { id, input } })
|
|
60
|
+
const onSave = (input: Update${singularPascalName}Input) => {
|
|
61
|
+
update${singularPascalName}({ variables: { ${idName}: ${singularCamelName}.${idName}, input } })
|
|
64
62
|
}
|
|
65
63
|
|
|
66
64
|
return (
|
|
67
65
|
<div className="rw-segment">
|
|
68
66
|
<header className="rw-segment-header">
|
|
69
|
-
<h2 className="rw-heading rw-heading-secondary">Edit ${singularPascalName} {${singularCamelName}
|
|
67
|
+
<h2 className="rw-heading rw-heading-secondary">Edit ${singularPascalName} {${singularCamelName}.${idName}}</h2>
|
|
70
68
|
</header>
|
|
71
69
|
<div className="rw-segment-main">
|
|
72
70
|
<${singularPascalName}Form ${singularCamelName}={${singularCamelName}} onSave={onSave} error={error} loading={loading} />
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Edit${singularPascalName}By${pascalIdName},
|
|
1
|
+
import type { Edit${singularPascalName}By${pascalIdName}, Create${singularPascalName}Input } from 'types/graphql'
|
|
2
2
|
|
|
3
3
|
import type { RWGqlError } from '@cedarjs/forms'
|
|
4
4
|
import {
|
|
@@ -12,17 +12,17 @@ import {
|
|
|
12
12
|
|
|
13
13
|
<% if (fieldsToImport.includes('DatetimeLocalField')) { %>
|
|
14
14
|
/**
|
|
15
|
-
* Formats a DateTime string from the API, which is a UTC timestamp, for use as
|
|
15
|
+
* Formats a DateTime <%= parsedDateTime ? 'Date object' : 'string' %> from the API, which is a UTC timestamp, for use as
|
|
16
16
|
* the value of a `datetime-local` input. The input takes `YYYY-MM-DDTHH:mm` in
|
|
17
17
|
* the user's local time, with no seconds, milliseconds or timezone, and the
|
|
18
18
|
* form reads its value back as local time when it is submitted.
|
|
19
19
|
*/
|
|
20
|
-
function formatDatetime(value: string | undefined | null) {
|
|
20
|
+
function formatDatetime(value: <%= parsedDateTime ? 'Date' : 'string' %> | undefined | null) {
|
|
21
21
|
if (!value) {
|
|
22
22
|
return undefined
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
const date = new Date(value)
|
|
25
|
+
const date = <%= parsedDateTime ? 'value' : 'new Date(value)' %>
|
|
26
26
|
|
|
27
27
|
if (Number.isNaN(date.getTime())) {
|
|
28
28
|
return undefined
|
|
@@ -48,8 +48,8 @@ type Form${singularPascalName} = NonNullable<Edit${singularPascalName}By${pascal
|
|
|
48
48
|
|
|
49
49
|
interface ${singularPascalName}FormProps {
|
|
50
50
|
${singularCamelName}?: Edit${singularPascalName}By${pascalIdName}['${singularCamelName}']
|
|
51
|
-
onSave: (data:
|
|
52
|
-
error
|
|
51
|
+
onSave: (data: Create${singularPascalName}Input) => void
|
|
52
|
+
error?: RWGqlError
|
|
53
53
|
loading: boolean
|
|
54
54
|
}
|
|
55
55
|
|
|
@@ -70,7 +70,8 @@ const ${singularPascalName}Form = (props: ${singularPascalName}FormProps) => {
|
|
|
70
70
|
}
|
|
71
71
|
<% } %>
|
|
72
72
|
<% if (column.isEnum && !column.isList && !column.isRequired) { %>
|
|
73
|
-
|
|
73
|
+
// The "None" option has an empty value
|
|
74
|
+
if (!data.${column.name}) {
|
|
74
75
|
data.${column.name} = null
|
|
75
76
|
}
|
|
76
77
|
<% } %>
|
|
@@ -80,7 +81,7 @@ const ${singularPascalName}Form = (props: ${singularPascalName}FormProps) => {
|
|
|
80
81
|
}
|
|
81
82
|
<% } %>
|
|
82
83
|
<% }) %>
|
|
83
|
-
props.onSave(data
|
|
84
|
+
props.onSave(data)
|
|
84
85
|
}
|
|
85
86
|
|
|
86
87
|
return (
|
|
@@ -136,7 +137,7 @@ const ${singularPascalName}Form = (props: ${singularPascalName}FormProps) => {
|
|
|
136
137
|
<% } else { %>
|
|
137
138
|
<${column.component}
|
|
138
139
|
name="${column.name}"
|
|
139
|
-
${column.defaultProp}={${column.deserializeFunction && column.deserializeFunction + '('}props.${singularCamelName}?.<%= column.name %>${column.deserializeFunction && ')'}}
|
|
140
|
+
${column.defaultProp}={${column.deserializeFunction && column.deserializeFunction + '('}props.${singularCamelName}?.<%= column.name %>${column.deserializeFunction && ')'}<%= !column.deserializeFunction && !column.isRequired ? ' ?? undefined' : '' %>}
|
|
140
141
|
className="rw-input"
|
|
141
142
|
errorClassName="rw-input rw-input-error"
|
|
142
143
|
<%= !column.validation ? '' : `validation=${column.validation}` %>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { WrapperProps } from '@cedarjs/router'
|
|
2
|
+
|
|
3
|
+
import ScaffoldLayout from '${importLayoutNames}'
|
|
4
|
+
|
|
5
|
+
function ${pluralPascalName}ScaffoldLayout({ children }: WrapperProps) {
|
|
6
|
+
return (
|
|
7
|
+
<ScaffoldLayout
|
|
8
|
+
title="${pluralPascalName}"
|
|
9
|
+
titleTo="${pluralRouteName}"
|
|
10
|
+
buttonLabel="New ${singularPascalName}"
|
|
11
|
+
buttonTo="${newRouteName}"
|
|
12
|
+
>
|
|
13
|
+
{children}
|
|
14
|
+
</ScaffoldLayout>
|
|
15
|
+
)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export default ${pluralPascalName}ScaffoldLayout
|
|
@@ -83,14 +83,25 @@ describe('jsonTruncate', () => {
|
|
|
83
83
|
|
|
84
84
|
describe('timeTag', () => {
|
|
85
85
|
it('renders a date', async () => {
|
|
86
|
+
<% if (parsedDateTime) { %>
|
|
87
|
+
render(<div>{timeTag(new Date('1970-08-20'))}</div>)
|
|
88
|
+
<% } else { %>
|
|
86
89
|
render(<div>{timeTag(new Date('1970-08-20').toUTCString())}</div>)
|
|
90
|
+
<% } %>
|
|
87
91
|
|
|
88
92
|
await waitFor(() => screen.getByText(/1970.*00:00:00/))
|
|
89
93
|
})
|
|
90
94
|
|
|
95
|
+
<% if (parsedDateTime) { %>
|
|
96
|
+
it('can take an empty input', async () => {
|
|
97
|
+
expect(timeTag(null)).toEqual('')
|
|
98
|
+
expect(timeTag(undefined)).toEqual('')
|
|
99
|
+
})
|
|
100
|
+
<% } else { %>
|
|
91
101
|
it('can take an empty input string', async () => {
|
|
92
102
|
expect(timeTag('')).toEqual('')
|
|
93
103
|
})
|
|
104
|
+
<% } %>
|
|
94
105
|
})
|
|
95
106
|
|
|
96
107
|
describe('jsonDisplay', () => {
|
|
@@ -25,7 +25,7 @@ export const jsonDisplay = (obj: unknown) => {
|
|
|
25
25
|
)
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
export const truncate = (value: string | number) => {
|
|
28
|
+
export const truncate = (value: string | number | null | undefined) => {
|
|
29
29
|
let output = value?.toString() ?? ''
|
|
30
30
|
|
|
31
31
|
if (output.length > MAX_STRING_LENGTH) {
|
|
@@ -39,20 +39,32 @@ export const jsonTruncate = (obj: unknown) => {
|
|
|
39
39
|
return truncate(JSON.stringify(obj, null, 2))
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
export function timeTag(dateTime: string | undefined | null) {
|
|
42
|
+
export function timeTag(dateTime: <%= parsedDateTime ? 'Date' : 'string' %> | undefined | null) {
|
|
43
43
|
let output: string | React.JSX.Element = ''
|
|
44
44
|
|
|
45
45
|
if (dateTime) {
|
|
46
|
+
<% if (parsedDateTime) { %>
|
|
47
|
+
if (!Number.isNaN(dateTime.getTime())) {
|
|
48
|
+
const isoString = dateTime.toISOString()
|
|
49
|
+
|
|
50
|
+
output = (
|
|
51
|
+
<time dateTime={isoString} title={isoString}>
|
|
52
|
+
{dateTime.toUTCString()}
|
|
53
|
+
</time>
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
<% } else { %>
|
|
46
57
|
output = (
|
|
47
58
|
<time dateTime={dateTime} title={dateTime}>
|
|
48
59
|
{new Date(dateTime).toUTCString()}
|
|
49
60
|
</time>
|
|
50
61
|
)
|
|
62
|
+
<% } %>
|
|
51
63
|
}
|
|
52
64
|
|
|
53
65
|
return output
|
|
54
66
|
}
|
|
55
67
|
|
|
56
|
-
export const checkboxInputTag = (checked: boolean) => {
|
|
57
|
-
return <input type="checkbox" checked={checked} disabled />
|
|
68
|
+
export const checkboxInputTag = (checked: boolean | null | undefined) => {
|
|
69
|
+
return <input type="checkbox" checked={!!checked} disabled />
|
|
58
70
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { Listr } from "listr2";
|
|
4
|
-
import {
|
|
4
|
+
import { colors as c } from "@cedarjs/cli-helpers/colors";
|
|
5
|
+
import { recordTelemetryAttributes } from "@cedarjs/cli-helpers/telemetry";
|
|
5
6
|
import { errorTelemetry } from "@cedarjs/telemetry";
|
|
6
7
|
import {
|
|
7
8
|
getPaths,
|
|
@@ -2,8 +2,9 @@ import ansis from "ansis";
|
|
|
2
2
|
import boxen from "boxen";
|
|
3
3
|
import camelcase from "camelcase";
|
|
4
4
|
import { Listr } from "listr2";
|
|
5
|
-
import {
|
|
5
|
+
import { colors as c } from "@cedarjs/cli-helpers/colors";
|
|
6
6
|
import { formatCedarCommand } from "@cedarjs/cli-helpers/packageManager/display";
|
|
7
|
+
import { recordTelemetryAttributes } from "@cedarjs/cli-helpers/telemetry";
|
|
7
8
|
import { generate as generateTypes } from "@cedarjs/internal/dist/generate/generate";
|
|
8
9
|
import { getConfig } from "@cedarjs/project-config";
|
|
9
10
|
import { errorTelemetry } from "@cedarjs/telemetry";
|
|
@@ -18,7 +19,11 @@ import {
|
|
|
18
19
|
getEnum,
|
|
19
20
|
verifyModelName
|
|
20
21
|
} from "../../../lib/schemaHelpers.js";
|
|
21
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
DEFAULT_IGNORE_FIELDS_FOR_INPUT,
|
|
24
|
+
redactedModelFields,
|
|
25
|
+
relationsForModel
|
|
26
|
+
} from "../helpers.js";
|
|
22
27
|
import { files as serviceFiles } from "../service/serviceHandler.js";
|
|
23
28
|
import { templateForFile } from "../yargsHandlerHelpers.js";
|
|
24
29
|
import {
|
|
@@ -26,7 +31,6 @@ import {
|
|
|
26
31
|
missingRelatedModels,
|
|
27
32
|
writeFilesWithStubsTask
|
|
28
33
|
} from "./stubFiles.js";
|
|
29
|
-
const DEFAULT_IGNORE_FIELDS_FOR_INPUT = ["createdAt", "updatedAt"];
|
|
30
34
|
const missingIdConsoleMessage = () => {
|
|
31
35
|
const line1 = ansis.bold.yellow("WARNING") + ": Cannot generate CRUD SDL without an `@id` database column.";
|
|
32
36
|
const line2 = "If you are trying to generate for a many-to-many join table ";
|
|
@@ -60,7 +64,8 @@ const modelFieldToSDL = ({
|
|
|
60
64
|
Bytes: "Byte"
|
|
61
65
|
};
|
|
62
66
|
const gqlType = prismaTypeToGraphqlType[field.type] || field.type;
|
|
63
|
-
const
|
|
67
|
+
const listItemType = field.kind === "object" ? gqlType : `${gqlType}!`;
|
|
68
|
+
const type = field.isList ? `[${listItemType}]` : gqlType;
|
|
64
69
|
const isRequired = field.isRequired && required || field.isList || field.isId;
|
|
65
70
|
const fieldContent = `${field.name}: ${type}${isRequired ? "!" : ""}`;
|
|
66
71
|
if (docs) {
|
|
@@ -331,6 +336,7 @@ const handler = async ({
|
|
|
331
336
|
export {
|
|
332
337
|
files,
|
|
333
338
|
handler,
|
|
339
|
+
modelFieldToSDL,
|
|
334
340
|
printRedactedFieldsNote,
|
|
335
341
|
redactedSensitiveFields,
|
|
336
342
|
stubFiles
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
2
|
import { terminalLink } from "termi-link";
|
|
3
|
-
import { recordTelemetryAttributes } from "@cedarjs/cli-helpers";
|
|
3
|
+
import { recordTelemetryAttributes } from "@cedarjs/cli-helpers/telemetry";
|
|
4
4
|
const DEFAULT_LENGTH = 32;
|
|
5
5
|
const generateSecret = (length = DEFAULT_LENGTH) => {
|
|
6
6
|
return crypto.randomBytes(Math.trunc(length)).toString("base64");
|