1
0
mirror of synced 2026-02-03 18:01:02 -05:00

Compare commits

...

117 Commits

Author SHA1 Message Date
Aleksandra Sikora
8f2a5fc895 Merge branch 'canary' into subtemplate 2022-02-07 11:23:17 +01:00
Aleksandra
7b47463f69 fix imports 2022-01-20 14:07:08 +01:00
Aleksandra Sikora
06a91c39ce Merge branch 'canary' into subtemplate 2022-01-20 13:32:08 +01:00
Roshan Manuel
b9104e0917 add parentmodel options when belongsTo is specified 2022-01-09 15:59:24 -05:00
Roshan Manuel
c1787029de merge codegen keys 2022-01-09 15:26:11 -05:00
Aleksandra Sikora
f8e26e8014 Update packages/generator/src/generators/template-builders/field-values-builder.ts 2022-01-07 13:36:40 +01:00
Aleksandra
b531f7e68d Merge branch 'canary' into subtemplate 2022-01-07 13:27:57 +01:00
Aleksandra
19bcd68b09 fix types 2022-01-07 13:26:13 +01:00
Aleksandra Sikora
67e4a1e21b Merge branch 'canary' into subtemplate 2022-01-03 19:08:51 +01:00
Roshan Manuel
a4a64b282f move codegen key into blitz region 2021-12-29 17:35:02 -05:00
Roshan Manuel
26a2c948fa get zodtype from codegen, add comment about reverse map 2021-12-14 18:37:28 -05:00
Roshan Manuel
09535eeb0e Merge branch 'subtemplate' of https://github.com/blitz-js/blitz into subtemplate 2021-12-14 18:21:04 -05:00
Roshan Manuel
2a03bbe141 udpate comment 2021-12-14 18:21:02 -05:00
Roshan Manuel
cd1df5c900 delete schema needs only id 2021-12-14 18:20:24 -05:00
Aleksandra
9b409dfd8d Simplify getcodegen function 2021-12-14 15:54:10 +01:00
Aleksandra
dacbc38306 Fix empty line and template regex 2021-12-14 15:38:50 +01:00
Aleksandra
9dba376710 bring back removed test 2021-12-14 15:08:09 +01:00
Aleksandra
b0c9638f5b fix wrong import 2021-12-14 14:58:21 +01:00
Aleksandra
7226cb92ce Merge branch 'canary' into subtemplate 2021-12-14 14:22:47 +01:00
Aleksandra
b8b19728cd Fix minor bugs, make generator tests pass 2021-12-14 13:37:36 +01:00
Aleksandra
f3bef294a4 Add more tests, fix bugs 2021-12-14 13:02:43 +01:00
Aleksandra
a468f5ca59 Move codegen config to next shared and extend its keys 2021-12-14 12:10:10 +01:00
Aleksandra
55144fb287 Merge branch 'subtemplate' of https://github.com/blitz-js/blitz into subtemplate 2021-12-13 16:56:54 +01:00
Roshan Manuel
4f17fe29d8 clean up naming, add belongsto to special handling 2021-12-11 23:30:17 -05:00
Roshan Manuel
4c05a8a4f7 fix spacing issue in mutations 2021-12-10 12:29:11 -05:00
Aleksandra
2eb0441624 improve type 2021-12-10 18:23:48 +01:00
Aleksandra
950d67f7ab fix issue with optional types and defaults, refactor code a little 2021-12-10 18:21:27 +01:00
Aleksandra
0bc3f779d3 Fix uuid issue, add more types 2021-12-10 17:54:48 +01:00
Aleksandra
53fa6cc044 Fix re-generating content 2021-12-10 17:29:12 +01:00
Roshan Manuel
bd064ebfd6 move codegen key fetch into utility, remove codegen key from new app template 2021-11-30 20:01:32 -05:00
Roshan Manuel
08fddcc4f4 add warning and debug logs about process to field.ts 2021-11-23 18:47:10 -05:00
Roshan Manuel
fda6df56e5 attempt to get config vals for field 2021-11-20 23:52:19 -05:00
Roshan Manuel
b81a1787e2 fix lint - update type, fieldTemplateValues is now dynamic 2021-11-18 17:52:13 -05:00
Roshan Manuel
4c594f0bf4 fix test 2021-11-18 17:45:34 -05:00
Roshan Manuel
402e909e22 values keys are taken from config, add number to field types in new apps and auth example, FieldComponent > component 2021-11-18 17:44:31 -05:00
Roshan Manuel
f05bb711d2 add input type to component 2021-11-18 17:20:43 -05:00
Roshan Manuel
3331cfb796 Merge branch 'subtemplate' of https://github.com/blitz-js/blitz into subtemplate 2021-11-18 17:10:20 -05:00
Roshan Manuel
1edf2b2c72 ensure inputType is present, fix test 2021-11-18 17:09:09 -05:00
Brandon Bayer
d53cce87dc Merge branch 'canary' into subtemplate 2021-11-17 19:11:58 -05:00
Roshan Manuel
fd52380c6c update auth example config, fix builder test 2021-11-09 23:31:51 -05:00
Roshan Manuel
b5516d8707 disable requiring await for fns 2021-11-01 21:28:03 -04:00
Roshan Manuel
f1186c8c58 add fast-glob as dependency to generator package.json 2021-10-31 20:48:14 -04:00
Roshan Manuel
678d5efe6f add fast-glob as dependency to generator package.json 2021-10-31 19:16:22 -04:00
Roshan Manuel
be2dabb115 escape special characters used by fast-glob 2021-10-26 17:17:02 -04:00
Roshan Manuel
12e3fcdfa0 use existing generated file if exists when generating 2021-10-25 19:42:23 -04:00
Roshan Manuel
64e1b68898 create generic fn to get resource value from map. Defaults have builtin fns too 2021-10-22 12:58:28 -04:00
Roshan Manuel
06a91c995c ignore ts error 2021-10-22 12:43:33 -04:00
Roshan Manuel
ec2127748c move webpack config above codegen 2021-10-22 12:42:34 -04:00
Roshan Manuel
67729da47e run prisma format before prompt to migrate 2021-10-22 09:05:23 -04:00
Roshan Manuel
62db0d4dbc update template name, type and defaults 2021-10-20 19:08:02 -04:00
Roshan Manuel
0d20dbe2f4 Merge branch 'subtemplate' of https://github.com/blitz-js/blitz into subtemplate 2021-10-17 08:07:30 -04:00
Roshan Manuel
dfa4d2d6aa consolidate options types, add raw parent model name to options, rename zodType 2021-10-17 07:56:39 -04:00
Brandon Bayer
d96692de7e Merge branch 'canary' into subtemplate 2021-10-14 18:25:54 -04:00
Roshan Manuel
c064b866fc fix comment on runtime complexity 2021-10-11 11:19:37 -04:00
Roshan Manuel
b0dc5ca486 fix page generator test 2021-10-08 17:13:47 -04:00
Roshan Manuel
f076fcf94a fix parent zod schema, comma required 2021-10-08 17:13:31 -04:00
Roshan Manuel
3c57229d4e make parent zod id optional in type 2021-10-08 17:13:04 -04:00
Roshan Manuel
c59c3609fb add int to maps 2021-10-08 07:45:28 -04:00
Roshan Manuel
09ca2c0a5a builder has option arg to pass in memfseditor, current builders use it 2021-10-08 07:43:21 -04:00
Roshan Manuel
dfe3d0255e field values infers parent model id type 2021-10-08 07:42:32 -04:00
Roshan Manuel
5264915130 extract getprisma logic into utility 2021-10-08 07:37:04 -04:00
Roshan Manuel
e9cbbf11c7 remove todo comment 2021-10-07 16:23:01 -04:00
Roshan Manuel
33e701de2c ensure correct typing in abstract generator 2021-10-07 11:34:04 -04:00
Roshan Manuel
db992c048e narrow typing of templatevalues 2021-10-07 11:33:31 -04:00
Roshan Manuel
a0bccd02a1 create app template values interface 2021-10-07 11:27:28 -04:00
Roshan Manuel
8753ae4f0a attempt fix for blitz_app_dir not set in test 2021-10-07 11:18:57 -04:00
Roshan Manuel
fafb79fd53 ensure correct zod type for uuid in new apps 2021-10-07 02:37:18 -04:00
Roshan Manuel
990ba07cb0 fix zod mapping check 2021-10-07 02:36:39 -04:00
Roshan Manuel
5db5b67d5d Merge branch 'canary' into subtemplate 2021-10-06 13:45:08 -04:00
Roshan Manuel
b2cafb0dff support user specified id type 2021-10-06 13:43:40 -04:00
Roshan Manuel
ebb50ac6d2 update logic to get zod type 2021-10-06 13:42:22 -04:00
Roshan Manuel
b767ababf4 remove unused comment 2021-10-06 13:40:42 -04:00
Roshan Manuel
cb6e949ed7 strongly type map, add zod types map 2021-10-06 13:40:11 -04:00
Brandon Bayer
77c01f30bc Merge branch 'canary' into subtemplate 2021-10-02 16:40:16 -04:00
Roshan Manuel
254e4770db ensure test str is correct 2021-10-01 17:18:59 -04:00
Roshan Manuel
f361a8ba51 fix test logic 2021-10-01 13:13:43 -04:00
Roshan Manuel
933d2667dc fix logic obtaining component map 2021-10-01 13:13:30 -04:00
Roshan Manuel
5a29427ab9 allow 0 or more spaces not 1 or more 2021-10-01 13:12:36 -04:00
Roshan Manuel
87a0217d45 only LabeledTextFields at present 2021-09-27 12:56:11 -04:00
Roshan Manuel
bc748ef40d fix lint errors 2021-09-27 12:55:45 -04:00
Roshan Manuel
b7c68b7c03 create default map 2021-09-26 19:45:53 -04:00
Roshan Manuel
7da8661df6 remove blitz_app_dir being set in postwrite 2021-09-26 19:32:21 -04:00
Roshan Manuel
9d2a05c8e4 Merge branch 'canary' into subtemplate 2021-09-26 19:06:58 -04:00
Roshan Manuel
2cc50e5d80 cache config 2021-09-22 20:31:00 -04:00
Roshan Manuel
4fe69aea56 move field regex into class, add tests for regex, fix page generator test 2021-09-19 14:42:50 -04:00
Roshan Manuel
8a1d682f45 Merge branch 'canary' into subtemplate 2021-09-16 14:14:40 -05:00
Roshan Manuel
bdd84e51ba fix lint, remove debug statements 2021-09-16 14:49:46 -04:00
Roshan Manuel
2140b8eaae make promises const, remove debug import 2021-09-16 00:51:14 -04:00
Roshan Manuel
3a468395b9 abstract builder now builds field template values 2021-09-16 00:46:35 -04:00
Roshan Manuel
fc1594886c fix BLITZ_APP_DIR not found error 2021-09-16 00:45:57 -04:00
Roshan Manuel
f997255ed1 add template key to base config type, add defaults for new apps 2021-09-16 00:45:27 -04:00
Roshan Manuel
6352ef30c0 match zodType name with builder values 2021-09-16 00:43:02 -04:00
Roshan Manuel
38adf5b506 create component mapper, call in field values builder 2021-09-14 22:37:46 -04:00
Roshan Manuel
850190454f fix lint again 2 2021-09-13 18:11:07 -04:00
Roshan Manuel
5619f22ed5 fix lint again 2021-09-13 18:07:44 -04:00
Roshan Manuel
c384cf25fb fix lint 2021-09-13 17:44:22 -04:00
Roshan Manuel
44e05c971b fix ci 2021-09-13 17:36:42 -04:00
Roshan Manuel
29282fc825 queries generator uses field values builder 2021-09-13 15:44:24 -04:00
Roshan Manuel
6639f4172e add fields to field values builder 2021-09-13 15:33:20 -04:00
Roshan Manuel
64d053537f page generator uses field values builder 2021-09-13 15:29:00 -04:00
Roshan Manuel
a1153981b9 mutations generator uses field values builder 2021-09-13 15:28:17 -04:00
Roshan Manuel
7a3f57c8e3 update abstract builder with common functions 2021-09-13 15:26:55 -04:00
Roshan Manuel
2193cb88a9 rename form values builder 2021-09-13 14:56:34 -04:00
Roshan Manuel
2b34600365 app generator uses app values builder 2021-09-13 14:56:11 -04:00
Roshan Manuel
72978b0c16 model generator uses null builder by default 2021-09-13 14:36:43 -04:00
Roshan Manuel
b6ec8e7867 update templates with correct comments 2021-09-13 03:06:15 -04:00
Roshan Manuel
2dc66498e7 use field templates builder in form generator 2021-09-13 03:02:13 -04:00
Roshan Manuel
cc0c1ae1d5 update imports for builders 2021-09-13 03:00:10 -04:00
Roshan Manuel
93a0f88916 make base generator implement null generator (nothing in common among all generators) 2021-09-13 02:59:31 -04:00
Roshan Manuel
30588eec0c create builders for field template object generation 2021-09-13 02:56:43 -04:00
Roshan Manuel
c1f102119b move spacing into inflector 2021-09-13 02:35:17 -04:00
Roshan Manuel
0f9e9eb804 support '// template' 2021-09-03 22:20:30 -04:00
Roshan Manuel
956c58b12c form template now has spaces for some props 2021-08-31 17:11:56 -04:00
Roshan Manuel
9d8a0a5c16 update form generator 2021-08-31 17:09:49 -04:00
Roshan Manuel
8e64a6ce9b add form generator test 2021-08-31 17:04:35 -04:00
Roshan Manuel
1489092d4f update form and mutation generator, edit models 2021-08-27 18:09:00 -04:00
Roshan Manuel
42f4840c6d copy maastrich's template replacer 2021-08-27 18:08:02 -04:00
31 changed files with 937 additions and 276 deletions

