新しいAmplify CLIを試す
Schemaメモ
input AMPLIFY { globalAuthRule: AuthRule = { allow: public } }
これはAPI Key onlyっぽい。これが書かれている限りkeyを作るか?と聞かれてしまう。
providerとかを足してもエラーにはならないが無視される
consoleページで試せない(IAM認証でpublicにしていてもApp Sync Consoleで確認ができない)
custom-roles.json を作成
RoleもしくはUsernameをかくっぽい
custom-roles.jsonに書いた内容はすべてのvtlにチェックを入れるという形で入るようです。
新しいリレーションについて
@hasMany + @belongsToの場合
type Blog @model
@auth(rules: [
{allow: public, provider: iam, operations: [read, create, update, delete]}
])
{
id: ID!
name: String!
posts: [Post] @hasMany
}
type Post @model
@auth(rules: [
{allow: public, provider: iam, operations: [read, create, update, delete]}
])
{
id: ID!
title: String!
blog: Blog @belongsTo
}
デフォルトのBlogに@authだけ足したschema
query MyQuery {
getBlog(id: "772460da-6d7a-4167-ad48-4fbb99d5a9ee") {
createdAt
id
name
updatedAt
posts {
items {
blog {
id
}
blogPostsId
id
title
updatedAt
createdAt
}
}
}
}
{
"data": {
"getBlog": {
"createdAt": "2022-01-14T09:26:46.428Z",
"id": "772460da-6d7a-4167-ad48-4fbb99d5a9ee",
"name": "testBlog",
"updatedAt": "2022-01-14T09:26:46.428Z",
"posts": {
"items": [
{
"blog": {
"id": "772460da-6d7a-4167-ad48-4fbb99d5a9ee"
},
"blogPostsId": "772460da-6d7a-4167-ad48-4fbb99d5a9ee",
"id": "1b23932c-661d-4895-8f2e-df995765153c",
"title": "testBlog-testPost1",
"updatedAt": "2022-01-14T10:10:38.390Z",
"createdAt": "2022-01-14T10:10:38.390Z"
}
]
}
}
}
}
query MyQuery {
getPost(id: "1b23932c-661d-4895-8f2e-df995765153c") {
blog {
createdAt
id
name
updatedAt
}
blogPostsId
createdAt
id
title
updatedAt
}
}
{
"data": {
"getPost": {
"blog": {
"createdAt": "2022-01-14T09:26:46.428Z",
"id": "772460da-6d7a-4167-ad48-4fbb99d5a9ee",
"name": "testBlog",
"updatedAt": "2022-01-14T09:26:46.428Z"
},
"blogPostsId": "772460da-6d7a-4167-ad48-4fbb99d5a9ee",
"createdAt": "2022-01-14T10:10:38.390Z",
"id": "1b23932c-661d-4895-8f2e-df995765153c",
"title": "testBlog-testPost1",
"updatedAt": "2022-01-14T10:10:38.390Z"
}
}
}
今回の場合だとblogPostsIdというIdがPostに追加され、getQueryを使うとPostからBlog、BlogからPostが引けるようになる
DynamoDBの生データはこんな感じ
{
"id": {
"S": "772460da-6d7a-4167-ad48-4fbb99d5a9ee"
},
"__typename": {
"S": "Blog"
},
"createdAt": {
"S": "2022-01-14T09:26:46.428Z"
},
"name": {
"S": "testBlog"
},
"updatedAt": {
"S": "2022-01-14T09:26:46.428Z"
}
}
{
"id": {
"S": "1b23932c-661d-4895-8f2e-df995765153c"
},
"__typename": {
"S": "Post"
},
"updatedAt": {
"S": "2022-01-14T10:10:38.390Z"
},
"createdAt": {
"S": "2022-01-14T10:10:38.390Z"
},
"blogPostsId": {
"S": "772460da-6d7a-4167-ad48-4fbb99d5a9ee"
},
"title": {
"S": "testBlog-testPost1"
}
}
__typenameが追加されるのはGraphQL Transformerの旧バージョンと同じ、blogPostsIdは前述のとおり自動で追加されている
@belongsTo を消してみても特にidは変化なし。
単純にgetPost側からBlogが引けなくなる。
↑で書き忘れたけど、blogPostsIdはGSIのパーティションキーになる。
このデフォルト設定だとソートキーがないので、Blog側からPostを時系列でとってこれないので不便そう・・・
DynamoDB的に正しいかはおいておいて、hasOneを試す
type Blog @model
@auth(rules: [
{allow: public, provider: iam, operations: [read, create, update, delete]}
])
{
id: ID!
name: String!
posts: [Post] @hasMany
user: User @belongsTo
}
type Post @model
@auth(rules: [
{allow: public, provider: iam, operations: [read, create, update, delete]}
])
{
id: ID!
title: String!
# blog: Blog @belongsTo
}
type User @model
@auth(rules: [
{allow: public, provider: iam, operations: [read, create, update, delete]}
])
{
id: ID!
name: String!
blog: Blog @hasOne
}
{
"id": {
"S": "e377dc1e-429f-4cdc-a7a3-9cca42813633"
},
"__typename": {
"S": "User"
},
"updatedAt": {
"S": "2022-01-14T11:22:52.834Z"
},
"createdAt": {
"S": "2022-01-14T11:22:52.834Z"
},
"name": {
"S": "testUser"
},
"userBlogId": {
"S": "772460da-6d7a-4167-ad48-4fbb99d5a9ee"
}
}
{
"id": {
"S": "772460da-6d7a-4167-ad48-4fbb99d5a9ee"
},
"__typename": {
"S": "Blog"
},
"updatedAt": {
"S": "2022-01-14T11:24:11.729Z"
},
"createdAt": {
"S": "2022-01-14T09:26:46.428Z"
},
"blogUserId": {
"S": "e377dc1e-429f-4cdc-a7a3-9cca42813633"
},
"name": {
"S": "testBlog"
}
}
それぞれblogUserIdとuserBlogIdが出現する。
manyTomany
・Followerみたいなテーブルは作れない
type FollowRelationship
@model(
mutations: {create: "createFollowRelationship", delete: "deleteFollowRelationship", update: null}
timestamps: null
)
@auth(rules: [
{allow: owner, ownerField:"followerId", provider: userPools, operations:[read, create, delete]},
{allow: private, provider: userPools, operations:[read]}
])
@key(fields: ["followeeId", "followerId"])
{
followeeId: ID!
followerId: ID!
timestamp: Int!
}
これを
type User @model
@auth(rules: [
{allow: public, provider: iam, operations: [read, create, update, delete]}
])
{
id: ID!
name: String!
blog: Blog @hasOne
follower: [User] @manyToMany (relationName: "follower")
}
としても
× An error occurred when pushing the resources to the cloud
🛑 An error occurred during the push operation: @manyToMany relation 'Follower' must be used in exactly two locations.
となってNG
下記のように2二種類のモデルの間でだけ作れるリレーションになっている。
type Post @model
@auth(rules: [
{allow: public, provider: iam, operations: [read, create, update, delete]}
])
{
id: ID!
title: String!
# blog: Blog @belongsTo
tags: [Tags] @manyToMany(relationName: "PostTags")
}
type Tags @model
@auth(rules: [
{allow: public, provider: iam, operations: [read, create, update, delete]}
])
{
id: ID!
label: String!
posts: [Post] @manyToMany(relationName: "PostTags")
}
自動でPostTagsというテーブルが間にできる
二者間の関係を作る場合は別途
createPostTags(input: {postID: "12fce11d-e5c0-4c83-b879-06121fff3a4b", tagsID: "5e7a83ee-fc10-468f-9744-a2d080efcc4b"}) {
id
}
みたいなcreate Mutationで書き込む。
テーブルの実体がschemaに出現しないので、authまわりとかを設定できない気がする。
リレーションを削除したときのテーブル削除については、ちゃんと警告が出る
🛑 An error occurred during the push operation: Removing a model from the GraphQL schema will also remove the underlying DynamoDB table.
This update will remove table(s) [PostTags]
ALL EXISTING DATA IN THESE TABLES WILL BE LOST!
If this is intended, rerun the command with '--allow-destructive-graphql-schema-updates'.
さてじゃあアクセス権限どうなってるのか、と思ったのだけれどもどうもこのあたりが全部マッピングテンプレートに入っているらしい。
IAMにもarn:aws:iam:::policy/-UnauthRolePolicy* というのができるが、こちらはすべてアクセス権がありになる。
パイプラインレゾルバーのMutationcreatePostauth0Function の中に
## [Start] Authorization Steps. **
$util.qr($ctx.stash.put("hasAuth", true))
#set( $inputFields = $util.parseJson($util.toJson($ctx.args.input.keySet())) )
#set( $isAuthorized = false )
#set( $allowedFields = [] )
#if( $util.authType() == "IAM Authorization" )
#set( $adminRoles = ["***"] )
#foreach( $adminRole in $adminRoles )
#if( $ctx.identity.userArn.contains($adminRole) && $ctx.identity.userArn != $ctx.stash.authRole && $ctx.identity.userArn != $ctx.stash.unauthRole )
#return($util.toJson({}))
#end
#end
#if( $ctx.identity.userArn == $ctx.stash.unauthRole )
#set( $isAuthorized = true )
#end
#end
#if( !$isAuthorized && $allowedFields.isEmpty() )
$util.unauthorized()
#end
#if( !$isAuthorized )
#set( $deniedFields = $util.list.copyAndRemoveAll($inputFields, $allowedFields) )
#if( $deniedFields.size() > 0 )
$util.error("Unauthorized on ${deniedFields}", "Unauthorized")
#end
#end
$util.toJson({"version":"2018-05-29","payload":{}})
## [End] Authorization Steps. **
といった感じでuserArnをチェックしているらしい。
authを {allow: private, provider: iam, operations: [read, create, update, delete]}
に変えると
#if( $ctx.identity.userArn == $ctx.stash.unauthRole )
#set( $isAuthorized = true )
#end
が
#if( ($ctx.identity.userArn == $ctx.stash.authRole) || ($ctx.identity.cognitoIdentityPoolId == "***" && $ctx.identity.cognitoIdentityAuthType == "authenticated") )
#set( $isAuthorized = true )
#end
へ変化。
SortKeyを利用できるようにするhasManyリレーション
上記にも書いたように、hasManyのリレーションでは新しくGSIは貼られるが、ソートしても時間順にはできない。(ソートキーが設定されていないため。)
query MyQuery {
getBlog(id: "772460da-6d7a-4167-ad48-4fbb99d5a9ee") {
posts(sortDirection: ASC) {
items {
id
title
createdAt
updatedAt
}
}
}
}
{
"data": {
"getBlog": {
"posts": {
"items": [
{
"id": "1b23932c-661d-4895-8f2e-df995765153c",
"title": "testBlog-testPost1",
"createdAt": "2022-01-14T10:10:38.390Z",
"updatedAt": "2022-01-14T10:10:38.390Z"
},
{
"id": "3b190941-2ec4-49d5-914a-312b7a5903d2",
"title": "BBB",
"createdAt": "2022-01-17T03:36:23.794Z",
"updatedAt": "2022-01-17T03:36:23.794Z"
},
{
"id": "84b42f46-1c80-4959-a789-94404b784d93",
"title": "testpost2",
"createdAt": "2022-01-15T08:30:08.024Z",
"updatedAt": "2022-01-15T08:30:08.024Z"
},
{
"id": "1ed5557b-e7bd-4b79-9eee-c302fa89e12e",
"title": "testpost3",
"createdAt": "2022-01-15T08:30:14.395Z",
"updatedAt": "2022-01-15T08:30:14.395Z"
},
{
"id": "f57dc0f7-b30b-4fdf-9d69-fdbc74aa89a1",
"title": "testpost4",
"createdAt": "2022-01-15T08:30:20.025Z",
"updatedAt": "2022-01-15T08:30:20.025Z"
},
{
"id": "09f5456b-1133-4a1c-b83d-e2a8e15352d8",
"title": "AAA",
"createdAt": "2022-01-17T03:36:11.559Z",
"updatedAt": "2022-01-17T03:36:11.559Z"
},
{
"id": "282e4347-cba2-4d57-9429-9ef8a0c78348",
"title": "CCC",
"createdAt": "2022-01-17T03:35:39.582Z",
"updatedAt": "2022-01-17T03:35:39.582Z"
}
]
}
}
}
}
type Blog @model
@auth(rules: [
{allow: public, provider: iam, operations: [read]},
{allow: private, provider: iam, operations: [read, create, update, delete]}
])
{
id: ID!
name: String!
posts: [Post] @hasMany (indexName: "byBlogPostsId", fields:["id"])
user: User @belongsTo
}
type Post @model
@auth(rules: [
{allow: public, provider: iam, operations: [read]},
{allow: private, provider: iam, operations: [read, create, update, delete]}
])
{
id: ID! @primaryKey
title: String!
blogPostsId: ID @index(name: "byBlogPostsId", sortKeyFields: ["updatedAt"])
# blog: Blog @belongsTo
tags: [Tags] @manyToMany(relationName: "PostTags")
createdAt: AWSDateTime
updatedAt: AWSDateTime
}
というわけで、こんな感じにPostに明示的にblogPostsIdを追加、さらにindexを貼る(sortkeyをupdatedAtに)
BlogのhasManyにindexNameとfieldsを指定すると・・・
query MyQuery {
getBlog(id: "772460da-6d7a-4167-ad48-4fbb99d5a9ee") {
posts(sortDirection: ASC) {
items {
id
title
createdAt
updatedAt
}
}
}
}
{
"data": {
"getBlog": {
"posts": {
"items": [
{
"id": "1b23932c-661d-4895-8f2e-df995765153c",
"title": "testBlog-testPost1",
"createdAt": "2022-01-14T10:10:38.390Z",
"updatedAt": "2022-01-14T10:10:38.390Z"
},
{
"id": "84b42f46-1c80-4959-a789-94404b784d93",
"title": "testpost2",
"createdAt": "2022-01-15T08:30:08.024Z",
"updatedAt": "2022-01-15T08:30:08.024Z"
},
{
"id": "1ed5557b-e7bd-4b79-9eee-c302fa89e12e",
"title": "testpost3",
"createdAt": "2022-01-15T08:30:14.395Z",
"updatedAt": "2022-01-15T08:30:14.395Z"
},
{
"id": "f57dc0f7-b30b-4fdf-9d69-fdbc74aa89a1",
"title": "testpost4",
"createdAt": "2022-01-15T08:30:20.025Z",
"updatedAt": "2022-01-15T08:30:20.025Z"
},
{
"id": "282e4347-cba2-4d57-9429-9ef8a0c78348",
"title": "CCC",
"createdAt": "2022-01-17T03:35:39.582Z",
"updatedAt": "2022-01-17T03:35:39.582Z"
},
{
"id": "09f5456b-1133-4a1c-b83d-e2a8e15352d8",
"title": "AAA",
"createdAt": "2022-01-17T03:36:11.559Z",
"updatedAt": "2022-01-17T03:36:11.559Z"
},
{
"id": "3b190941-2ec4-49d5-914a-312b7a5903d2",
"title": "BBB",
"createdAt": "2022-01-17T03:36:23.794Z",
"updatedAt": "2022-01-17T03:36:23.794Z"
}
]
}
}
}
}
無事、時間でソートできるようになりました!
DynamoDBテーブルのGSIは新しく作り替わっています。
(ちなみに今回の変更ではCLI上で警告が出ることはありませんでした)
GSIのキー名はblogPostsIdのままにしたため、データについては特にいじることなく変更が適用されています。
ちなみに
type Post @model
@auth(rules: [
{allow: public, provider: iam, operations: [read]},
{allow: private, provider: iam, operations: [read, create, update, delete]}
])
{
id: ID!
id2: ID! @primaryKey
title: String!
blogPostsId: ID @index(name: "byBlogPostsId", sortKeyFields: ["updatedAt"])
# blog: Blog @belongsTo
tags: [Tags] @manyToMany(relationName: "PostTags")
createdAt: AWSDateTime
updatedAt: AWSDateTime
}
誤ってprimaryKeyを違うカラムにした場合は
× An error occurred when pushing the resources to the cloud
🛑 An error occurred during the push operation: Editing the primary key of a model requires replacement of the underlying DynamoDB table.
This update will replace table(s) [PostTable]
ALL EXISTING DATA IN THESE TABLES WILL BE LOST!
If this is intended, rerun the command with '--allow-destructive-graphql-schema-updates'.
とエラーになってくれます。
ここまでの操作でうっかりデータが消える操作はこの警告が出てくれるように思います。
自動生成のQuery/Mutaion/Subscriptionの名前変更・抑制
特にlistクエリは不要だけれど、アクセス権としてはreadという形でgetクエリと同時に管理することになってしまう、というのを回避することができるようになる、というのが魅力だなと思いました。
例えば
type User @model (queries: {list: null, get: "getUser"} subscriptions:null)
@auth(rules: [
{allow: public, provider: iam, operations: [read]},
{allow: private, provider: iam, operations: [read, create, update, delete]}
])
{
id: ID!
name: String!
blog: Blog @hasOne
}
とすると、subscriptionは存在しなくなり、queryもgetUserクエリのみがデプロイ(生成)されます。
注意ポイントがあって
- 片方をnullにした際に何も記載しないと別のクエリも消えてしまう
@model (queries: {list: null })
とするとgetクエリも消えてしまいました
- 名前はプレフィックス指定ではなく全体の名前を指定する
@model (queries: {list: null, get: "get"})
とすると、getというクエリができてしまいます。
getUserとしないといけません。
こちらはdocsからは読み取りづらかったです。
Queryの生成制御について調べてあれ、とおもったので追記。
GSIのフィールドをクエリするためには・・・
type Post @model (queries: {list: null, get: "getPost"} subscriptions:null)
@auth(rules: [
{allow: public, provider: iam, operations: [read]},
{allow: private, provider: iam, operations: [read, create, update, delete]}
])
{
id: ID! @primaryKey
title: String!
blogPostsId: ID @index(name: "byBlogPostsId", sortKeyFields: ["updatedAt"], queryField: "getPostsByBlogPostsId")
# blog: Blog @belongsTo
tags: [Tags] @manyToMany(relationName: "PostTags")
createdAt: AWSDateTime
updatedAt: AWSDateTime
}
blogPostsId: ID @index(name: "byBlogPostsId", sortKeyFields: ["updatedAt"], queryField: "getPostsByBlogPostsId")
こんな感じでqueryFieldを足せばOK
上記の例のようにqueryの生成制御と同時に設定できる。
これを私も踏んでしまった。
こんな感じで行けそうだけど、回答ついてないんだよな~
const cognitoSetting = amplifyConfiguration.aws_cognito_password_protection_settings as {
passwordPolicyMinLength: number;
passwordPolicyCharacters: ("REQUIRES_LOWERCASE" | "REQUIRES_NUMBERS" | "REQUIRES_SYMBOLS" | "REQUIRES_UPPERCASE")[];
}
const errorKeyList = ["confirm_password",
"is_password_shorter",
"is_password_longer",
"require_lowercase",
"require_numbers",
"require_symbols",
"require_uppercase"]
return(
<Authenticator
initialState="signUp"
components={{
SignUp: {
FormFields() {
const {validationErrors}= useAuthenticator()
return (
<>
<Authenticator.SignUp.FormFields />
{validationErrors && errorKeyList.map((key) => {
if (validationErrors[key]) {
return (
<div key={key} style={{color: "red"}}>
{validationErrors[key]}
</div>
)
}
})
}
</>
)
},
},
}}
services={{
async validateConfirmPassword(
formData: { password: string; confirm_password: string },
touchData: { password: boolean; confirm_password: boolean }
) {
if (!formData.password && !formData.confirm_password) {
return null
} else if (touchData.password && touchData.confirm_password) {
const errorObj: {
confirm_password?: string,
is_password_shorter?: string,
is_password_longer?: string,
require_lowercase?: string,
require_numbers?: string,
require_symbols?: string,
require_uppercase?: string,
} = {}
if (formData.password !== formData.confirm_password) {
errorObj.confirm_password = 'Your passwords must match'
} else {
if (formData.password.length < cognitoSetting.passwordPolicyMinLength) {
errorObj.is_password_shorter = `Your passwords' length should be greater than ${cognitoSetting.passwordPolicyMinLength}`
} else if (formData.password.length > 99) {
errorObj.is_password_longer = `Your passwords' length should be shorter than 99`
}
for (const policy of cognitoSetting.passwordPolicyCharacters) {
switch(policy) {
case "REQUIRES_LOWERCASE":
if (!/[a-z]/.test(formData.password)){
errorObj.require_lowercase = 'Your passwords should contain lower case alphabet'
}
break
case "REQUIRES_SYMBOLS":
if (!/[0-9]/.test(formData.password)){
errorObj.require_lowercase = 'Your passwords should contain number'
}
break
case "REQUIRES_NUMBERS":
if (!/[\^\$\*\.\[\]\{\}\(\)\?\"\!\@\#\%\&\/\\\,\>\<\'\:\;\|\_\~\`]/.test(formData.password)){
errorObj.require_numbers = 'Your passwords should contain special character'
}
break
case "REQUIRES_UPPERCASE":
if (!/[A-Z]/.test(formData.password)){
errorObj.require_uppercase = 'Your passwords should contain upper case alphabet'
}
break
}
}
}
return errorObj
}
},
}}
>
{({ signOut, user }) => (
<div>
<h1>Hello {user.username}</h1>
<button onClick={signOut}>Sign out</button>
</div>
)}
</Authenticator>
)