@cedarjs/cli 8.0.0-canary.3410 → 8.0.0-canary.3411
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/generate/dbAuth/templates/login.webAuthn.tsx.template +20 -12
- package/dist/commands/generate/helpers.js +26 -0
- package/dist/commands/generate/ogImage/templates/ogImage.og.tsx.template +1 -1
- package/dist/commands/generate/package/templates/tsconfig.json.template +1 -0
- package/dist/commands/generate/realtime/templates/liveQueries/blank/blank.service.ts.template +12 -4
- package/dist/commands/generate/realtime/templates/subscriptions/blank/blank.service.ts.template +2 -2
- package/dist/commands/generate/realtime/templates/subscriptions/blank/blank.ts.template +3 -3
- package/dist/commands/generate/scaffold/templates/components/EditNameCell.tsx.template +5 -7
- package/dist/commands/generate/scaffold/templates/components/NameForm.tsx.template +7 -6
- package/dist/commands/generate/scaffold/templates/lib/formatters.tsx.template +3 -3
- package/dist/commands/generate/script/templates/tsconfig.json.template +1 -0
- package/dist/commands/generate/sdl/sdlHandler.js +8 -3
- package/dist/commands/generate/service/serviceHandler.js +32 -11
- package/dist/commands/generate/service/templates/service.ts.template +21 -8
- package/dist/commands/generate/service/templates/test.ts.template +7 -5
- package/package.json +13 -13
|
@@ -17,7 +17,7 @@ import { useAuth } from 'src/auth'
|
|
|
17
17
|
const WELCOME_MESSAGE = 'Welcome back!'
|
|
18
18
|
const REDIRECT = routes.home()
|
|
19
19
|
|
|
20
|
-
const LoginPage = ({ type }) => {
|
|
20
|
+
const LoginPage = ({ type }: { type?: string }) => {
|
|
21
21
|
const {
|
|
22
22
|
isAuthenticated,
|
|
23
23
|
client: webAuthn,
|
|
@@ -27,12 +27,12 @@ const LoginPage = ({ type }) => {
|
|
|
27
27
|
} = useAuth()
|
|
28
28
|
const [shouldShowWebAuthn, setShouldShowWebAuthn] = useState(false)
|
|
29
29
|
const [showWebAuthn, setShowWebAuthn] = useState(
|
|
30
|
-
webAuthn
|
|
30
|
+
!!webAuthn?.isEnabled() && type !== 'password'
|
|
31
31
|
)
|
|
32
32
|
|
|
33
33
|
// should redirect right after login or wait to show the webAuthn prompts?
|
|
34
34
|
useEffect(() => {
|
|
35
|
-
if (isAuthenticated && (!shouldShowWebAuthn || webAuthn
|
|
35
|
+
if (isAuthenticated && (!shouldShowWebAuthn || webAuthn?.isEnabled())) {
|
|
36
36
|
navigate(REDIRECT)
|
|
37
37
|
}
|
|
38
38
|
}, [isAuthenticated, shouldShowWebAuthn])
|
|
@@ -45,13 +45,13 @@ const LoginPage = ({ type }) => {
|
|
|
45
45
|
}, [loading, isAuthenticated])
|
|
46
46
|
|
|
47
47
|
// focus on the ${usernameLowerCase} field as soon as the page loads
|
|
48
|
-
const ${usernameCamelCase}Ref = useRef()
|
|
48
|
+
const ${usernameCamelCase}Ref = useRef<HTMLInputElement>(null)
|
|
49
49
|
useEffect(() => {
|
|
50
|
-
${usernameCamelCase}Ref.current
|
|
50
|
+
${usernameCamelCase}Ref.current?.focus()
|
|
51
51
|
}, [])
|
|
52
52
|
|
|
53
|
-
const onSubmit = async (data) => {
|
|
54
|
-
const webAuthnSupported = await webAuthn
|
|
53
|
+
const onSubmit = async (data: Record<string, string>) => {
|
|
54
|
+
const webAuthnSupported = !!(await webAuthn?.isSupported())
|
|
55
55
|
|
|
56
56
|
if (webAuthnSupported) {
|
|
57
57
|
setShouldShowWebAuthn(true)
|
|
@@ -75,30 +75,38 @@ const LoginPage = ({ type }) => {
|
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
const onAuthenticate = async () => {
|
|
78
|
+
if (!webAuthn) {
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
|
|
78
82
|
try {
|
|
79
83
|
await webAuthn.authenticate()
|
|
80
84
|
await reauthenticate()
|
|
81
85
|
toast.success(WELCOME_MESSAGE)
|
|
82
86
|
navigate(REDIRECT)
|
|
83
87
|
} catch (e) {
|
|
84
|
-
if (e.name === 'WebAuthnDeviceNotFoundError') {
|
|
88
|
+
if (e instanceof Error && e.name === 'WebAuthnDeviceNotFoundError') {
|
|
85
89
|
toast.error(
|
|
86
90
|
'Device not found, log in with ${usernameTitleCase}/${passwordTitleCase} to continue'
|
|
87
91
|
)
|
|
88
92
|
setShowWebAuthn(false)
|
|
89
93
|
} else {
|
|
90
|
-
toast.error(e.message)
|
|
94
|
+
toast.error(e instanceof Error ? e.message : String(e))
|
|
91
95
|
}
|
|
92
96
|
}
|
|
93
97
|
}
|
|
94
98
|
|
|
95
99
|
const onRegister = async () => {
|
|
100
|
+
if (!webAuthn) {
|
|
101
|
+
return
|
|
102
|
+
}
|
|
103
|
+
|
|
96
104
|
try {
|
|
97
105
|
await webAuthn.register()
|
|
98
106
|
toast.success(WELCOME_MESSAGE)
|
|
99
107
|
navigate(REDIRECT)
|
|
100
108
|
} catch (e) {
|
|
101
|
-
toast.error(e.message)
|
|
109
|
+
toast.error(e instanceof Error ? e.message : String(e))
|
|
102
110
|
}
|
|
103
111
|
}
|
|
104
112
|
|
|
@@ -200,7 +208,7 @@ const LoginPage = ({ type }) => {
|
|
|
200
208
|
|
|
201
209
|
const formToRender = () => {
|
|
202
210
|
if (showWebAuthn) {
|
|
203
|
-
if (webAuthn
|
|
211
|
+
if (webAuthn?.isEnabled()) {
|
|
204
212
|
return <AuthWebAuthnPrompt />
|
|
205
213
|
} else {
|
|
206
214
|
return <RegisterWebAuthnPrompt />
|
|
@@ -212,7 +220,7 @@ const LoginPage = ({ type }) => {
|
|
|
212
220
|
|
|
213
221
|
const linkToRender = () => {
|
|
214
222
|
if (showWebAuthn) {
|
|
215
|
-
if (webAuthn
|
|
223
|
+
if (webAuthn?.isEnabled()) {
|
|
216
224
|
return (
|
|
217
225
|
<div className="rw-login-link">
|
|
218
226
|
<span>or login with </span>{' '}
|
|
@@ -37,11 +37,33 @@ const redactedModelFields = (fieldNames) => {
|
|
|
37
37
|
}
|
|
38
38
|
return sensitive;
|
|
39
39
|
};
|
|
40
|
+
const DEFAULT_IGNORE_FIELDS_FOR_INPUT = ["createdAt", "updatedAt"];
|
|
41
|
+
const requiredFieldsInUpdateInput = (model) => {
|
|
42
|
+
return inputFields(model).filter((field) => {
|
|
43
|
+
return field.isRequired && !field.isList && !field.isId;
|
|
44
|
+
}).map((field) => field.name);
|
|
45
|
+
};
|
|
46
|
+
const nullableJsonFieldsInInput = (model) => {
|
|
47
|
+
return inputFields(model).filter((field) => {
|
|
48
|
+
return field.type === "Json" && !field.isRequired && !field.isList;
|
|
49
|
+
}).map((field) => field.name);
|
|
50
|
+
};
|
|
51
|
+
const inputFields = (model) => {
|
|
52
|
+
const redactedFields = redactedModelFields(
|
|
53
|
+
model.fields.map((field) => field.name)
|
|
54
|
+
);
|
|
55
|
+
return model.fields.filter((field) => {
|
|
56
|
+
return field.kind !== "object" && !DEFAULT_IGNORE_FIELDS_FOR_INPUT.includes(field.name) && !redactedFields.includes(field.name);
|
|
57
|
+
});
|
|
58
|
+
};
|
|
40
59
|
const relationsForModel = (model) => {
|
|
41
60
|
return model?.fields.filter((f) => f.relationName).map((field) => {
|
|
42
61
|
return field.name;
|
|
43
62
|
});
|
|
44
63
|
};
|
|
64
|
+
const nullableRelationsForModel = (model) => {
|
|
65
|
+
return model.fields.filter((f) => f.kind === "object" && !f.isList && !f.isRequired).map((f) => f.name);
|
|
66
|
+
};
|
|
45
67
|
const intForeignKeysForModel = (model) => {
|
|
46
68
|
return model.fields.filter((f) => f.name.match(/Id$/) && f.type === "Int").map((f) => f.name);
|
|
47
69
|
};
|
|
@@ -74,14 +96,18 @@ const mapPrismaScalarToPagePropTsType = (scalarType) => {
|
|
|
74
96
|
return prismaScalarToTsType[scalarType] || "unknown";
|
|
75
97
|
};
|
|
76
98
|
export {
|
|
99
|
+
DEFAULT_IGNORE_FIELDS_FOR_INPUT,
|
|
77
100
|
SENSITIVE_FIELDS,
|
|
78
101
|
forcePluralizeWord,
|
|
79
102
|
intForeignKeysForModel,
|
|
80
103
|
mapPrismaScalarToPagePropTsType,
|
|
81
104
|
mapRouteParamTypeToTsType,
|
|
105
|
+
nullableJsonFieldsInInput,
|
|
106
|
+
nullableRelationsForModel,
|
|
82
107
|
pathName,
|
|
83
108
|
redactedModelFields,
|
|
84
109
|
relationsForModel,
|
|
85
110
|
removeGeneratorName,
|
|
111
|
+
requiredFieldsInUpdateInput,
|
|
86
112
|
validateName
|
|
87
113
|
};
|
package/dist/commands/generate/realtime/templates/liveQueries/blank/blank.service.ts.template
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
// api/src/services/${name}s/${name}s.ts
|
|
2
|
+
import type { ${typeName}ItemInput } from 'types/graphql'
|
|
3
|
+
|
|
2
4
|
import type { LiveQueryStorageMechanism } from '@cedarjs/realtime'
|
|
3
5
|
|
|
4
6
|
import { logger } from 'src/lib/logger'
|
|
5
7
|
|
|
6
|
-
|
|
8
|
+
interface ${typeName}Data {
|
|
9
|
+
id: string
|
|
10
|
+
title: string
|
|
11
|
+
items: { amount: number }[]
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const ${collectionName}: ${typeName}Data[] = [
|
|
7
15
|
{ id: '1', title: 'First ${modelName}', items: [{ amount: 11 }] },
|
|
8
16
|
{ id: '2', title: '2nd ${modelName}', items: [{ amount: 22 }] },
|
|
9
17
|
{ id: '3', title: 'Third ${modelName}', items: [{ amount: 33 }] },
|
|
@@ -35,14 +43,14 @@ const ${collectionName} = [
|
|
|
35
43
|
* }
|
|
36
44
|
* }
|
|
37
45
|
*/
|
|
38
|
-
export const ${liveQueryName} = async ({ id }) => {
|
|
46
|
+
export const ${liveQueryName} = async ({ id }: { id: string }) => {
|
|
39
47
|
const found${modelName} = ${collectionName}.find((a) => a.id === id)
|
|
40
48
|
logger.debug({ id, ${name}: found${modelName} }, `${name} details`)
|
|
41
49
|
return found${modelName}
|
|
42
50
|
}
|
|
43
51
|
|
|
44
52
|
export const create${typeName}Item = async (
|
|
45
|
-
{ input },
|
|
53
|
+
{ input }: { input: ${typeName}ItemInput },
|
|
46
54
|
{ context }: { context: { liveQueryStore: LiveQueryStorageMechanism } }
|
|
47
55
|
) => {
|
|
48
56
|
const { ${camelName}Id, amount } = input
|
|
@@ -63,7 +71,7 @@ export const create${typeName}Item = async (
|
|
|
63
71
|
}
|
|
64
72
|
|
|
65
73
|
export const ${modelName} = {
|
|
66
|
-
largest${typeName}Item: (obj, { root }) => {
|
|
74
|
+
largest${typeName}Item: (obj: unknown, { root }: { root: ${typeName}Data }) => {
|
|
67
75
|
const [largest] = root.items.sort((a, b) => b.amount - a.amount)
|
|
68
76
|
|
|
69
77
|
logger.debug({ obj, root }, 'largest ${typeName}Item')
|
package/dist/commands/generate/realtime/templates/subscriptions/blank/blank.service.ts.template
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { ${subscriptionInputType} } from 'types/graphql'
|
|
2
2
|
|
|
3
3
|
import { logger } from 'src/lib/logger'
|
|
4
|
-
import type { Publish${typeName}ChannelType } from 'src/subscriptions/${name}/${name}.
|
|
4
|
+
import type { Publish${typeName}ChannelType } from 'src/subscriptions/${name}/${name}.js'
|
|
5
5
|
|
|
6
|
-
export const ${subscriptionQueryName} = ({ id }) => [id]
|
|
6
|
+
export const ${subscriptionQueryName} = ({ id }: { id: string }) => [id]
|
|
7
7
|
|
|
8
8
|
export const ${subscriptionServiceResolver} = async (
|
|
9
9
|
{ input }: { input: ${subscriptionInputType} },
|
|
@@ -39,15 +39,15 @@ export type Publish${typeName}ChannelType = PubSub<Publish${typeName}Channel>
|
|
|
39
39
|
const ${subscriptionName}Subscription = {
|
|
40
40
|
${subscriptionName}: {
|
|
41
41
|
subscribe: (
|
|
42
|
-
_,
|
|
43
|
-
{ id },
|
|
42
|
+
_: unknown,
|
|
43
|
+
{ id }: { id: string },
|
|
44
44
|
{ pubSub }: { pubSub: Publish${typeName}ChannelType }
|
|
45
45
|
) => {
|
|
46
46
|
logger.debug({ id }, '${name} subscription')
|
|
47
47
|
|
|
48
48
|
return pubSub.subscribe('${typeName}', id)
|
|
49
49
|
},
|
|
50
|
-
resolve: (payload) => {
|
|
50
|
+
resolve: (payload: Publish${typeName}Channel['${typeName}'][1]) => {
|
|
51
51
|
logger.debug({ payload }, '${name} subscription resolve')
|
|
52
52
|
|
|
53
53
|
return payload
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
${useClientDirective}import type {
|
|
2
2
|
Edit${singularPascalName}By${pascalIdName},
|
|
3
3
|
Update${singularPascalName}Input,
|
|
4
|
+
Update${singularPascalName}Mutation,
|
|
4
5
|
Update${singularPascalName}MutationVariables
|
|
5
6
|
} from 'types/graphql'
|
|
6
7
|
|
|
@@ -24,7 +25,7 @@ export const QUERY: TypedDocumentNode<Edit${singularPascalName}By${pascalIdName}
|
|
|
24
25
|
`
|
|
25
26
|
|
|
26
27
|
const UPDATE_${singularConstantName}_MUTATION: TypedDocumentNode<
|
|
27
|
-
|
|
28
|
+
Update${singularPascalName}Mutation,
|
|
28
29
|
Update${singularPascalName}MutationVariables
|
|
29
30
|
> = gql`
|
|
30
31
|
mutation Update${singularPascalName}Mutation($${idName}: ${idType}!, $input: Update${singularPascalName}Input!) {
|
|
@@ -54,17 +55,14 @@ export const Success = ({ ${singularCamelName} }: CellSuccessProps<Edit${singula
|
|
|
54
55
|
}
|
|
55
56
|
)
|
|
56
57
|
|
|
57
|
-
const onSave = (
|
|
58
|
-
|
|
59
|
-
id: Edit${singularPascalName}By${pascalIdName}['${singularCamelName}']['id']
|
|
60
|
-
) => {
|
|
61
|
-
update${singularPascalName}({ variables: { id, input } })
|
|
58
|
+
const onSave = (input: Update${singularPascalName}Input) => {
|
|
59
|
+
update${singularPascalName}({ variables: { ${idName}: ${singularCamelName}.${idName}, input } })
|
|
62
60
|
}
|
|
63
61
|
|
|
64
62
|
return (
|
|
65
63
|
<div className="rw-segment">
|
|
66
64
|
<header className="rw-segment-header">
|
|
67
|
-
<h2 className="rw-heading rw-heading-secondary">Edit ${singularPascalName} {${singularCamelName}
|
|
65
|
+
<h2 className="rw-heading rw-heading-secondary">Edit ${singularPascalName} {${singularCamelName}.${idName}}</h2>
|
|
68
66
|
</header>
|
|
69
67
|
<div className="rw-segment-main">
|
|
70
68
|
<${singularPascalName}Form ${singularCamelName}={${singularCamelName}} onSave={onSave} error={error} loading={loading} />
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Edit${singularPascalName}By${pascalIdName},
|
|
1
|
+
import type { Edit${singularPascalName}By${pascalIdName}, Create${singularPascalName}Input } from 'types/graphql'
|
|
2
2
|
|
|
3
3
|
import type { RWGqlError } from '@cedarjs/forms'
|
|
4
4
|
import {
|
|
@@ -48,8 +48,8 @@ type Form${singularPascalName} = NonNullable<Edit${singularPascalName}By${pascal
|
|
|
48
48
|
|
|
49
49
|
interface ${singularPascalName}FormProps {
|
|
50
50
|
${singularCamelName}?: Edit${singularPascalName}By${pascalIdName}['${singularCamelName}']
|
|
51
|
-
onSave: (data:
|
|
52
|
-
error
|
|
51
|
+
onSave: (data: Create${singularPascalName}Input) => void
|
|
52
|
+
error?: RWGqlError
|
|
53
53
|
loading: boolean
|
|
54
54
|
}
|
|
55
55
|
|
|
@@ -70,7 +70,8 @@ const ${singularPascalName}Form = (props: ${singularPascalName}FormProps) => {
|
|
|
70
70
|
}
|
|
71
71
|
<% } %>
|
|
72
72
|
<% if (column.isEnum && !column.isList && !column.isRequired) { %>
|
|
73
|
-
|
|
73
|
+
// The "None" option has an empty value
|
|
74
|
+
if (!data.${column.name}) {
|
|
74
75
|
data.${column.name} = null
|
|
75
76
|
}
|
|
76
77
|
<% } %>
|
|
@@ -80,7 +81,7 @@ const ${singularPascalName}Form = (props: ${singularPascalName}FormProps) => {
|
|
|
80
81
|
}
|
|
81
82
|
<% } %>
|
|
82
83
|
<% }) %>
|
|
83
|
-
props.onSave(data
|
|
84
|
+
props.onSave(data)
|
|
84
85
|
}
|
|
85
86
|
|
|
86
87
|
return (
|
|
@@ -136,7 +137,7 @@ const ${singularPascalName}Form = (props: ${singularPascalName}FormProps) => {
|
|
|
136
137
|
<% } else { %>
|
|
137
138
|
<${column.component}
|
|
138
139
|
name="${column.name}"
|
|
139
|
-
${column.defaultProp}={${column.deserializeFunction && column.deserializeFunction + '('}props.${singularCamelName}?.<%= column.name %>${column.deserializeFunction && ')'}}
|
|
140
|
+
${column.defaultProp}={${column.deserializeFunction && column.deserializeFunction + '('}props.${singularCamelName}?.<%= column.name %>${column.deserializeFunction && ')'}<%= !column.deserializeFunction && !column.isRequired ? ' ?? undefined' : '' %>}
|
|
140
141
|
className="rw-input"
|
|
141
142
|
errorClassName="rw-input rw-input-error"
|
|
142
143
|
<%= !column.validation ? '' : `validation=${column.validation}` %>
|
|
@@ -25,7 +25,7 @@ export const jsonDisplay = (obj: unknown) => {
|
|
|
25
25
|
)
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
export const truncate = (value: string | number) => {
|
|
28
|
+
export const truncate = (value: string | number | null | undefined) => {
|
|
29
29
|
let output = value?.toString() ?? ''
|
|
30
30
|
|
|
31
31
|
if (output.length > MAX_STRING_LENGTH) {
|
|
@@ -65,6 +65,6 @@ export function timeTag(dateTime: <%= parsedDateTime ? 'Date' : 'string' %> | un
|
|
|
65
65
|
return output
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
export const checkboxInputTag = (checked: boolean) => {
|
|
69
|
-
return <input type="checkbox" checked={checked} disabled />
|
|
68
|
+
export const checkboxInputTag = (checked: boolean | null | undefined) => {
|
|
69
|
+
return <input type="checkbox" checked={!!checked} disabled />
|
|
70
70
|
}
|
|
@@ -19,7 +19,11 @@ import {
|
|
|
19
19
|
getEnum,
|
|
20
20
|
verifyModelName
|
|
21
21
|
} from "../../../lib/schemaHelpers.js";
|
|
22
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
DEFAULT_IGNORE_FIELDS_FOR_INPUT,
|
|
24
|
+
redactedModelFields,
|
|
25
|
+
relationsForModel
|
|
26
|
+
} from "../helpers.js";
|
|
23
27
|
import { files as serviceFiles } from "../service/serviceHandler.js";
|
|
24
28
|
import { templateForFile } from "../yargsHandlerHelpers.js";
|
|
25
29
|
import {
|
|
@@ -27,7 +31,6 @@ import {
|
|
|
27
31
|
missingRelatedModels,
|
|
28
32
|
writeFilesWithStubsTask
|
|
29
33
|
} from "./stubFiles.js";
|
|
30
|
-
const DEFAULT_IGNORE_FIELDS_FOR_INPUT = ["createdAt", "updatedAt"];
|
|
31
34
|
const missingIdConsoleMessage = () => {
|
|
32
35
|
const line1 = ansis.bold.yellow("WARNING") + ": Cannot generate CRUD SDL without an `@id` database column.";
|
|
33
36
|
const line2 = "If you are trying to generate for a many-to-many join table ";
|
|
@@ -61,7 +64,8 @@ const modelFieldToSDL = ({
|
|
|
61
64
|
Bytes: "Byte"
|
|
62
65
|
};
|
|
63
66
|
const gqlType = prismaTypeToGraphqlType[field.type] || field.type;
|
|
64
|
-
const
|
|
67
|
+
const listItemType = field.kind === "object" ? gqlType : `${gqlType}!`;
|
|
68
|
+
const type = field.isList ? `[${listItemType}]` : gqlType;
|
|
65
69
|
const isRequired = field.isRequired && required || field.isList || field.isId;
|
|
66
70
|
const fieldContent = `${field.name}: ${type}${isRequired ? "!" : ""}`;
|
|
67
71
|
if (docs) {
|
|
@@ -332,6 +336,7 @@ const handler = async ({
|
|
|
332
336
|
export {
|
|
333
337
|
files,
|
|
334
338
|
handler,
|
|
339
|
+
modelFieldToSDL,
|
|
335
340
|
printRedactedFieldsNote,
|
|
336
341
|
redactedSensitiveFields,
|
|
337
342
|
stubFiles
|
|
@@ -3,7 +3,14 @@ import camelcase from "camelcase";
|
|
|
3
3
|
import { pluralize, singularize } from "@cedarjs/utils/cedarPluralize";
|
|
4
4
|
import { transformTSToJS } from "../../../lib/index.js";
|
|
5
5
|
import { getSchema, verifyModelName } from "../../../lib/schemaHelpers.js";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
DEFAULT_IGNORE_FIELDS_FOR_INPUT,
|
|
8
|
+
nullableJsonFieldsInInput,
|
|
9
|
+
nullableRelationsForModel,
|
|
10
|
+
redactedModelFields,
|
|
11
|
+
relationsForModel,
|
|
12
|
+
requiredFieldsInUpdateInput
|
|
13
|
+
} from "../helpers.js";
|
|
7
14
|
import { createHandler, templateForFile } from "../yargsHandlerHelpers.js";
|
|
8
15
|
function isServiceModel(schema) {
|
|
9
16
|
return typeof schema === "object" && schema !== null && "fields" in schema && "name" in schema;
|
|
@@ -59,11 +66,13 @@ function scenarioFieldValue(field) {
|
|
|
59
66
|
return randInt;
|
|
60
67
|
case "Json":
|
|
61
68
|
return { foo: "bar" };
|
|
62
|
-
case "String":
|
|
69
|
+
case "String": {
|
|
70
|
+
const isUnique = field.isUnique || field.isId;
|
|
63
71
|
if (field.name?.toLowerCase().includes("email")) {
|
|
64
|
-
return
|
|
72
|
+
return isUnique ? `foo${randInt}@bar.com` : "foo@bar.com";
|
|
65
73
|
}
|
|
66
|
-
return
|
|
74
|
+
return isUnique ? `String${randInt}` : "String";
|
|
75
|
+
}
|
|
67
76
|
case "Bytes":
|
|
68
77
|
return `new Uint8Array([${randIntArray}])`;
|
|
69
78
|
default: {
|
|
@@ -152,11 +161,14 @@ const fieldsToInput = async (model) => {
|
|
|
152
161
|
const modelName = camelcase(singularize(model));
|
|
153
162
|
const inputObj = {};
|
|
154
163
|
if (scalarFields.some(
|
|
155
|
-
(field) => redactedFields.includes(field.name)
|
|
164
|
+
(field) => redactedFields.includes(field.name) || DEFAULT_IGNORE_FIELDS_FOR_INPUT.includes(field.name) && !field.isUpdatedAt
|
|
156
165
|
)) {
|
|
157
166
|
return false;
|
|
158
167
|
}
|
|
159
168
|
scalarFields.forEach((field) => {
|
|
169
|
+
if (DEFAULT_IGNORE_FIELDS_FOR_INPUT.includes(field.name)) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
160
172
|
if (foreignKeys.includes(field.name)) {
|
|
161
173
|
inputObj[field.name] = `scenario.${modelName}.two.${field.name}`;
|
|
162
174
|
} else {
|
|
@@ -174,7 +186,7 @@ const fieldsToUpdate = async (model) => {
|
|
|
174
186
|
const modelName = camelcase(singularize(model));
|
|
175
187
|
let field, newValue, fieldName;
|
|
176
188
|
const updatableFields = scalarFields.filter(
|
|
177
|
-
(scalar) => !redactedFields.includes(scalar.name)
|
|
189
|
+
(scalar) => !redactedFields.includes(scalar.name) && !DEFAULT_IGNORE_FIELDS_FOR_INPUT.includes(scalar.name)
|
|
178
190
|
);
|
|
179
191
|
field = updatableFields.find(
|
|
180
192
|
(scalar) => !foreignKeys.includes(scalar.name)
|
|
@@ -273,13 +285,19 @@ const files = async ({
|
|
|
273
285
|
}
|
|
274
286
|
const prismaImportSource = "src/lib/db";
|
|
275
287
|
let modelRelations;
|
|
288
|
+
let updateInputRequiredFields = [];
|
|
289
|
+
let nullableJsonFields = [];
|
|
290
|
+
let nullableRelations = [];
|
|
291
|
+
const schemaResult = await getSchema(model);
|
|
292
|
+
if (isServiceModel(schemaResult)) {
|
|
293
|
+
updateInputRequiredFields = requiredFieldsInUpdateInput(schemaResult);
|
|
294
|
+
nullableJsonFields = nullableJsonFieldsInInput(schemaResult);
|
|
295
|
+
nullableRelations = nullableRelationsForModel(schemaResult);
|
|
296
|
+
}
|
|
276
297
|
if (relations) {
|
|
277
298
|
modelRelations = relations;
|
|
278
|
-
} else {
|
|
279
|
-
|
|
280
|
-
if (isServiceModel(schemaResult)) {
|
|
281
|
-
modelRelations = relationsForModel(schemaResult);
|
|
282
|
-
}
|
|
299
|
+
} else if (isServiceModel(schemaResult)) {
|
|
300
|
+
modelRelations = relationsForModel(schemaResult);
|
|
283
301
|
}
|
|
284
302
|
const serviceFile = await templateForFile({
|
|
285
303
|
name,
|
|
@@ -292,6 +310,9 @@ const files = async ({
|
|
|
292
310
|
relations: modelRelations,
|
|
293
311
|
idName,
|
|
294
312
|
prismaImportSource,
|
|
313
|
+
updateInputRequiredFields,
|
|
314
|
+
nullableJsonFields,
|
|
315
|
+
nullableRelations,
|
|
295
316
|
...rest
|
|
296
317
|
}
|
|
297
318
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { QueryResolvers<% if (crud) { %>, MutationResolvers<% } %><% if (relations.length) { %>, ${singularPascalName}RelationResolvers<% } %> } from 'types/graphql'
|
|
2
2
|
|
|
3
|
-
import { db } from 'src/lib/db'
|
|
3
|
+
import { db<% if (crud && nullableJsonFields.length) { %>, Prisma<% } %> } from 'src/lib/db'
|
|
4
4
|
<%
|
|
5
5
|
// A compound `@@id([...])` key comes through as `{ fields, whereArg }`
|
|
6
6
|
// instead of a single field name -- build the destructure/where-clause
|
|
@@ -13,8 +13,8 @@ import { db } from 'src/lib/db'
|
|
|
13
13
|
? `${idName.whereArg}: { ${idFields.join(', ')} }`
|
|
14
14
|
: idName
|
|
15
15
|
const idWhereFromRoot = idIsComposite
|
|
16
|
-
? `${idName.whereArg}: { ${idFields.map((f) => `${f}: root
|
|
17
|
-
: `${idName}: root
|
|
16
|
+
? `${idName.whereArg}: { ${idFields.map((f) => `${f}: root.${f}`).join(', ')} }`
|
|
17
|
+
: `${idName}: root.${idName}`
|
|
18
18
|
%>
|
|
19
19
|
export const ${pluralCamelName}: QueryResolvers['${pluralCamelName}'] = () => {
|
|
20
20
|
return db.${singularCamelName}.findMany()
|
|
@@ -27,14 +27,27 @@ export const ${singularCamelName}: QueryResolvers['${singularCamelName}'] = ({ $
|
|
|
27
27
|
}<% } %><% if (crud) { %>
|
|
28
28
|
|
|
29
29
|
export const create${singularPascalName}: MutationResolvers['create${singularPascalName}'] = ({ input }) => {
|
|
30
|
-
return db.${singularCamelName}.create({
|
|
31
|
-
data:
|
|
30
|
+
return db.${singularCamelName}.create({<% if (nullableJsonFields.length) { %>
|
|
31
|
+
data: {
|
|
32
|
+
...input,
|
|
33
|
+
// Prisma uses Prisma.DbNull, not null, to set a Json field to NULL<% nullableJsonFields.forEach((field) => { %>
|
|
34
|
+
${field}: input.${field} === null ? Prisma.DbNull : input.${field},<% }) %>
|
|
35
|
+
},<% } else { %>
|
|
36
|
+
data: input,<% } %>
|
|
32
37
|
})
|
|
33
38
|
}
|
|
34
39
|
|
|
35
40
|
export const update${singularPascalName}: MutationResolvers['update${singularPascalName}'] = ({ ${idArgs}, input }) => {
|
|
36
|
-
return db.${singularCamelName}.update({
|
|
37
|
-
data:
|
|
41
|
+
return db.${singularCamelName}.update({<% if (updateInputRequiredFields.length || nullableJsonFields.length) { %>
|
|
42
|
+
data: {
|
|
43
|
+
...input,<% if (updateInputRequiredFields.length) { %>
|
|
44
|
+
// Every field in the GraphQL input is nullable, but Prisma doesn't
|
|
45
|
+
// accept null for required fields. undefined leaves the field unchanged<% updateInputRequiredFields.forEach((field) => { %>
|
|
46
|
+
${field}: input.${field} ?? undefined,<% }) %><% } %><% if (nullableJsonFields.length) { %>
|
|
47
|
+
// Prisma uses Prisma.DbNull, not null, to set a Json field to NULL<% nullableJsonFields.forEach((field) => { %>
|
|
48
|
+
${field}: input.${field} === null ? Prisma.DbNull : input.${field},<% }) %><% } %>
|
|
49
|
+
},<% } else { %>
|
|
50
|
+
data: input,<% } %>
|
|
38
51
|
where: { ${idWhere} },
|
|
39
52
|
})
|
|
40
53
|
}
|
|
@@ -47,6 +60,6 @@ export const delete${singularPascalName}: MutationResolvers['delete${singularPas
|
|
|
47
60
|
|
|
48
61
|
export const ${singularPascalName}: ${singularPascalName}RelationResolvers = {<% relations.forEach(relation => { %>
|
|
49
62
|
${relation}: (_obj, { root }) => {
|
|
50
|
-
return db.${singularCamelName}.findUnique({ where: { ${idWhereFromRoot} } }).${relation}()
|
|
63
|
+
return db.${singularCamelName}.findUnique<%= nullableRelations.includes(relation) ? '' : 'OrThrow' %>({ where: { ${idWhereFromRoot} } }).${relation}()
|
|
51
64
|
},<% }) %>
|
|
52
65
|
}<% } %>
|
|
@@ -7,8 +7,10 @@
|
|
|
7
7
|
// Looks for quoted strings, either by single (') or double (") quotes.
|
|
8
8
|
// When found
|
|
9
9
|
// - Removes the quotes around `scenario` variables.
|
|
10
|
-
// - Removes the quotes around `BigInt` fields.
|
|
11
|
-
|
|
10
|
+
// - Removes the quotes around `BigInt` fields. GraphQL inputs type BigInt
|
|
11
|
+
// values as numbers, so the `n` suffix is also removed when `asInput` is
|
|
12
|
+
// set
|
|
13
|
+
const transformValue = (obj, type, asInput = false) => {
|
|
12
14
|
if (type === 'DateTime') {
|
|
13
15
|
return `new Date('${obj.toISOString()}')`
|
|
14
16
|
} else if (type === 'Decimal') {
|
|
@@ -22,7 +24,7 @@
|
|
|
22
24
|
|
|
23
25
|
// BigInt
|
|
24
26
|
if (string.match(/^\"\d+n\"$/)) {
|
|
25
|
-
return string.slice(1, string.length - 1)
|
|
27
|
+
return string.slice(1, string.length - (asInput ? 2 : 1))
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
return string
|
|
@@ -57,7 +59,7 @@ describe('${pluralCamelName}', () => {
|
|
|
57
59
|
|
|
58
60
|
<% if (create) { %>scenario('creates a ${singularCamelName}', async (${transformValue(create).includes('scenario.') ? 'scenario: StandardScenario' : ''}) => {
|
|
59
61
|
const result = await create${singularPascalName}({
|
|
60
|
-
input: ${transformValue(create)},
|
|
62
|
+
input: ${transformValue(create, undefined, true)},
|
|
61
63
|
})
|
|
62
64
|
|
|
63
65
|
<% for (const [name, value] of Object.entries(create)) { %>
|
|
@@ -68,7 +70,7 @@ describe('${pluralCamelName}', () => {
|
|
|
68
70
|
const original = await (${singularCamelName}({ ${idName}: scenario.${singularCamelName}.one.${idName} })) as ${prismaModel}
|
|
69
71
|
const result = await update${singularPascalName}({
|
|
70
72
|
${idName}: original.${idName},
|
|
71
|
-
input: ${transformValue(update)},
|
|
73
|
+
input: ${transformValue(update, undefined, true)},
|
|
72
74
|
})
|
|
73
75
|
|
|
74
76
|
<% for (const [name, value] of Object.entries(update)) { %>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cedarjs/cli",
|
|
3
|
-
"version": "8.0.0-canary.
|
|
3
|
+
"version": "8.0.0-canary.3411",
|
|
4
4
|
"description": "The CedarJS Command Line",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -36,17 +36,17 @@
|
|
|
36
36
|
"@babel/preset-typescript": "7.29.7",
|
|
37
37
|
"@babel/traverse": "7.29.10",
|
|
38
38
|
"@babel/types": "7.29.8",
|
|
39
|
-
"@cedarjs/api-server": "8.0.0-canary.
|
|
40
|
-
"@cedarjs/babel-config": "8.0.0-canary.
|
|
41
|
-
"@cedarjs/cli-helpers": "8.0.0-canary.
|
|
42
|
-
"@cedarjs/internal": "8.0.0-canary.
|
|
43
|
-
"@cedarjs/prerender": "8.0.0-canary.
|
|
44
|
-
"@cedarjs/project-config": "8.0.0-canary.
|
|
45
|
-
"@cedarjs/structure": "8.0.0-canary.
|
|
46
|
-
"@cedarjs/telemetry": "8.0.0-canary.
|
|
47
|
-
"@cedarjs/utils": "8.0.0-canary.
|
|
48
|
-
"@cedarjs/vite": "8.0.0-canary.
|
|
49
|
-
"@cedarjs/web-server": "8.0.0-canary.
|
|
39
|
+
"@cedarjs/api-server": "8.0.0-canary.3411",
|
|
40
|
+
"@cedarjs/babel-config": "8.0.0-canary.3411",
|
|
41
|
+
"@cedarjs/cli-helpers": "8.0.0-canary.3411",
|
|
42
|
+
"@cedarjs/internal": "8.0.0-canary.3411",
|
|
43
|
+
"@cedarjs/prerender": "8.0.0-canary.3411",
|
|
44
|
+
"@cedarjs/project-config": "8.0.0-canary.3411",
|
|
45
|
+
"@cedarjs/structure": "8.0.0-canary.3411",
|
|
46
|
+
"@cedarjs/telemetry": "8.0.0-canary.3411",
|
|
47
|
+
"@cedarjs/utils": "8.0.0-canary.3411",
|
|
48
|
+
"@cedarjs/vite": "8.0.0-canary.3411",
|
|
49
|
+
"@cedarjs/web-server": "8.0.0-canary.3411",
|
|
50
50
|
"@listr2/prompt-adapter-enquirer": "4.3.3",
|
|
51
51
|
"@opentelemetry/api": "1.9.1",
|
|
52
52
|
"@opentelemetry/core": "2.12.0",
|
|
@@ -94,7 +94,7 @@
|
|
|
94
94
|
"yargs": "17.7.3"
|
|
95
95
|
},
|
|
96
96
|
"devDependencies": {
|
|
97
|
-
"@cedarjs/framework-tools": "8.0.0-canary.
|
|
97
|
+
"@cedarjs/framework-tools": "8.0.0-canary.3411",
|
|
98
98
|
"@prisma/dmmf": "7.10.0",
|
|
99
99
|
"@types/archiver": "^7.0.0",
|
|
100
100
|
"memfs": "4.80.0",
|