Compare commits
117 Commits
v1.0.1-alp
...
subtemplat
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8f2a5fc895 | ||
|
|
7b47463f69 | ||
|
|
06a91c39ce | ||
|
|
b9104e0917 | ||
|
|
c1787029de | ||
|
|
f8e26e8014 | ||
|
|
b531f7e68d | ||
|
|
19bcd68b09 | ||
|
|
67e4a1e21b | ||
|
|
a4a64b282f | ||
|
|
26a2c948fa | ||
|
|
09535eeb0e | ||
|
|
2a03bbe141 | ||
|
|
cd1df5c900 | ||
|
|
9b409dfd8d | ||
|
|
dacbc38306 | ||
|
|
9dba376710 | ||
|
|
b0c9638f5b | ||
|
|
7226cb92ce | ||
|
|
b8b19728cd | ||
|
|
f3bef294a4 | ||
|
|
a468f5ca59 | ||
|
|
55144fb287 | ||
|
|
4f17fe29d8 | ||
|
|
4c05a8a4f7 | ||
|
|
2eb0441624 | ||
|
|
950d67f7ab | ||
|
|
0bc3f779d3 | ||
|
|
53fa6cc044 | ||
|
|
bd064ebfd6 | ||
|
|
08fddcc4f4 | ||
|
|
fda6df56e5 | ||
|
|
b81a1787e2 | ||
|
|
4c594f0bf4 | ||
|
|
402e909e22 | ||
|
|
f05bb711d2 | ||
|
|
3331cfb796 | ||
|
|
1edf2b2c72 | ||
|
|
d53cce87dc | ||
|
|
fd52380c6c | ||
|
|
b5516d8707 | ||
|
|
f1186c8c58 | ||
|
|
678d5efe6f | ||
|
|
be2dabb115 | ||
|
|
12e3fcdfa0 | ||
|
|
64e1b68898 | ||
|
|
06a91c995c | ||
|
|
ec2127748c | ||
|
|
67729da47e | ||
|
|
62db0d4dbc | ||
|
|
0d20dbe2f4 | ||
|
|
dfa4d2d6aa | ||
|
|
d96692de7e | ||
|
|
c064b866fc | ||
|
|
b0dc5ca486 | ||
|
|
f076fcf94a | ||
|
|
3c57229d4e | ||
|
|
c59c3609fb | ||
|
|
09ca2c0a5a | ||
|
|
dfe3d0255e | ||
|
|
5264915130 | ||
|
|
e9cbbf11c7 | ||
|
|
33e701de2c | ||
|
|
db992c048e | ||
|
|
a0bccd02a1 | ||
|
|
8753ae4f0a | ||
|
|
fafb79fd53 | ||
|
|
990ba07cb0 | ||
|
|
5db5b67d5d | ||
|
|
b2cafb0dff | ||
|
|
ebb50ac6d2 | ||
|
|
b767ababf4 | ||
|
|
cb6e949ed7 | ||
|
|
77c01f30bc | ||
|
|
254e4770db | ||
|
|
f361a8ba51 | ||
|
|
933d2667dc | ||
|
|
5a29427ab9 | ||
|
|
87a0217d45 | ||
|
|
bc748ef40d | ||
|
|
b7c68b7c03 | ||
|
|
7da8661df6 | ||
|
|
9d2a05c8e4 | ||
|
|
2cc50e5d80 | ||
|
|
4fe69aea56 | ||
|
|
8a1d682f45 | ||
|
|
bdd84e51ba | ||
|
|
2140b8eaae | ||
|
|
3a468395b9 | ||
|
|
fc1594886c | ||
|
|
f997255ed1 | ||
|
|
6352ef30c0 | ||
|
|
38adf5b506 | ||
|
|
850190454f | ||
|
|
5619f22ed5 | ||
|
|
c384cf25fb | ||
|
|
44e05c971b | ||
|
|
29282fc825 | ||
|
|
6639f4172e | ||
|
|
64d053537f | ||
|
|
a1153981b9 | ||
|
|
7a3f57c8e3 | ||
|
|
2193cb88a9 | ||
|
|
2b34600365 | ||
|
|
72978b0c16 | ||
|
|
b6ec8e7867 | ||
|
|
2dc66498e7 | ||
|
|
cc0c1ae1d5 | ||
|
|
93a0f88916 | ||
|
|
30588eec0c | ||
|
|
c1f102119b | ||
|
|
0f9e9eb804 | ||
|
|
956c58b12c | ||
|
|
9d8a0a5c16 | ||
|
|
8e64a6ce9b | ||
|
|
1489092d4f | ||
|
|
42f4840c6d |
@@ -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",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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}`)
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
@@ -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)}/` : ""
|
||||
|
||||
@@ -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()))
|
||||
})
|
||||
|
||||
@@ -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)}/` : ""
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)}/` : ""
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
118
packages/generator/src/generators/template-builders/builder.ts
Normal file
118
packages/generator/src/generators/template-builders/builder.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import {IBuilder} from "./builder"
|
||||
|
||||
export const NullBuilder: IBuilder<any,any> = {
|
||||
// eslint-disable-next-line require-await
|
||||
getTemplateValues: async () => {
|
||||
return {}
|
||||
},
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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) {
|
||||
|
||||
101
packages/generator/src/utils/get-codegen.ts
Normal file
101
packages/generator/src/utils/get-codegen.ts
Normal 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
|
||||
}
|
||||
}
|
||||
20
packages/generator/src/utils/get-prisma-schema.ts
Normal file
20
packages/generator/src/utils/get-prisma-schema.ts
Normal 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}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
14
packages/generator/src/utils/model-names.ts
Normal file
14
packages/generator/src/utils/model-names.ts
Normal 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)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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__(),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
223
packages/generator/test/builders/field-values-builder.test.ts
Normal file
223
packages/generator/test/builders/field-values-builder.test.ts
Normal 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",
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
47
packages/generator/test/generators/form-generator.test.ts
Normal file
47
packages/generator/test/generators/form-generator.test.ts
Normal 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__(),`,
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -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: "",
|
||||
|
||||
@@ -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})
|
||||
})
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user