View File

@@ -16,9 +16,6 @@ module.exports = withBundleAnalyzer({
cli: {
clearConsoleOnBlitzDev: false,
},
codegen: {
templateDir: "./my-templates",
},
log: {
// level: "trace",
},
@@ -40,4 +37,69 @@ module.exports = withBundleAnalyzer({
return config
},
*/
codegen: {
templateDir: "./my-templates",
fieldTypeMap: {
string: {
component: "LabeledTextField",
inputType: "text",
zodType: "string",
prismaType: "String",
},
boolean: {
component: "LabeledTextField",
inputType: "text",
zodType: "boolean",
prismaType: "Boolean",
},
int: {
component: "LabeledTextField",
inputType: "number",
zodType: "number",
prismaType: "Int",
},
number: {
component: "LabeledTextField",
inputType: "number",
zodType: "number",
prismaType: "Int",
},
bigint: {
component: "LabeledTextField",
inputType: "number",
zodType: "number",
prismaType: "BigInt",
},
float: {
component: "LabeledTextField",
inputType: "number",
zodType: "number",
prismaType: "Float",
},
decimal: {
component: "LabeledTextField",
inputType: "number",
zodType: "number",
prismaType: "Decimal",
},
datetime: {
component: "LabeledTextField",
inputType: "string",
zodType: "string",
prismaType: "DateTime",
},
uuid: {
component: "LabeledTextField",
inputType: "text",
zodType: "string().uuid",
prismaType: "Uuid",
},
json: {
component: "LabeledTextField",
inputType: "text",
zodType: "any",
prismaType: "Json",
},
},
},
})

View File

@@ -71,6 +71,20 @@ export interface ESLintConfig {
ignoreDuringBuilds?: boolean
}
export type CodegenField = {
component: string
inputType: string
zodType: string
prismaType: string
default?: string
[index: string]: string | undefined
}
export type CodegenConfig = {
templateDir?: string
fieldTypeMap?: Record<string, CodegenField>
}
export type NextConfig = { [key: string]: any } & {
i18n?: I18NConfig | null
@@ -127,9 +141,7 @@ export type NextConfig = { [key: string]: any } & {
httpsProxy?: string
noProxy?: string
}
codegen?: {
templateDir?: string
}
codegen?: CodegenConfig
log?: {
level?: LogLevel
type?: LogType

View File

@@ -2,15 +2,15 @@ import {
capitalize,
FormGenerator,
ModelGenerator,
ModelName,
modelName,
ModelNames,
modelNames,
MutationGenerator,
MutationsGenerator,
PageGenerator,
pluralCamel,
pluralPascal,
QueriesGenerator,
QueryGenerator,
singleCamel,
singlePascal,
uncapitalize,
} from "@blitzjs/generator"
import {flags} from "@oclif/command"
@@ -51,19 +51,6 @@ interface Args {
model: string
}
function modelName(input: string = "") {
return singleCamel(input)
}
function modelNames(input: string = "") {
return pluralCamel(input)
}
function ModelName(input: string = "") {
return singlePascal(input)
}
function ModelNames(input: string = "") {
return pluralPascal(input)
}
const generatorMap = {
[ResourceType.All]: [
PageGenerator,
@@ -254,6 +241,7 @@ export class Generate extends Command {
modelNames: modelNames(singularRootContext),
ModelName: ModelName(singularRootContext),
ModelNames: ModelNames(singularRootContext),
rawParentModelName: flags.parent,
parentModel: modelName(flags.parent),
parentModels: modelNames(flags.parent),
ParentModel: ModelName(flags.parent),

View File

@@ -40,6 +40,7 @@
"cross-spawn": "7.0.3",
"diff": "5.0.0",
"enquirer": "2.3.6",
"fast-glob": "^3.1.1",
"fs-extra": "^9.1.0",
"got": "^11.8.1",
"jscodeshift": "0.13.0",

View File

@@ -3,6 +3,7 @@ import * as babel from "@babel/core"
import babelTransformTypescript from "@babel/plugin-transform-typescript"
import Enquirer from "enquirer"
import {EventEmitter} from "events"
import {escapePath} from "fast-glob"
import * as fs from "fs-extra"
import j from "jscodeshift"
import {create as createStore, Store} from "mem-fs"
@@ -12,6 +13,8 @@ import * as path from "path"
import getBabelOptions, {Overrides} from "recast/parsers/_babel_options"
import * as babelParser from "recast/parsers/babel"
import {ConflictChecker} from "./conflict-checker"
import {IBuilder} from "./generators/template-builders/builder"
import {NullBuilder} from "./generators/template-builders/null-builder"
import {pipe} from "./utils/pipe"
import {readdirRecursive} from "./utils/readdir-recursive"
const debug = require("debug")("blitz:generator")
@@ -160,7 +163,12 @@ export abstract class Generator<
if (!this.options.destinationRoot) this.options.destinationRoot = process.cwd()
}
abstract getTemplateValues(): Promise<any>
public templateValuesBuilder: IBuilder<T, any> = NullBuilder
async getTemplateValues(): Promise<any> {
const values = await this.templateValuesBuilder.getTemplateValues(this.options)
return values
}
abstract getTargetDirectory(): string
@@ -196,6 +204,10 @@ export abstract class Generator<
return result
}
public fieldTemplateRegExp: RegExp = new RegExp(
/({?\/\*\s*template: (.*) \*\/}?|\s*\/\/\s*template: (.*))/g,
)
process(
input: Buffer,
pathEnding: string,
@@ -211,6 +223,21 @@ export abstract class Generator<
if (codeFileExtensions.test(pathEnding)) {
templatedFile = this.replaceConditionals(inputStr, templateValues, prettierOptions || {})
}
const fieldTemplateString = templatedFile
.match(this.fieldTemplateRegExp)?.[0]
.replace(this.fieldTemplateRegExp, "$2$3")
if (fieldTemplateString) {
const fieldTemplatePosition = templatedFile.search(this.fieldTemplateRegExp)
templatedFile = [
templatedFile.slice(0, fieldTemplatePosition),
...(templateValues.fieldTemplateValues?.map((values: any) =>
this.replaceTemplateValues(fieldTemplateString, values),
) || []),
templatedFile.slice(fieldTemplatePosition),
].join("")
}
templatedFile = this.replaceTemplateValues(templatedFile, templateValues)
if (!this.useTs && tsExtension.test(pathEnding)) {
templatedFile =
@@ -259,16 +286,34 @@ export abstract class Generator<
pathSuffix = path.join(this.getTargetDirectory(), pathSuffix)
const templateValues = await this.getTemplateValues()
this.fs.copy(this.sourcePath(filePath), this.destinationPath(pathSuffix), {
process: (input) =>
this.process(input, pathSuffix, templateValues, prettierOptions ?? undefined),
})
let sourcePath = this.sourcePath(filePath)
let destinationPath = this.destinationPath(pathSuffix)
let templatedPathSuffix = this.replaceTemplateValues(pathSuffix, templateValues)
if (!this.useTs && tsExtension.test(this.destinationPath(pathSuffix))) {
templatedPathSuffix = templatedPathSuffix.replace(tsExtension, ".js")
}
if (templatedPathSuffix !== pathSuffix) {
this.fs.move(this.destinationPath(pathSuffix), this.destinationPath(templatedPathSuffix))
let templatedDestinationPath = this.destinationPath(templatedPathSuffix)
const destinationExists = fs.existsSync(templatedDestinationPath)
if (destinationExists) {
const newContent = this.process(
this.fs.read(templatedDestinationPath, {raw: true}) as any,
pathSuffix,
templateValues,
prettierOptions ?? undefined,
)
this.fs.write(templatedDestinationPath, newContent)
} else {
this.fs.copy(escapePath(sourcePath), escapePath(destinationPath), {
process: (input) =>
this.process(input, pathSuffix, templateValues, prettierOptions ?? undefined),
})
if (templatedPathSuffix !== pathSuffix) {
this.fs.move(destinationPath, templatedDestinationPath)
}
}
} catch (error) {
baseLogger({displayDateTime: false}).error(`Error generating ${filePath}`)

View File

@@ -3,10 +3,10 @@ import spawn from "cross-spawn"
import {readJSONSync, writeJson} from "fs-extra"
import {baseLogger, log} from "next/dist/server/lib/logging"
import {join} from "path"
import username from "username"
import {Generator, GeneratorOptions, SourceRootType} from "../generator"
import {fetchLatestVersionsFor} from "../utils/fetch-latest-version-for"
import {getBlitzDependencyVersion} from "../utils/get-blitz-dependency-version"
import {AppValuesBuilder} from "./template-builders/app-values-builder"
function assert(condition: any, message: string): asserts condition {
if (!condition) throw new Error(message)
@@ -30,6 +30,11 @@ export interface AppGeneratorOptions extends GeneratorOptions {
form?: "React Final Form" | "React Hook Form" | "Formik"
onPostInstall?: () => Promise<void>
}
export interface AppTemplateValues {
name: string,
safeNameSlug: string,
username: string | undefined
}
type PkgManager = "npm" | "yarn" | "pnpm"
export class AppGenerator extends Generator<AppGeneratorOptions> {
@@ -52,13 +57,7 @@ export class AppGenerator extends Generator<AppGeneratorOptions> {
return ["jsconfig.json", "jest.config.js", "package.js.json", "pre-push-js"]
}
async getTemplateValues() {
return {
name: this.options.appName,
safeNameSlug: this.options.appName.replace(/[^a-zA-Z0-9-_]/g, "-"),
username: await username(),
}
}
templateValuesBuilder = new AppValuesBuilder(this.fs)
getTargetDirectory() {
return ""

View File

@@ -1,17 +1,9 @@
import {Generator, GeneratorOptions, SourceRootType} from "../generator"
import {FieldValuesBuilder, ResourceGeneratorOptions} from ".."
import {Generator, SourceRootType} from "../generator"
import {getTemplateRoot} from "../utils/get-template-root"
import {camelCaseToKebabCase} from "../utils/inflector"
export interface FormGeneratorOptions extends GeneratorOptions {
ModelName: string
ModelNames: string
modelName: string
modelNames: string
parentModel?: string
parentModels?: string
ParentModel?: string
ParentModels?: string
}
export interface FormGeneratorOptions extends ResourceGeneratorOptions {}
export class FormGenerator extends Generator<FormGeneratorOptions> {
sourceRoot: SourceRootType
@@ -22,33 +14,7 @@ export class FormGenerator extends Generator<FormGeneratorOptions> {
static subdirectory = "queries"
private getId(input: string = "") {
if (!input) return input
return `${input}Id`
}
private getParam(input: string = "") {
if (!input) return input
return `[${input}]`
}
// eslint-disable-next-line require-await
async getTemplateValues() {
return {
parentModelId: this.getId(this.options.parentModel),
parentModelParam: this.getParam(this.getId(this.options.parentModel)),
parentModel: this.options.parentModel,
parentModels: this.options.parentModels,
ParentModel: this.options.ParentModel,
ParentModels: this.options.ParentModels,
modelId: this.getId(this.options.modelName),
modelIdParam: this.getParam(this.getId(this.options.modelName)),
modelName: this.options.modelName,
modelNames: this.options.modelNames,
ModelName: this.options.ModelName,
ModelNames: this.options.ModelNames,
}
}
templateValuesBuilder = new FieldValuesBuilder()
getTargetDirectory() {
const context = this.options.context ? `${camelCaseToKebabCase(this.options.context)}/` : ""

View File

@@ -1,11 +1,11 @@
import * as ast from "@mrleebo/prisma-ast"
import {spawn} from "cross-spawn"
import {baseLogger, log, newline} from "next/dist/server/lib/logging"
import {log,newline} from "next/dist/server/lib/logging"
import which from "npm-which"
import path from "path"
import {Generator, GeneratorOptions, SourceRootType} from "../generator"
import {Field} from "../prisma/field"
import {Model} from "../prisma/model"
import {getPrismaSchema} from "../utils/get-prisma-schema"
import {getTemplateRoot} from "../utils/get-template-root"
export interface ModelGeneratorOptions extends GeneratorOptions {
@@ -23,8 +23,6 @@ export class ModelGenerator extends Generator<ModelGeneratorOptions> {
static subdirectory = "../.."
unsafe_disableConflictChecker = true
async getTemplateValues() {}
getTargetDirectory() {
return ""
}
@@ -41,26 +39,15 @@ export class ModelGenerator extends Generator<ModelGeneratorOptions> {
// eslint-disable-next-line require-await
async write() {
const schemaPath = path.resolve("db/schema.prisma")
if (!this.fs.exists(schemaPath)) {
throw new Error("Prisma schema file was not found")
}
let schema: ast.Schema
try {
schema = ast.getSchema(this.fs.read(schemaPath))
} catch (err) {
baseLogger({displayDateTime: false}).error("Failed to parse db/schema.prisma file")
throw err
}
const {schema, schemaPath} = getPrismaSchema(this.fs)
const {modelName, extraArgs, dryRun} = this.options
let updatedOrCreated = "created"
let fields = (extraArgs.length === 1 && extraArgs[0].includes(" ")
let fieldPromises = (extraArgs.length === 1 && extraArgs[0].includes(" ")
? extraArgs[0].split(" ")
: extraArgs
).flatMap((input) => Field.parse(input, schema))
).map((input) => Field.parse(input, schema))
let fields = (await Promise.all(fieldPromises)).flatMap(fieldArray => fieldArray)
const modelDefinition = new Model(modelName, fields)
let model: ast.Model | undefined
@@ -99,10 +86,13 @@ export class ModelGenerator extends Generator<ModelGeneratorOptions> {
}
async postWrite() {
const prismaBin = which(process.cwd()).sync("prisma")
//@ts-ignore
spawn.sync(prismaBin, ["format"], {stdio: "inherit"})
const shouldMigrate = await this.prismaMigratePrompt()
if (shouldMigrate) {
await new Promise<void>((res, rej) => {
const prismaBin = which(process.cwd()).sync("prisma")
const child = spawn(prismaBin, ["migrate", "dev"], {stdio: "inherit"})
child.on("exit", (code) => (code === 0 ? res() : rej()))
})

View File

@@ -1,17 +1,9 @@
import {Generator, GeneratorOptions, SourceRootType} from "../generator"
import {FieldValuesBuilder, ResourceGeneratorOptions} from ".."
import {Generator, SourceRootType} from "../generator"
import {getTemplateRoot} from "../utils/get-template-root"
import {camelCaseToKebabCase} from "../utils/inflector"
export interface MutationsGeneratorOptions extends GeneratorOptions {
ModelName: string
ModelNames: string
modelName: string
modelNames: string
parentModel?: string
parentModels?: string
ParentModel?: string
ParentModels?: string
}
export interface MutationsGeneratorOptions extends ResourceGeneratorOptions {}
export class MutationsGenerator extends Generator<MutationsGeneratorOptions> {
sourceRoot: SourceRootType
@@ -21,33 +13,7 @@ export class MutationsGenerator extends Generator<MutationsGeneratorOptions> {
}
static subdirectory = "mutations"
private getId(input: string = "") {
if (!input) return input
return `${input}Id`
}
private getParam(input: string = "") {
if (!input) return input
return `[${input}]`
}
// eslint-disable-next-line require-await
async getTemplateValues() {
return {
parentModelId: this.getId(this.options.parentModel),
parentModelParam: this.getParam(this.getId(this.options.parentModel)),
parentModel: this.options.parentModel,
parentModels: this.options.parentModels,
ParentModel: this.options.ParentModel,
ParentModels: this.options.ParentModels,
modelId: this.getId(this.options.modelName),
modelIdParam: this.getParam(this.getId(this.options.modelName)),
modelName: this.options.modelName,
modelNames: this.options.modelNames,
ModelName: this.options.ModelName,
ModelNames: this.options.ModelNames,
}
}
templateValuesBuilder = new FieldValuesBuilder(this.fs)
getTargetDirectory() {
const context = this.options.context ? `${camelCaseToKebabCase(this.options.context)}/` : ""

View File

@@ -1,17 +1,9 @@
import {Generator, GeneratorOptions, SourceRootType} from "../generator"
import {FieldValuesBuilder, ResourceGeneratorOptions} from ".."
import {Generator, SourceRootType} from "../generator"
import {getTemplateRoot} from "../utils/get-template-root"
import {camelCaseToKebabCase} from "../utils/inflector"
export interface PageGeneratorOptions extends GeneratorOptions {
ModelName: string
ModelNames: string
modelName: string
modelNames: string
parentModel?: string
parentModels?: string
ParentModel?: string
ParentModels?: string
}
export interface PageGeneratorOptions extends ResourceGeneratorOptions {}
export class PageGenerator extends Generator<PageGeneratorOptions> {
sourceRoot: SourceRootType
@@ -21,42 +13,7 @@ export class PageGenerator extends Generator<PageGeneratorOptions> {
}
static subdirectory = "pages"
private getId(input: string = "") {
if (!input) return input
return `${input}Id`
}
private getParam(input: string = "") {
if (!input) return input
return `[${input}]`
}
// eslint-disable-next-line require-await
async getTemplateValues() {
return {
parentModelId: this.getId(this.options.parentModel),
parentModelParam: this.getParam(this.getId(this.options.parentModel)),
parentModel: this.options.parentModel,
parentModels: this.options.parentModels,
ParentModel: this.options.ParentModel,
ParentModels: this.options.ParentModels,
modelId: this.getId(this.options.modelName),
modelIdParam: this.getParam(this.getId(this.options.modelName)),
modelName: this.options.modelName,
modelNames: this.options.modelNames,
ModelName: this.options.ModelName,
ModelNames: this.options.ModelNames,
modelNamesPath: this.getModelNamesPath(),
}
}
getModelNamesPath() {
const kebabCaseContext = this.options.context
? `${camelCaseToKebabCase(this.options.context)}/`
: ""
const kebabCaseModelNames = camelCaseToKebabCase(this.options.modelNames)
return kebabCaseContext + kebabCaseModelNames
}
templateValuesBuilder = new FieldValuesBuilder(this.fs)
getTargetDirectory() {
const kebabCaseModelName = camelCaseToKebabCase(this.options.modelNames)

View File

@@ -1,17 +1,9 @@
import {Generator, GeneratorOptions, SourceRootType} from "../generator"
import {FieldValuesBuilder, ResourceGeneratorOptions} from ".."
import {Generator, SourceRootType} from "../generator"
import {getTemplateRoot} from "../utils/get-template-root"
import {camelCaseToKebabCase} from "../utils/inflector"
export interface QueriesGeneratorOptions extends GeneratorOptions {
ModelName: string
ModelNames: string
modelName: string
modelNames: string
parentModel?: string
parentModels?: string
ParentModel?: string
ParentModels?: string
}
export interface QueriesGeneratorOptions extends ResourceGeneratorOptions {}
export class QueriesGenerator extends Generator<QueriesGeneratorOptions> {
sourceRoot: SourceRootType
@@ -21,33 +13,7 @@ export class QueriesGenerator extends Generator<QueriesGeneratorOptions> {
}
static subdirectory = "queries"
private getId(input: string = "") {
if (!input) return input
return `${input}Id`
}
private getParam(input: string = "") {
if (!input) return input
return `[${input}]`
}
// eslint-disable-next-line require-await
async getTemplateValues() {
return {
parentModelId: this.getId(this.options.parentModel),
parentModelParam: this.getParam(this.getId(this.options.parentModel)),
parentModel: this.options.parentModel,
parentModels: this.options.parentModels,
ParentModel: this.options.ParentModel,
ParentModels: this.options.ParentModels,
modelId: this.getId(this.options.modelName),
modelIdParam: this.getParam(this.getId(this.options.modelName)),
modelName: this.options.modelName,
modelNames: this.options.modelNames,
ModelName: this.options.ModelName,
ModelNames: this.options.ModelNames,
}
}
templateValuesBuilder = new FieldValuesBuilder(this.fs)
getTargetDirectory() {
const context = this.options.context ? `${camelCaseToKebabCase(this.options.context)}/` : ""

View File

@@ -0,0 +1,14 @@
import username from "username"
import {AppGeneratorOptions, AppTemplateValues} from "../.."
import {Builder} from "./builder"
export class AppValuesBuilder extends Builder<AppGeneratorOptions, AppTemplateValues> {
public async getTemplateValues(options: AppGeneratorOptions): Promise<AppTemplateValues> {
const values = {
name: options.appName,
safeNameSlug: options.appName.replace(/[^a-zA-Z0-9-_]/g, "-"),
username: await username(),
}
return values
}
}

View File

@@ -0,0 +1,118 @@
import {Editor} from "mem-fs-editor"
import {CodegenField} from "next/dist/server/config-shared"
import {GeneratorOptions} from "../../generator"
import {getCodegen, getResourceValueFromCodegen} from "../../utils/get-codegen"
import {
addSpaceBeforeCapitals,
camelCaseToKebabCase,
singleCamel,
singlePascal,
} from "../../utils/inflector"
export interface IBuilder<T, U> {
getTemplateValues(Options: T): Promise<U>
}
export interface ResourceGeneratorOptions extends GeneratorOptions {
ModelName: string
ModelNames: string
modelName: string
modelNames: string
rawParentModelName?: string
parentModel?: string
parentModels?: string
ParentModel?: string
ParentModels?: string
extraArgs?: string[]
}
export interface CommonTemplateValues {
parentModelId: string
parentModelParam: string
parentModel?: string
parentModels?: string
ParentModel?: string
ParentModels?: string
parentModelIdZodType?: string
modelId: string
modelIdZodType?: string
modelIdParam: string
modelName: string
modelNames: string
ModelName: string
ModelNames: string
modelNamesPath: string
fieldTemplateValues?: {[x: string]: any}
}
export abstract class Builder<T, U> implements IBuilder<T, U> {
public constructor(fs?: Editor) {
this.fs = fs
}
abstract getTemplateValues(Options: T): Promise<U>
public fs: Editor | undefined
public defaultFieldConfig: CodegenField = {
component: "LabeledTextField",
inputType: "text",
zodType: "string",
prismaType: "String",
}
public getId(input: string = "") {
if (!input) return input
return `${input}Id`
}
public getParam(input: string = "") {
if (!input) return input
return `[${input}]`
}
public getModelNamesPath(context: string | undefined, modelNames: string) {
const kebabCaseContext = context ? `${camelCaseToKebabCase(context)}/` : ""
const kebabCaseModelNames = camelCaseToKebabCase(modelNames)
return kebabCaseContext + kebabCaseModelNames
}
// eslint-disable-next-line require-await
public async getZodType(type: string = "") {
return getResourceValueFromCodegen(type, "zodType")
}
// eslint-disable-next-line require-await
public async getComponentForType(type: string = "") {
return getResourceValueFromCodegen(type, "component")
}
// eslint-disable-next-line require-await
public async getInputType(type: string = "") {
return getResourceValueFromCodegen(type, "inputType")
}
// eslint-disable-next-line require-await
public async getFieldTemplateValues(args: string[]) {
const argsPromises = args.map(async (arg: string) => {
let [valueName, typeName] = arg.split(":")
if (typeName.includes("?")) {
typeName = typeName.replace("?", "")
}
let values = {
attributeName: singleCamel(valueName),
fieldName: singleCamel(valueName),
FieldName: singlePascal(valueName),
field_name: addSpaceBeforeCapitals(valueName).toLocaleLowerCase(), // field name
Field_name: singlePascal(addSpaceBeforeCapitals(valueName).toLocaleLowerCase()), // Field name
Field_Name: singlePascal(addSpaceBeforeCapitals(valueName)), // Field Name
}
const codegen = await getCodegen()
// iterate over resources defined for this field type
const fieldConfig = codegen.fieldTypeMap?.[typeName] || this.defaultFieldConfig
values = {...values, ...fieldConfig}
return values
})
return Promise.all(argsPromises)
}
}

View File

@@ -0,0 +1,117 @@
import * as ast from "@mrleebo/prisma-ast"
import {create as createStore} from "mem-fs"
import {create as createEditor, Editor} from "mem-fs-editor"
import {getResourceValueFromCodegen} from "../../utils/get-codegen"
import {getPrismaSchema} from "../../utils/get-prisma-schema"
import {ModelName, modelName, ModelNames, modelNames} from "../../utils/model-names"
import {Builder, CommonTemplateValues, ResourceGeneratorOptions} from "./builder"
export class FieldValuesBuilder extends Builder<ResourceGeneratorOptions, CommonTemplateValues> {
private getEditor = (): Editor => {
if (this.fs !== undefined) {
return this.fs
}
const store = createStore()
this.fs = createEditor(store)
return this.fs
}
// eslint-disable-next-line require-await
public async getTemplateValues(options: ResourceGeneratorOptions): Promise<CommonTemplateValues> {
const values: CommonTemplateValues = {
parentModelId: this.getId(options.parentModel),
parentModelIdZodType: undefined,
parentModelParam: this.getParam(this.getId(options.parentModel)),
parentModel: options.parentModel,
parentModels: options.parentModels,
ParentModel: options.ParentModel,
ParentModels: options.ParentModels,
modelId: this.getId(options.modelName),
modelIdZodType: "number",
modelIdParam: this.getParam(this.getId(options.modelName)),
modelName: options.modelName,
modelNames: options.modelNames,
ModelName: options.ModelName,
ModelNames: options.ModelNames,
modelNamesPath: this.getModelNamesPath(options.context, options.modelNames),
}
if (options.extraArgs) {
// specialArgs - these are arguments like 'id' or 'belongsTo', which are not meant to
// be processed as fields but have their own special logic
let specialArgs: {[key in string]: string} = {}
const processSpecialArgs: Promise<void>[] = options.extraArgs.map(async (arg) => {
const [valueName, typeName] = arg.split(":")
if (valueName === "id") {
values.modelIdZodType = await this.getZodType(typeName)
specialArgs[arg] = "present"
}
if (valueName === "belongsTo") {
// TODO: Determine how this is done. The model will generate with a field with the id name
// and type of the parent of this model, and forms etc. should
// In addition, need to do the same logic that the options.parentModel != undefined below does
specialArgs[arg] = "present"
process.env.parentModel = typeName
options.rawParentModelName = typeName
options.parentModel = modelName(typeName)
options.parentModels = modelNames(typeName)
options.ParentModel = ModelName(typeName)
options.ParentModels = ModelNames(typeName)
}
})
await Promise.all(processSpecialArgs)
// Filter out special args by making sure the argument isn't present in the list
const nonSpecialArgs = options.extraArgs.filter((arg) => specialArgs[arg] !== "present")
// Get the parent model it type if options.parentModel exists
if (options.parentModel !== undefined && options.parentModel.length > 0) {
const {schema} = getPrismaSchema(this.getEditor())
// O(N) - N is total ast Blocks
const model = schema.list.find(function (component): component is ast.Model {
return component.type === "model" && component.name === options.rawParentModelName
})
if (model !== undefined) {
// O(N) - N is number of properties in parent model
const idField = model.properties.find(function (property): property is ast.Field {
return (
property.type === "field" &&
property.attributes?.findIndex((attr) => attr.name === "id") !== -1
)
})
// TODO: Do we want a map between prisma types and "user types", we can then use that map instead of these conditionals
// We have a map from "user types" (which are what users type into the blitz generate command)
// to primsa type and other types, but we dont have a reverse map 1:1. This is because we lose
// some information for certain maps. E.g.: fieldname:uuid will be converted into a Prisma field with
// the String type, and the uuid portion is added to a decorator at the end of the field.
// This means it is more complicated to extract the original "user specified type" than creating a reverse map
if (idField?.fieldType === "Int") {
// TODO: Check if ints have decorators that make them a different type, like Bigint, etc.
// And see if that has to map to a different user specified type
values.parentModelIdZodType = await getResourceValueFromCodegen("int", "zodType")
} else if (idField?.fieldType === "String") {
if (
idField.attributes?.find(
(attr) =>
attr.name === "default" &&
attr.args?.findIndex((arg) => arg.value === "uuid") !== -1,
)
) {
values.parentModelIdZodType = await getResourceValueFromCodegen("uuid", "zodType")
} else {
values.parentModelIdZodType = await getResourceValueFromCodegen("string", "zodType")
}
}
} else {
// TODO: handle scenario where parent wasnt found in existing schema. Should we throw an error, or a warning asking the user to verify that the parent model exists?
}
}
if (nonSpecialArgs.length > 0) {
const ftv = await this.getFieldTemplateValues(nonSpecialArgs)
return {...values, fieldTemplateValues: ftv}
}
}
return values
}
}

View File

@@ -0,0 +1,8 @@
import {IBuilder} from "./builder"
export const NullBuilder: IBuilder<any,any> = {
// eslint-disable-next-line require-await
getTemplateValues: async () => {
return {}
},
}

View File

@@ -8,6 +8,11 @@ export * from "./generators/query-generator"
export * from "./generators/form-generator"
export * from "./generator"
export * from "./conflict-checker"
export * from "./generators/template-builders/builder"
export * from "./generators/template-builders/null-builder"
export * from "./generators/template-builders/app-values-builder"
export * from "./generators/template-builders/field-values-builder"
export * from "./utils/model-names"
export {getLatestVersion} from "./utils/get-latest-version"
export {
singleCamel,
@@ -16,4 +21,5 @@ export {
pluralPascal,
capitalize,
uncapitalize,
addSpaceBeforeCapitals,
} from "./utils/inflector"

View File

@@ -1,6 +1,8 @@
import * as ast from "@mrleebo/prisma-ast"
import {baseLogger, log} from "next/dist/server/lib/logging"
import {getResourceConfigFromCodegen} from "../utils/get-codegen"
import {capitalize, singlePascal, uncapitalize} from "../utils/inflector"
const debug = require("debug")("blitz:field")
export enum FieldType {
Boolean = "Boolean",
@@ -55,25 +57,39 @@ export class Field {
relationToFields?: string[]
// 'name:type?[]:attribute' => Field
static parse(input: string, schema?: ast.Schema): Field[] {
static async parse(input: string, schema?: ast.Schema): Promise<Field[]> {
debug(`parsing "Field" for input ${input}`)
const [_fieldName, _fieldType = "String", _attribute] = input.split(":")
let attribute = _attribute
let fieldName = uncapitalize(_fieldName)
let fieldType = capitalize(_fieldType)
// Check if it would make sense to expose that to users as well?
// Also in the case of a relationship, need to use the raw model name, cant capitalize it.
const isId = fieldName === "id"
let isRequired = true
let isList = false
let isUpdatedAt = false
let isUnique = false
let defaultValue = undefined
let relationFromFields = undefined
let relationToFields = undefined
let maybeIdField = undefined
let isUnique = false
if (fieldType.includes("?")) {
fieldType = fieldType.replace("?", "")
isRequired = false
}
const {prismaType, default: defaultConfigValue} =
(await Field.getConfigForPrismaType(fieldType)) ?? {}
if (prismaType) {
fieldType = prismaType
}
if (defaultConfigValue) {
attribute = `default=${defaultConfigValue}`
}
if (fieldType.includes("[]")) {
fieldType = fieldType.replace("[]", "")
fieldName = uncapitalize(fieldName)
@@ -163,6 +179,10 @@ export class Field {
}
}
public static getConfigForPrismaType = async (fieldType: string) => {
return await getResourceConfigFromCodegen(fieldType.toLowerCase())
}
constructor(name: string, options: FieldArgs) {
if (!name) throw new MissingFieldNameError("[PrismaField]: A field name is required")
if (!options.type) {

View File

@@ -0,0 +1,101 @@
import {CodegenConfig, CodegenField, NextConfigComplete} from "next/dist/server/config-shared"
import {baseLogger} from "next/dist/server/lib/logging"
export const defaultCodegenConfig: CodegenConfig = {
fieldTypeMap: {
string: {
component: "LabeledTextField",
inputType: "text",
zodType: "string",
prismaType: "String",
},
boolean: {
component: "LabeledTextField",
inputType: "text",
zodType: "boolean",
prismaType: "Boolean",
},
int: {
component: "LabeledTextField",
inputType: "number",
zodType: "number",
prismaType: "Int",
},
number: {
component: "LabeledTextField",
inputType: "number",
zodType: "number",
prismaType: "Int",
},
bigint: {
component: "LabeledTextField",
inputType: "number",
zodType: "number",
prismaType: "BigInt",
},
float: {
component: "LabeledTextField",
inputType: "number",
zodType: "number",
prismaType: "Float",
},
decimal: {
component: "LabeledTextField",
inputType: "number",
zodType: "number",
prismaType: "Decimal",
},
datetime: {
component: "LabeledTextField",
inputType: "text",
zodType: "string",
prismaType: "DateTime",
},
uuid: {
component: "LabeledTextField",
inputType: "text",
zodType: "string().uuid",
prismaType: "String",
default: "uuid",
},
json: {
component: "LabeledTextField",
inputType: "text",
zodType: "any",
prismaType: "Json",
},
},
}
export const getResourceValueFromCodegen = async (
fieldType: string,
resource: keyof CodegenField,
): Promise<string | undefined> => {
const codegen = await getCodegen()
const templateValue = codegen.fieldTypeMap?.[fieldType]?.[resource]
return templateValue
}
export const getResourceConfigFromCodegen = async (
fieldType: string,
): Promise<CodegenField | undefined> => {
const codegen = await getCodegen()
const config = codegen.fieldTypeMap?.[fieldType]
return config
}
export const getCodegen = async (): Promise<NextConfigComplete["codegen"]> => {
try {
const {loadConfigAtRuntime} = await import("next/dist/server/config-shared")
const config = await loadConfigAtRuntime()
if (config.codegen !== undefined) {
// TODO: potentially verify that codegen is well formed using zod
return config.codegen
}
return defaultCodegenConfig
} catch (ex) {
baseLogger({displayDateTime: false}).warn("Failed loading config from blitz.config file " + ex)
return defaultCodegenConfig
}
}

View File

@@ -0,0 +1,20 @@
import * as ast from "@mrleebo/prisma-ast"
import {Editor} from "mem-fs-editor"
import { baseLogger } from "next/dist/server/lib/logging"
import path from "path"
export const getPrismaSchema = (memFsEditor : Editor): {schema: ast.Schema, schemaPath: string} => {
const schemaPath = path.resolve("db/schema.prisma")
if (!memFsEditor.exists(schemaPath)) {
throw new Error("Prisma schema file was not found")
}
let schema: ast.Schema
try {
schema = ast.getSchema(memFsEditor.read(schemaPath))
} catch (err) {
baseLogger({displayDateTime: false}).error("Failed to parse db/schema.prisma file")
throw err
}
return {schema, schemaPath}
}

View File

@@ -25,3 +25,7 @@ export const pluralCamel = pipe(plural, uncapitalize)
export function camelCaseToKebabCase(transformString: string) {
return transformString.replace(/([a-z0-9]|(?=[A-Z]))([A-Z])/g, "$1-$2").toLowerCase()
}
export function addSpaceBeforeCapitals(input: string): string {
return singleCamel(input).replace(/(?!^)([A-Z])/g, " $1")
}

View File

@@ -0,0 +1,14 @@
import {pluralCamel, pluralPascal, singleCamel, singlePascal} from "../index"
export function modelName(input: string = "") {
return singleCamel(input)
}
export function modelNames(input: string = "") {
return pluralCamel(input)
}
export function ModelName(input: string = "") {
return singlePascal(input)
}
export function ModelNames(input: string = "") {
return pluralPascal(input)
}

View File

@@ -6,7 +6,7 @@ const config: BlitzConfig = {
cookiePrefix: "__safeNameSlug__",
isAuthorized: simpleRolesIsAuthorized,
}),
],
],
/* Uncomment this to customize the webpack config
webpack: (config, { buildId, dev, isServer, defaultLoaders, webpack }) => {
// Note: we provide webpack above so you should not `require` it
@@ -14,6 +14,6 @@ const config: BlitzConfig = {
// Important: return the modified config
return config
},
*/
*/
}
module.exports = config

View File

@@ -6,7 +6,7 @@ export {FORM_ERROR} from "app/core/components/Form"
export function __ModelName__Form<S extends z.ZodType<any, any>>(props: FormProps<S>) {
return (
<Form<S> {...props}>
<LabeledTextField name="name" label="Name" placeholder="Name" />
{/* template: <__component__ name="__fieldName__" label="__Field_Name__" placeholder="__Field_Name__" type="__inputType__" /> */}
</Form>
)
}

View File

@@ -4,12 +4,12 @@ import {z} from "zod"
if (process.env.parentModel) {
const Create__ModelName__ = z.object({
name: z.string(),
__parentModelId__: z.number()
__parentModelId__: z.__parentModelIdZodType__(),
// template: __fieldName__: z.__zodType__(),
})
} else {
const Create__ModelName__ = z.object({
name: z.string(),
// template: __fieldName__: z.__zodType__(),
})
}

View File

@@ -3,7 +3,7 @@ import db from "db"
import {z} from "zod"
const Delete__ModelName__ = z.object({
id: z.number(),
id: z.__modelIdZodType__(),
})
export default resolver.pipe(

View File

@@ -3,8 +3,8 @@ import db from "db"
import {z} from "zod"
const Update__ModelName__ = z.object({
id: z.number(),
name: z.string(),
id: z.__modelIdZodType__(),
// template: __fieldName__: z.__zodType__(),
})
export default resolver.pipe(

View File

@@ -0,0 +1,223 @@
import {FieldValuesBuilder} from "../../src/generators/template-builders/field-values-builder"
describe("Form Generator", () => {
process.env.BLITZ_APP_DIR = process.cwd()
const generator = new FieldValuesBuilder()
it("Should work with simple types", async () => {
expect(
await generator.getFieldTemplateValues(["field1:string", "field2:string"]),
).toStrictEqual([
{
component: "LabeledTextField",
FieldName: "Field1",
Field_Name: "Field1",
Field_name: "Field1",
attributeName: "field1",
fieldName: "field1",
field_name: "field1",
zodType: "string",
prismaType: "String",
inputType: "text",
},
{
component: "LabeledTextField",
FieldName: "Field2",
Field_Name: "Field2",
Field_name: "Field2",
attributeName: "field2",
fieldName: "field2",
field_name: "field2",
zodType: "string",
prismaType: "String",
inputType: "text",
},
])
})
it("Should work with optional types", async () => {
expect(
await generator.getFieldTemplateValues(["field1:string?", "field2:number"]),
).toStrictEqual([
{
component: "LabeledTextField",
FieldName: "Field1",
Field_Name: "Field1",
Field_name: "Field1",
attributeName: "field1",
fieldName: "field1",
field_name: "field1",
zodType: "string",
prismaType: "String",
inputType: "text",
},
{
component: "LabeledTextField",
FieldName: "Field2",
Field_Name: "Field2",
Field_name: "Field2",
attributeName: "field2",
fieldName: "field2",
field_name: "field2",
zodType: "number",
prismaType: "Int",
inputType: "number",
},
])
})
it("Should work with default values", async () => {
expect(await generator.getFieldTemplateValues(["field1:string:default='test'"])).toStrictEqual([
{
component: "LabeledTextField",
FieldName: "Field1",
Field_Name: "Field1",
Field_name: "Field1",
attributeName: "field1",
fieldName: "field1",
field_name: "field1",
zodType: "string",
prismaType: "String",
inputType: "text",
},
])
})
it("Should work with different input types", async () => {
const fields = [
"field1:string?",
"field2:boolean",
"field3:int",
"field4:number",
"field5:bigint?",
"field6:float",
"field7:decimal",
"field8:datetime",
"field9:uuid",
"field10:json?",
]
expect(await generator.getFieldTemplateValues(fields)).toStrictEqual([
{
component: "LabeledTextField",
FieldName: "Field1",
Field_Name: "Field1",
Field_name: "Field1",
attributeName: "field1",
fieldName: "field1",
field_name: "field1",
zodType: "string",
prismaType: "String",
inputType: "text",
},
{
component: "LabeledTextField",
FieldName: "Field2",
Field_Name: "Field2",
Field_name: "Field2",
attributeName: "field2",
fieldName: "field2",
field_name: "field2",
zodType: "boolean",
prismaType: "Boolean",
inputType: "text",
},
{
component: "LabeledTextField",
FieldName: "Field3",
Field_Name: "Field3",
Field_name: "Field3",
attributeName: "field3",
fieldName: "field3",
field_name: "field3",
zodType: "number",
prismaType: "Int",
inputType: "number",
},
{
component: "LabeledTextField",
FieldName: "Field4",
Field_Name: "Field4",
Field_name: "Field4",
attributeName: "field4",
fieldName: "field4",
field_name: "field4",
zodType: "number",
prismaType: "Int",
inputType: "number",
},
{
component: "LabeledTextField",
FieldName: "Field5",
Field_Name: "Field5",
Field_name: "Field5",
attributeName: "field5",
fieldName: "field5",
field_name: "field5",
zodType: "number",
prismaType: "BigInt",
inputType: "number",
},
{
component: "LabeledTextField",
FieldName: "Field6",
Field_Name: "Field6",
Field_name: "Field6",
attributeName: "field6",
fieldName: "field6",
field_name: "field6",
zodType: "number",
prismaType: "Float",
inputType: "number",
},
{
component: "LabeledTextField",
FieldName: "Field7",
Field_Name: "Field7",
Field_name: "Field7",
attributeName: "field7",
fieldName: "field7",
field_name: "field7",
zodType: "number",
prismaType: "Decimal",
inputType: "number",
},
{
component: "LabeledTextField",
FieldName: "Field8",
Field_Name: "Field8",
Field_name: "Field8",
attributeName: "field8",
fieldName: "field8",
field_name: "field8",
zodType: "string",
prismaType: "DateTime",
inputType: "text",
},
{
component: "LabeledTextField",
FieldName: "Field9",
Field_Name: "Field9",
Field_name: "Field9",
attributeName: "field9",
fieldName: "field9",
field_name: "field9",
zodType: "string().uuid",
prismaType: "String",
inputType: "text",
default: "uuid",
},
{
component: "LabeledTextField",
FieldName: "Field10",
Field_Name: "Field10",
Field_name: "Field10",
attributeName: "field10",
fieldName: "field10",
field_name: "field10",
zodType: "any",
prismaType: "Json",
inputType: "text",
},
])
})
})

View File

@@ -0,0 +1,47 @@
import {FormGenerator} from "../../src/generators/form-generator"
describe("Form Generator", () => {
process.env.BLITZ_APP_DIR = process.cwd()
const generator = new FormGenerator({
ModelName: "project",
ModelNames: "projects",
modelName: "project",
modelNames: "projects",
extraArgs: ["myProjectName:string"],
})
it("Correctly generates field names", async () => {
const templateValues = await generator.getTemplateValues()
expect(templateValues.fieldTemplateValues[0].fieldName).toEqual("myProjectName")
expect(templateValues.fieldTemplateValues[0].FieldName).toEqual("MyProjectName")
expect(templateValues.fieldTemplateValues[0].field_name).toEqual("my project name")
expect(templateValues.fieldTemplateValues[0].Field_name).toEqual("My project name")
expect(templateValues.fieldTemplateValues[0].Field_Name).toEqual("My Project Name")
})
it("matches template comments correctly", () => {
const regex = generator.fieldTemplateRegExp
const curlyBraceComment1 = `{/* template: <__component__ name="__fieldName__" label="__Field_Name__" placeholder="__Field_Name__" /> */}`
expect(curlyBraceComment1.match(regex)?.[0].replace(regex, "$2$3")).toBe(
`<__component__ name="__fieldName__" label="__Field_Name__" placeholder="__Field_Name__" />`,
)
expect(curlyBraceComment1.match(regex)?.[0].replace(regex, "$2$3")).not.toBe(`something Random`)
const normalComment1 = `// template: __fieldName__: z.__zodType__(),`
expect(normalComment1.match(regex)?.[0].replace(regex, "$2$3")).toBe(
`__fieldName__: z.__zodType__(),`,
)
expect(normalComment1.match(regex)?.[0].replace(regex, "$2$3")).not.toBe(`something Random`)
const commentWithSpacing = `// template: __fieldName__: z.__zodType__(),`
const commentWithNoSpacing = `//template: __fieldName__: z.__zodType__(),`
expect(commentWithSpacing.match(regex)?.[0].replace(regex, "$2$3")).toBe(
`__fieldName__: z.__zodType__(),`,
)
expect(commentWithNoSpacing.match(regex)?.[0].replace(regex, "$2$3")).toBe(
`__fieldName__: z.__zodType__(),`,
)
})
})

View File

@@ -9,8 +9,10 @@ describe("PageGenerator", () => {
})
describe("#getModelNamesPath", () => {
it("returns path only with default modelNames", () => {
expect(generator.getModelNamesPath()).toEqual("projects")
it("returns path only with default modelNames", async () => {
expect((await generator.getTemplateValues()).modelNamesPath).toBe(
"projects",
)
})
describe("when generator has context option", () => {
@@ -22,8 +24,10 @@ describe("PageGenerator", () => {
context: "marketing",
})
it("returns path with context as prefix", () => {
expect(generator.getModelNamesPath()).toEqual("marketing/projects")
it("returns path with context as prefix", async () => {
expect((await generator.getTemplateValues()).modelNamesPath).toBe(
"marketing/projects",
)
})
})
})
@@ -34,6 +38,7 @@ describe("PageGenerator", () => {
expect(values).toEqual({
ModelName: "project",
ModelNames: "projects",
parentModelIdZodType: undefined,
ParentModel: undefined,
ParentModels: undefined,
modelNamesPath: "projects",
@@ -41,6 +46,7 @@ describe("PageGenerator", () => {
modelIdParam: "[projectId]",
modelName: "project",
modelNames: "projects",
modelIdZodType: "number",
parentModel: undefined,
parentModelId: "",
parentModelParam: "",

View File

@@ -2,75 +2,86 @@ import {Schema} from "@mrleebo/prisma-ast"
import {Field} from "../../src/prisma/field"
describe("Field", () => {
it("parses optional types", () => {
const [field] = Field.parse("name:string?")
process.env.BLITZ_APP_DIR = process.cwd()
it("parses optional types", async () => {
const [field] = await await Field.parse("name:string?")
expect(field.isRequired).toBe(false)
})
it("appends unique attribute", () => {
const [field] = Field.parse("email:string?:unique")
it("appends unique attribute", async () => {
const [field] = await Field.parse("email:string?:unique")
expect(field.isUnique).toBe(true)
})
it("appends updatedAt attribute", () => {
const [field] = Field.parse("updatedAt:DateTime:updatedAt")
it("appends updatedAt attribute", async () => {
const [field] = await Field.parse("updatedAt:DateTime:updatedAt")
expect(field.isUpdatedAt).toBe(true)
})
it("handles default simple attribute", () => {
const [field] = Field.parse("isActive:boolean:default=true")
it("handles default simple attribute", async () => {
const [field] = await Field.parse("isActive:boolean:default=true")
expect(field.default).toBe("true")
})
it("handles default uuid attribute", () => {
const [field] = Field.parse("id:string:default=uuid")
it("handles default uuid attribute", async () => {
const [field] = await Field.parse("id:string:default=uuid")
expect(field.default).toMatchObject({name: "uuid"})
})
it("handles uuid convenience syntax", () => {
const [field] = Field.parse("someSpecialToken:uuid")
it("handles uuid convenience syntax", async () => {
const [field] = await Field.parse("someSpecialToken:uuid")
expect(field.type).toBe("String")
expect(field.default).toMatchObject({name: "uuid"})
})
it("handles default autoincrement attribute", () => {
const [field] = Field.parse("id:int:default=autoincrement")
it("handles default autoincrement attribute", async () => {
const [field] = await Field.parse("id:int:default=autoincrement")
expect(field.default).toMatchObject({name: "autoincrement"})
})
it("has default field type", () => {
const [field] = Field.parse("name")
it("has default field type", async () => {
const [field] = await Field.parse("name")
expect(field.type).toBe("String")
})
it("allow number characters in model name", () => {
const [field] = Field.parse("name2")
it("allow number characters in model name", async () => {
const [field] = await Field.parse("name2")
expect(field.name).toBe("name2")
})
it("allow underscore characters in model name", () => {
const [field] = Field.parse("first_name")
it("allow underscore characters in model name", async () => {
const [field] = await Field.parse("first_name")
expect(field.name).toBe("first_name")
})
it("disallows number as a first character in model name", () => {
expect(() => Field.parse("2first")).toThrow()
it("disallows number as a first character in model name", async () => {
await expect(async () => await Field.parse("2first")).rejects.toThrowError(
"[Field.parse]: received unknown special character in field name: 2first",
)
})
it("disallows underscore as a first character in model name", () => {
expect(() => Field.parse("_first")).toThrow()
it("disallows underscore as a first character in model name", async () => {
await expect(async () => await Field.parse("_first")).rejects.toThrowError(
"[Field.parse]: received unknown special character in field name: _first",
)
})
it("disallows special characters in model name", () => {
expect(() => Field.parse("app-user:int")).toThrow()
it("disallows special characters in model name", async () => {
await expect(async () => await Field.parse("app-user:int")).rejects.toThrowError(
"[Field.parse]: received unknown special character in field name: app-user",
)
})
it("disallows optional list fields", () => {
expect(() => Field.parse("users:int?[]")).toThrow()
it("disallows optional list fields", async () => {
await expect(async () => await Field.parse("users:int?[]")).rejects.toThrowError(
"[PrismaField]: a type cannot be both optional and a list",
)
})
it("requires a name", () => {
expect(() => Field.parse(":int")).toThrow()
it("requires a name", async () => {
await expect(async () => await Field.parse(":int")).rejects.toThrowError(
"[Field.parse]: received unknown special character in field name: ",
)
})
describe("belongsTo", () => {
@@ -102,8 +113,8 @@ describe("Field", () => {
],
}
it("simple relation", () => {
const [relation, foreignKey] = Field.parse("belongsTo:task")
it("simple relation", async () => {
const [relation, foreignKey] = await Field.parse("belongsTo:task")
expect(relation).toMatchObject({
name: "task",
type: "Task",
@@ -113,8 +124,8 @@ describe("Field", () => {
expect(foreignKey).toMatchObject({name: "taskId", type: "Int"})
})
it("relation with schema", () => {
const [relation, foreignKey] = Field.parse("belongsTo:project?", schema)
it("relation with schema", async () => {
const [relation, foreignKey] = await Field.parse("belongsTo:project?", schema)
expect(relation).toMatchObject({
name: "project",
type: "Project",
@@ -125,8 +136,8 @@ describe("Field", () => {
expect(foreignKey).toMatchObject({name: "projectId", type: "String", isRequired: false})
})
it("relation with list directive", () => {
const [relation, foreignKey] = Field.parse("belongsTo:tasks[]", schema)
it("relation with list directive", async () => {
const [relation, foreignKey] = await Field.parse("belongsTo:tasks[]", schema)
expect(relation).toMatchObject({name: "tasks", type: "Task", isList: false})
expect(foreignKey).toMatchObject({name: "tasksId", type: "Int", isList: false})
})

View File

@@ -2,16 +2,16 @@ import {Field} from "../../src/prisma/field"
import {Model} from "../../src/prisma/model"
describe("Prisma Model", () => {
it("generates a proper model", () => {
it("generates a proper model", async () => {
expect(
new Model(
"user",
[
Field.parse("email:string:unique"),
Field.parse("updated:dateTime:updatedAt"),
Field.parse("recentLogins:dateTime[]"),
Field.parse("twoFactorEnabled:boolean"),
Field.parse("twoFactorMethod:string?"),
await Field.parse("email:string:unique"),
await Field.parse("updated:dateTime:updatedAt"),
await Field.parse("recentLogins:dateTime[]"),
await Field.parse("twoFactorEnabled:boolean"),
await Field.parse("twoFactorMethod:string?"),
].flat(),
).toString(),
).toMatchSnapshot()