diff --git a/schema/testdata/proto_set_attribute_lhs_within_write_scope/proto.json b/schema/testdata/proto_set_attribute_lhs_within_write_scope/proto.json new file mode 100644 index 000000000..41f627066 --- /dev/null +++ b/schema/testdata/proto_set_attribute_lhs_within_write_scope/proto.json @@ -0,0 +1,1313 @@ +{ + "models": [ + { + "name": "Post", + "fields": [ + { + "modelName": "Post", + "name": "name", + "type": { + "type": "TYPE_STRING" + }, + "optional": true + }, + { + "modelName": "Post", + "name": "published", + "type": { + "type": "TYPE_BOOL" + }, + "optional": true + }, + { + "modelName": "Post", + "name": "identity", + "type": { + "type": "TYPE_MODEL", + "modelName": "Identity" + }, + "optional": true, + "foreignKeyFieldName": "identityId" + }, + { + "modelName": "Post", + "name": "publisher", + "type": { + "type": "TYPE_MODEL", + "modelName": "Publisher" + }, + "optional": true, + "foreignKeyFieldName": "publisherId" + }, + { + "modelName": "Post", + "name": "id", + "type": { + "type": "TYPE_ID" + }, + "primaryKey": true, + "defaultValue": { + "useZeroValue": true + } + }, + { + "modelName": "Post", + "name": "createdAt", + "type": { + "type": "TYPE_DATETIME" + }, + "defaultValue": { + "useZeroValue": true + } + }, + { + "modelName": "Post", + "name": "updatedAt", + "type": { + "type": "TYPE_DATETIME" + }, + "defaultValue": { + "useZeroValue": true + } + }, + { + "modelName": "Post", + "name": "identityId", + "type": { + "type": "TYPE_ID" + }, + "optional": true, + "foreignKeyInfo": { + "relatedModelName": "Identity", + "relatedModelField": "id" + } + }, + { + "modelName": "Post", + "name": "publisherId", + "type": { + "type": "TYPE_ID" + }, + "optional": true, + "foreignKeyInfo": { + "relatedModelName": "Publisher", + "relatedModelField": "id" + } + } + ], + "actions": [ + { + "modelName": "Post", + "name": "nestedData1", + "type": "ACTION_TYPE_CREATE", + "implementation": "ACTION_IMPLEMENTATION_AUTO", + "setExpressions": [ + { + "source": "post.publisher.departments.head.id = ctx.identity.person.id" + } + ], + "inputMessageName": "NestedData1Input" + }, + { + "modelName": "Post", + "name": "nestedData2", + "type": "ACTION_TYPE_CREATE", + "implementation": "ACTION_IMPLEMENTATION_AUTO", + "setExpressions": [ + { + "source": "post.publisher.id = ctx.identity.publisher.id" + } + ], + "inputMessageName": "NestedData2Input" + }, + { + "modelName": "Post", + "name": "nestedData3", + "type": "ACTION_TYPE_CREATE", + "implementation": "ACTION_IMPLEMENTATION_AUTO", + "setExpressions": [ + { + "source": "post.publisher.name = \"someName\"" + } + ], + "inputMessageName": "NestedData3Input" + }, + { + "modelName": "Post", + "name": "nestedData4", + "type": "ACTION_TYPE_CREATE", + "implementation": "ACTION_IMPLEMENTATION_AUTO", + "setExpressions": [ + { + "source": "post.name = \"some name\"" + } + ], + "inputMessageName": "NestedData4Input" + }, + { + "modelName": "Post", + "name": "nestedData5", + "type": "ACTION_TYPE_CREATE", + "implementation": "ACTION_IMPLEMENTATION_AUTO", + "setExpressions": [ + { + "source": "post.publisher = ctx.identity.publisher" + } + ], + "inputMessageName": "NestedData5Input" + }, + { + "modelName": "Post", + "name": "nestedData6", + "type": "ACTION_TYPE_CREATE", + "implementation": "ACTION_IMPLEMENTATION_AUTO", + "setExpressions": [ + { + "source": "post.publisher.name = \"someName\"" + } + ], + "inputMessageName": "NestedData6Input" + }, + { + "modelName": "Post", + "name": "nestedData7", + "type": "ACTION_TYPE_CREATE", + "implementation": "ACTION_IMPLEMENTATION_AUTO", + "setExpressions": [ + { + "source": "post.publisher.country.publishersCount = 1" + } + ], + "inputMessageName": "NestedData7Input" + }, + { + "modelName": "Post", + "name": "nestedData8", + "type": "ACTION_TYPE_CREATE", + "implementation": "ACTION_IMPLEMENTATION_AUTO", + "setExpressions": [ + { + "source": "post.publisher.departments.head = ctx.identity.person" + } + ], + "inputMessageName": "NestedData8Input" + } + ] + }, + { + "name": "Publisher", + "fields": [ + { + "modelName": "Publisher", + "name": "name", + "type": { + "type": "TYPE_STRING" + } + }, + { + "modelName": "Publisher", + "name": "owner", + "type": { + "type": "TYPE_MODEL", + "modelName": "Person" + }, + "foreignKeyFieldName": "ownerId" + }, + { + "modelName": "Publisher", + "name": "departments", + "type": { + "type": "TYPE_MODEL", + "modelName": "Department", + "repeated": true + }, + "inverseFieldName": "publisher" + }, + { + "modelName": "Publisher", + "name": "country", + "type": { + "type": "TYPE_MODEL", + "modelName": "Country" + }, + "foreignKeyFieldName": "countryId" + }, + { + "modelName": "Publisher", + "name": "admin", + "type": { + "type": "TYPE_MODEL", + "modelName": "Identity" + }, + "unique": true, + "foreignKeyFieldName": "adminId", + "inverseFieldName": "publisher" + }, + { + "modelName": "Publisher", + "name": "id", + "type": { + "type": "TYPE_ID" + }, + "primaryKey": true, + "defaultValue": { + "useZeroValue": true + } + }, + { + "modelName": "Publisher", + "name": "createdAt", + "type": { + "type": "TYPE_DATETIME" + }, + "defaultValue": { + "useZeroValue": true + } + }, + { + "modelName": "Publisher", + "name": "updatedAt", + "type": { + "type": "TYPE_DATETIME" + }, + "defaultValue": { + "useZeroValue": true + } + }, + { + "modelName": "Publisher", + "name": "ownerId", + "type": { + "type": "TYPE_ID" + }, + "foreignKeyInfo": { + "relatedModelName": "Person", + "relatedModelField": "id" + } + }, + { + "modelName": "Publisher", + "name": "countryId", + "type": { + "type": "TYPE_ID" + }, + "foreignKeyInfo": { + "relatedModelName": "Country", + "relatedModelField": "id" + } + }, + { + "modelName": "Publisher", + "name": "adminId", + "type": { + "type": "TYPE_ID" + }, + "unique": true, + "foreignKeyInfo": { + "relatedModelName": "Identity", + "relatedModelField": "id" + } + } + ] + }, + { + "name": "Department", + "fields": [ + { + "modelName": "Department", + "name": "publisher", + "type": { + "type": "TYPE_MODEL", + "modelName": "Publisher" + }, + "foreignKeyFieldName": "publisherId", + "inverseFieldName": "departments" + }, + { + "modelName": "Department", + "name": "head", + "type": { + "type": "TYPE_MODEL", + "modelName": "Person" + }, + "optional": true, + "foreignKeyFieldName": "headId" + }, + { + "modelName": "Department", + "name": "name", + "type": { + "type": "TYPE_STRING" + } + }, + { + "modelName": "Department", + "name": "number", + "type": { + "type": "TYPE_STRING" + } + }, + { + "modelName": "Department", + "name": "id", + "type": { + "type": "TYPE_ID" + }, + "primaryKey": true, + "defaultValue": { + "useZeroValue": true + } + }, + { + "modelName": "Department", + "name": "createdAt", + "type": { + "type": "TYPE_DATETIME" + }, + "defaultValue": { + "useZeroValue": true + } + }, + { + "modelName": "Department", + "name": "updatedAt", + "type": { + "type": "TYPE_DATETIME" + }, + "defaultValue": { + "useZeroValue": true + } + }, + { + "modelName": "Department", + "name": "publisherId", + "type": { + "type": "TYPE_ID" + }, + "foreignKeyInfo": { + "relatedModelName": "Publisher", + "relatedModelField": "id" + } + }, + { + "modelName": "Department", + "name": "headId", + "type": { + "type": "TYPE_ID" + }, + "optional": true, + "foreignKeyInfo": { + "relatedModelName": "Person", + "relatedModelField": "id" + } + } + ] + }, + { + "name": "Country", + "fields": [ + { + "modelName": "Country", + "name": "name", + "type": { + "type": "TYPE_STRING" + }, + "optional": true + }, + { + "modelName": "Country", + "name": "publishersCount", + "type": { + "type": "TYPE_INT" + }, + "optional": true + }, + { + "modelName": "Country", + "name": "id", + "type": { + "type": "TYPE_ID" + }, + "primaryKey": true, + "defaultValue": { + "useZeroValue": true + } + }, + { + "modelName": "Country", + "name": "createdAt", + "type": { + "type": "TYPE_DATETIME" + }, + "defaultValue": { + "useZeroValue": true + } + }, + { + "modelName": "Country", + "name": "updatedAt", + "type": { + "type": "TYPE_DATETIME" + }, + "defaultValue": { + "useZeroValue": true + } + } + ] + }, + { + "name": "Person", + "fields": [ + { + "modelName": "Person", + "name": "name", + "type": { + "type": "TYPE_STRING" + } + }, + { + "modelName": "Person", + "name": "identity", + "type": { + "type": "TYPE_MODEL", + "modelName": "Identity" + }, + "unique": true, + "foreignKeyFieldName": "identityId", + "inverseFieldName": "person" + }, + { + "modelName": "Person", + "name": "id", + "type": { + "type": "TYPE_ID" + }, + "primaryKey": true, + "defaultValue": { + "useZeroValue": true + } + }, + { + "modelName": "Person", + "name": "createdAt", + "type": { + "type": "TYPE_DATETIME" + }, + "defaultValue": { + "useZeroValue": true + } + }, + { + "modelName": "Person", + "name": "updatedAt", + "type": { + "type": "TYPE_DATETIME" + }, + "defaultValue": { + "useZeroValue": true + } + }, + { + "modelName": "Person", + "name": "identityId", + "type": { + "type": "TYPE_ID" + }, + "unique": true, + "foreignKeyInfo": { + "relatedModelName": "Identity", + "relatedModelField": "id" + } + } + ] + }, + { + "name": "Identity", + "fields": [ + { + "modelName": "Identity", + "name": "email", + "type": { + "type": "TYPE_STRING" + }, + "optional": true, + "uniqueWith": [ + "issuer" + ] + }, + { + "modelName": "Identity", + "name": "emailVerified", + "type": { + "type": "TYPE_BOOL" + }, + "defaultValue": { + "expression": { + "source": "false" + } + } + }, + { + "modelName": "Identity", + "name": "password", + "type": { + "type": "TYPE_PASSWORD" + }, + "optional": true + }, + { + "modelName": "Identity", + "name": "externalId", + "type": { + "type": "TYPE_STRING" + }, + "optional": true + }, + { + "modelName": "Identity", + "name": "issuer", + "type": { + "type": "TYPE_STRING" + }, + "optional": true, + "uniqueWith": [ + "email" + ] + }, + { + "modelName": "Identity", + "name": "id", + "type": { + "type": "TYPE_ID" + }, + "primaryKey": true, + "defaultValue": { + "useZeroValue": true + } + }, + { + "modelName": "Identity", + "name": "createdAt", + "type": { + "type": "TYPE_DATETIME" + }, + "defaultValue": { + "useZeroValue": true + } + }, + { + "modelName": "Identity", + "name": "updatedAt", + "type": { + "type": "TYPE_DATETIME" + }, + "defaultValue": { + "useZeroValue": true + } + }, + { + "modelName": "Identity", + "name": "publisher", + "type": { + "type": "TYPE_MODEL", + "modelName": "Publisher" + }, + "optional": true, + "inverseFieldName": "admin" + }, + { + "modelName": "Identity", + "name": "person", + "type": { + "type": "TYPE_MODEL", + "modelName": "Person" + }, + "optional": true, + "inverseFieldName": "identity" + } + ], + "actions": [ + { + "modelName": "Identity", + "name": "authenticate", + "type": "ACTION_TYPE_WRITE", + "implementation": "ACTION_IMPLEMENTATION_RUNTIME", + "inputMessageName": "AuthenticateInput", + "responseMessageName": "AuthenticateResponse" + }, + { + "modelName": "Identity", + "name": "requestPasswordReset", + "type": "ACTION_TYPE_WRITE", + "implementation": "ACTION_IMPLEMENTATION_RUNTIME", + "inputMessageName": "RequestPasswordResetInput", + "responseMessageName": "RequestPasswordResetResponse" + }, + { + "modelName": "Identity", + "name": "resetPassword", + "type": "ACTION_TYPE_WRITE", + "implementation": "ACTION_IMPLEMENTATION_RUNTIME", + "inputMessageName": "ResetPasswordInput", + "responseMessageName": "ResetPasswordResponse" + } + ] + } + ], + "apis": [ + { + "name": "Api", + "apiModels": [ + { + "modelName": "Country" + }, + { + "modelName": "Department" + }, + { + "modelName": "Identity" + }, + { + "modelName": "Person" + }, + { + "modelName": "Post" + }, + { + "modelName": "Publisher" + } + ] + } + ], + "messages": [ + { + "name": "Any" + }, + { + "name": "NestedData1Input", + "fields": [ + { + "messageName": "NestedData1Input", + "name": "publisher", + "type": { + "type": "TYPE_MESSAGE", + "messageName": "NestedData1PublisherInput" + }, + "nullable": true + } + ] + }, + { + "name": "NestedData1PublisherInput", + "fields": [ + { + "messageName": "NestedData1PublisherInput", + "name": "country", + "type": { + "type": "TYPE_MESSAGE", + "messageName": "NestedData1PublisherCountryInput" + } + }, + { + "messageName": "NestedData1PublisherInput", + "name": "departments", + "type": { + "type": "TYPE_MESSAGE", + "messageName": "NestedData1PublisherDepartmentsInput", + "repeated": true + } + } + ] + }, + { + "name": "NestedData1PublisherCountryInput", + "fields": [ + { + "messageName": "NestedData1PublisherCountryInput", + "name": "id", + "type": { + "type": "TYPE_ID", + "modelName": "Country", + "fieldName": "id" + }, + "target": [ + "publisher", + "country", + "id" + ] + } + ] + }, + { + "name": "NestedData1PublisherDepartmentsInput", + "fields": [ + { + "messageName": "NestedData1PublisherDepartmentsInput", + "name": "name", + "type": { + "type": "TYPE_STRING", + "modelName": "Department", + "fieldName": "name" + }, + "target": [ + "publisher", + "departments", + "name" + ] + }, + { + "messageName": "NestedData1PublisherDepartmentsInput", + "name": "number", + "type": { + "type": "TYPE_STRING", + "modelName": "Department", + "fieldName": "number" + }, + "target": [ + "publisher", + "departments", + "number" + ] + } + ] + }, + { + "name": "NestedData2Input", + "fields": [ + { + "messageName": "NestedData2Input", + "name": "name", + "type": { + "type": "TYPE_STRING", + "modelName": "Post", + "fieldName": "name" + }, + "nullable": true, + "target": [ + "name" + ] + } + ] + }, + { + "name": "NestedData3Input", + "fields": [ + { + "messageName": "NestedData3Input", + "name": "publisher", + "type": { + "type": "TYPE_MESSAGE", + "messageName": "NestedData3PublisherInput" + }, + "nullable": true + } + ] + }, + { + "name": "NestedData3PublisherInput", + "fields": [ + { + "messageName": "NestedData3PublisherInput", + "name": "country", + "type": { + "type": "TYPE_MESSAGE", + "messageName": "NestedData3PublisherCountryInput" + } + }, + { + "messageName": "NestedData3PublisherInput", + "name": "departments", + "type": { + "type": "TYPE_MESSAGE", + "messageName": "NestedData3PublisherDepartmentsInput", + "repeated": true + } + } + ] + }, + { + "name": "NestedData3PublisherCountryInput", + "fields": [ + { + "messageName": "NestedData3PublisherCountryInput", + "name": "id", + "type": { + "type": "TYPE_ID", + "modelName": "Country", + "fieldName": "id" + }, + "target": [ + "publisher", + "country", + "id" + ] + } + ] + }, + { + "name": "NestedData3PublisherDepartmentsInput", + "fields": [ + { + "messageName": "NestedData3PublisherDepartmentsInput", + "name": "name", + "type": { + "type": "TYPE_STRING", + "modelName": "Department", + "fieldName": "name" + }, + "target": [ + "publisher", + "departments", + "name" + ] + }, + { + "messageName": "NestedData3PublisherDepartmentsInput", + "name": "number", + "type": { + "type": "TYPE_STRING", + "modelName": "Department", + "fieldName": "number" + }, + "target": [ + "publisher", + "departments", + "number" + ] + } + ] + }, + { + "name": "NestedData4Input", + "fields": [ + { + "messageName": "NestedData4Input", + "name": "publisher", + "type": { + "type": "TYPE_MESSAGE", + "messageName": "NestedData4PublisherInput" + }, + "nullable": true + } + ] + }, + { + "name": "NestedData4PublisherInput", + "fields": [ + { + "messageName": "NestedData4PublisherInput", + "name": "country", + "type": { + "type": "TYPE_MESSAGE", + "messageName": "NestedData4PublisherCountryInput" + } + }, + { + "messageName": "NestedData4PublisherInput", + "name": "departments", + "type": { + "type": "TYPE_MESSAGE", + "messageName": "NestedData4PublisherDepartmentsInput", + "repeated": true + } + } + ] + }, + { + "name": "NestedData4PublisherCountryInput", + "fields": [ + { + "messageName": "NestedData4PublisherCountryInput", + "name": "id", + "type": { + "type": "TYPE_ID", + "modelName": "Country", + "fieldName": "id" + }, + "target": [ + "publisher", + "country", + "id" + ] + } + ] + }, + { + "name": "NestedData4PublisherDepartmentsInput", + "fields": [ + { + "messageName": "NestedData4PublisherDepartmentsInput", + "name": "name", + "type": { + "type": "TYPE_STRING", + "modelName": "Department", + "fieldName": "name" + }, + "target": [ + "publisher", + "departments", + "name" + ] + }, + { + "messageName": "NestedData4PublisherDepartmentsInput", + "name": "number", + "type": { + "type": "TYPE_STRING", + "modelName": "Department", + "fieldName": "number" + }, + "target": [ + "publisher", + "departments", + "number" + ] + } + ] + }, + { + "name": "NestedData5Input", + "fields": [ + { + "messageName": "NestedData5Input", + "name": "name", + "type": { + "type": "TYPE_STRING", + "modelName": "Post", + "fieldName": "name" + }, + "nullable": true, + "target": [ + "name" + ] + } + ] + }, + { + "name": "NestedData6Input", + "fields": [ + { + "messageName": "NestedData6Input", + "name": "publisher", + "type": { + "type": "TYPE_MESSAGE", + "messageName": "NestedData6PublisherInput" + }, + "nullable": true + } + ] + }, + { + "name": "NestedData6PublisherInput", + "fields": [ + { + "messageName": "NestedData6PublisherInput", + "name": "country", + "type": { + "type": "TYPE_MESSAGE", + "messageName": "NestedData6PublisherCountryInput" + } + } + ] + }, + { + "name": "NestedData6PublisherCountryInput", + "fields": [ + { + "messageName": "NestedData6PublisherCountryInput", + "name": "id", + "type": { + "type": "TYPE_ID", + "modelName": "Country", + "fieldName": "id" + }, + "target": [ + "publisher", + "country", + "id" + ] + } + ] + }, + { + "name": "NestedData7Input", + "fields": [ + { + "messageName": "NestedData7Input", + "name": "publisher", + "type": { + "type": "TYPE_MESSAGE", + "messageName": "NestedData7PublisherInput" + }, + "nullable": true + } + ] + }, + { + "name": "NestedData7PublisherInput", + "fields": [ + { + "messageName": "NestedData7PublisherInput", + "name": "country", + "type": { + "type": "TYPE_MESSAGE", + "messageName": "NestedData7PublisherCountryInput" + } + }, + { + "messageName": "NestedData7PublisherInput", + "name": "departments", + "type": { + "type": "TYPE_MESSAGE", + "messageName": "NestedData7PublisherDepartmentsInput", + "repeated": true + } + } + ] + }, + { + "name": "NestedData7PublisherCountryInput", + "fields": [ + { + "messageName": "NestedData7PublisherCountryInput", + "name": "name", + "type": { + "type": "TYPE_STRING", + "modelName": "Country", + "fieldName": "name" + }, + "nullable": true, + "target": [ + "publisher", + "country", + "name" + ] + } + ] + }, + { + "name": "NestedData7PublisherDepartmentsInput", + "fields": [ + { + "messageName": "NestedData7PublisherDepartmentsInput", + "name": "name", + "type": { + "type": "TYPE_STRING", + "modelName": "Department", + "fieldName": "name" + }, + "target": [ + "publisher", + "departments", + "name" + ] + }, + { + "messageName": "NestedData7PublisherDepartmentsInput", + "name": "number", + "type": { + "type": "TYPE_STRING", + "modelName": "Department", + "fieldName": "number" + }, + "target": [ + "publisher", + "departments", + "number" + ] + } + ] + }, + { + "name": "NestedData8Input", + "fields": [ + { + "messageName": "NestedData8Input", + "name": "publisher", + "type": { + "type": "TYPE_MESSAGE", + "messageName": "NestedData8PublisherInput" + }, + "nullable": true + } + ] + }, + { + "name": "NestedData8PublisherInput", + "fields": [ + { + "messageName": "NestedData8PublisherInput", + "name": "country", + "type": { + "type": "TYPE_MESSAGE", + "messageName": "NestedData8PublisherCountryInput" + } + }, + { + "messageName": "NestedData8PublisherInput", + "name": "departments", + "type": { + "type": "TYPE_MESSAGE", + "messageName": "NestedData8PublisherDepartmentsInput", + "repeated": true + } + } + ] + }, + { + "name": "NestedData8PublisherCountryInput", + "fields": [ + { + "messageName": "NestedData8PublisherCountryInput", + "name": "id", + "type": { + "type": "TYPE_ID", + "modelName": "Country", + "fieldName": "id" + }, + "target": [ + "publisher", + "country", + "id" + ] + } + ] + }, + { + "name": "NestedData8PublisherDepartmentsInput", + "fields": [ + { + "messageName": "NestedData8PublisherDepartmentsInput", + "name": "name", + "type": { + "type": "TYPE_STRING", + "modelName": "Department", + "fieldName": "name" + }, + "target": [ + "publisher", + "departments", + "name" + ] + }, + { + "messageName": "NestedData8PublisherDepartmentsInput", + "name": "number", + "type": { + "type": "TYPE_STRING", + "modelName": "Department", + "fieldName": "number" + }, + "target": [ + "publisher", + "departments", + "number" + ] + } + ] + }, + { + "name": "EmailPasswordInput", + "fields": [ + { + "messageName": "EmailPasswordInput", + "name": "email", + "type": { + "type": "TYPE_STRING" + } + }, + { + "messageName": "EmailPasswordInput", + "name": "password", + "type": { + "type": "TYPE_STRING" + } + } + ] + }, + { + "name": "AuthenticateInput", + "fields": [ + { + "messageName": "AuthenticateInput", + "name": "createIfNotExists", + "type": { + "type": "TYPE_BOOL" + }, + "optional": true + }, + { + "messageName": "AuthenticateInput", + "name": "emailPassword", + "type": { + "type": "TYPE_MESSAGE", + "messageName": "EmailPasswordInput" + } + } + ] + }, + { + "name": "AuthenticateResponse", + "fields": [ + { + "messageName": "AuthenticateResponse", + "name": "identityCreated", + "type": { + "type": "TYPE_BOOL" + } + }, + { + "messageName": "AuthenticateResponse", + "name": "token", + "type": { + "type": "TYPE_STRING" + } + } + ] + }, + { + "name": "RequestPasswordResetInput", + "fields": [ + { + "messageName": "RequestPasswordResetInput", + "name": "email", + "type": { + "type": "TYPE_STRING" + } + }, + { + "messageName": "RequestPasswordResetInput", + "name": "redirectUrl", + "type": { + "type": "TYPE_STRING" + } + } + ] + }, + { + "name": "RequestPasswordResetResponse" + }, + { + "name": "ResetPasswordInput", + "fields": [ + { + "messageName": "ResetPasswordInput", + "name": "token", + "type": { + "type": "TYPE_STRING" + } + }, + { + "messageName": "ResetPasswordInput", + "name": "password", + "type": { + "type": "TYPE_STRING" + } + } + ] + }, + { + "name": "ResetPasswordResponse" + } + ] +} \ No newline at end of file diff --git a/schema/testdata/proto_set_attribute_lhs_within_write_scope/schema.keel b/schema/testdata/proto_set_attribute_lhs_within_write_scope/schema.keel new file mode 100644 index 000000000..c757756e0 --- /dev/null +++ b/schema/testdata/proto_set_attribute_lhs_within_write_scope/schema.keel @@ -0,0 +1,88 @@ +model Post { + fields { + name Text? + published Boolean? + identity Identity? + publisher Publisher? + } + + actions { + create nestedData1() with ( + publisher.country.id, + publisher.departments.name, + publisher.departments.number, + ) { + @set(post.publisher.departments.head.id = ctx.identity.person.id) + } + create nestedData2() with (name) { + @set(post.publisher.id = ctx.identity.publisher.id) + } + create nestedData3() with ( + publisher.country.id, + publisher.departments.name, + publisher.departments.number, + ) { + @set(post.publisher.name = "someName") + } + create nestedData4() with ( + publisher.country.id, + publisher.departments.name, + publisher.departments.number, + ) { + @set(post.name = "some name") + } + create nestedData5() with (name) { + @set(post.publisher = ctx.identity.publisher) + } + create nestedData6() with (publisher.country.id) { + @set(post.publisher.name = "someName") + } + create nestedData7() with ( + publisher.country.name, + publisher.departments.name, + publisher.departments.number, + ) { + @set(post.publisher.country.publishersCount = 1) + } + create nestedData8() with ( + publisher.country.id, + publisher.departments.name, + publisher.departments.number, + ) { + @set(post.publisher.departments.head = ctx.identity.person) + } + } +} + +model Publisher { + fields { + name Text + owner Person + departments Department[] + country Country + admin Identity @unique + } +} + +model Department { + fields { + publisher Publisher + head Person? + name Text + number Text + } +} + +model Country { + fields { + name Text? + publishersCount Number? + } +} + +model Person { + fields { + name Text + identity Identity @unique + } +} diff --git a/schema/testdata/validation_expression_null_non_optional_field/errors.json b/schema/testdata/validation_expression_null_non_optional_field/errors.json index d3e1ab7c0..3c2392b52 100644 --- a/schema/testdata/validation_expression_null_non_optional_field/errors.json +++ b/schema/testdata/validation_expression_null_non_optional_field/errors.json @@ -6,32 +6,32 @@ "code": "E060", "pos": { "filename": "testdata/validation_expression_null_non_optional_field/schema.keel", - "offset": 142, + "offset": 163, "line": 9, "column": 18 }, "endPos": { "filename": "testdata/validation_expression_null_non_optional_field/schema.keel", - "offset": 160, + "offset": 181, "line": 9, "column": 36 } }, { - "message": "name cannot be null", + "message": "country cannot be null", "hint": "You cannot evaluate a field against null unless it is defined as optional", "code": "E060", "pos": { "filename": "testdata/validation_expression_null_non_optional_field/schema.keel", - "offset": 179, + "offset": 200, "line": 10, "column": 18 }, "endPos": { "filename": "testdata/validation_expression_null_non_optional_field/schema.keel", - "offset": 197, + "offset": 230, "line": 10, - "column": 36 + "column": 48 } }, { @@ -40,15 +40,15 @@ "code": "E060", "pos": { "filename": "testdata/validation_expression_null_non_optional_field/schema.keel", - "offset": 216, - "line": 11, - "column": 18 + "offset": 348, + "line": 15, + "column": 20 }, "endPos": { "filename": "testdata/validation_expression_null_non_optional_field/schema.keel", - "offset": 243, - "line": 11, - "column": 45 + "offset": 367, + "line": 15, + "column": 39 } }, { @@ -57,13 +57,13 @@ "code": "E060", "pos": { "filename": "testdata/validation_expression_null_non_optional_field/schema.keel", - "offset": 361, + "offset": 388, "line": 16, "column": 20 }, "endPos": { "filename": "testdata/validation_expression_null_non_optional_field/schema.keel", - "offset": 380, + "offset": 407, "line": 16, "column": 39 } @@ -74,31 +74,14 @@ "code": "E060", "pos": { "filename": "testdata/validation_expression_null_non_optional_field/schema.keel", - "offset": 401, + "offset": 428, "line": 17, "column": 20 }, "endPos": { "filename": "testdata/validation_expression_null_non_optional_field/schema.keel", - "offset": 420, + "offset": 456, "line": 17, - "column": 39 - } - }, - { - "message": "name cannot be null", - "hint": "You cannot evaluate a field against null unless it is defined as optional", - "code": "E060", - "pos": { - "filename": "testdata/validation_expression_null_non_optional_field/schema.keel", - "offset": 441, - "line": 18, - "column": 20 - }, - "endPos": { - "filename": "testdata/validation_expression_null_non_optional_field/schema.keel", - "offset": 469, - "line": 18, "column": 48 } }, @@ -108,14 +91,14 @@ "code": "E060", "pos": { "filename": "testdata/validation_expression_null_non_optional_field/schema.keel", - "offset": 281, - "line": 12, + "offset": 268, + "line": 11, "column": 37 }, "endPos": { "filename": "testdata/validation_expression_null_non_optional_field/schema.keel", - "offset": 300, - "line": 12, + "offset": 287, + "line": 11, "column": 56 } }, @@ -125,14 +108,14 @@ "code": "E060", "pos": { "filename": "testdata/validation_expression_null_non_optional_field/schema.keel", - "offset": 507, - "line": 19, + "offset": 494, + "line": 18, "column": 37 }, "endPos": { "filename": "testdata/validation_expression_null_non_optional_field/schema.keel", - "offset": 526, - "line": 19, + "offset": 513, + "line": 18, "column": 56 } } diff --git a/schema/testdata/validation_expression_null_non_optional_field/schema.keel b/schema/testdata/validation_expression_null_non_optional_field/schema.keel index af108818c..265f2a01d 100644 --- a/schema/testdata/validation_expression_null_non_optional_field/schema.keel +++ b/schema/testdata/validation_expression_null_non_optional_field/schema.keel @@ -5,10 +5,9 @@ model Person { } actions { - create createPerson() { + create createPerson() with (employer.name) { @set(person.name = null) - @set(null = person.name) - @set(person.employer.name = null) + @set(person.employer.country = null) @permission(expression: person.name != null) } @@ -24,6 +23,7 @@ model Person { model Company { fields { name Text + country Text employees Person[] } } \ No newline at end of file diff --git a/schema/testdata/validation_expression_validate_expression/errors.json b/schema/testdata/validation_expression_validate_expression/errors.json deleted file mode 100644 index 282d59a83..000000000 --- a/schema/testdata/validation_expression_validate_expression/errors.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "errors": [ - { - "message": "published is Boolean and \"foo\" is Text", - "hint": "Please make sure that you are evaluating entities of the same type", - "code": "E026", - "pos": { - "filename": "testdata/validation_expression_validate_expression/schema.keel", - "offset": 143, - "line": 8, - "column": 23 - }, - "endPos": { - "filename": "testdata/validation_expression_validate_expression/schema.keel", - "offset": 161, - "line": 8, - "column": 41 - } - } - ] -} \ No newline at end of file diff --git a/schema/testdata/validation_expression_validate_expression/schema.keel b/schema/testdata/validation_expression_validate_expression/schema.keel deleted file mode 100644 index f14048fc0..000000000 --- a/schema/testdata/validation_expression_validate_expression/schema.keel +++ /dev/null @@ -1,11 +0,0 @@ -model Post { - fields { - published Boolean - } - - actions { - create publishPost() with (published) { - @validate(published != "foo") - } - } -} diff --git a/schema/testdata/validation_operation_set_expression_forbidden_operator/errors.json b/schema/testdata/validation_operation_set_expression_forbidden_operator/errors.json index 111cf46bf..400bcda01 100644 --- a/schema/testdata/validation_operation_set_expression_forbidden_operator/errors.json +++ b/schema/testdata/validation_operation_set_expression_forbidden_operator/errors.json @@ -16,6 +16,23 @@ "line": 13, "column": 37 } + }, + { + "message": "The @set attribute cannot be a logical condition and must express an assignment", + "hint": "For example, assign a value to a field on this model with @set(profile.isActive = true)", + "code": "AttributeArgumentError", + "pos": { + "filename": "testdata/validation_operation_set_expression_forbidden_operator/schema.keel", + "offset": 230, + "line": 13, + "column": 18 + }, + "endPos": { + "filename": "testdata/validation_operation_set_expression_forbidden_operator/schema.keel", + "offset": 262, + "line": 13, + "column": 50 + } } ] } \ No newline at end of file diff --git a/schema/testdata/validation_operation_set_forbidden_value_expression/errors.json b/schema/testdata/validation_operation_set_forbidden_value_expression/errors.json index ec66663ba..e8f10ecf3 100644 --- a/schema/testdata/validation_operation_set_forbidden_value_expression/errors.json +++ b/schema/testdata/validation_operation_set_forbidden_value_expression/errors.json @@ -1,9 +1,9 @@ { "errors": [ { - "message": "Value-only conditions such as 'profile.identity' not permitted on @set", - "hint": "Please assign a value. Did you mean 'profile.identity = xxx'?", - "code": "E023", + "message": "The @set attribute cannot be a value condition and must express an assignment", + "hint": "For example, assign a value to a field on this model with @set(profile.isActive = true)", + "code": "AttributeArgumentError", "pos": { "filename": "testdata/validation_operation_set_forbidden_value_expression/schema.keel", "offset": 229, diff --git a/schema/testdata/validation_set_attribute_backlink_repeated_fields/errors.json b/schema/testdata/validation_set_attribute_backlink_repeated_fields/errors.json index db73ca750..5388d5e46 100644 --- a/schema/testdata/validation_set_attribute_backlink_repeated_fields/errors.json +++ b/schema/testdata/validation_set_attribute_backlink_repeated_fields/errors.json @@ -6,14 +6,14 @@ "code": "E031", "pos": { "filename": "testdata/validation_set_attribute_backlink_repeated_fields/schema.keel", - "offset": 201, - "line": 10, + "offset": 246, + "line": 11, "column": 40 }, "endPos": { "filename": "testdata/validation_set_attribute_backlink_repeated_fields/schema.keel", - "offset": 202, - "line": 10, + "offset": 247, + "line": 11, "column": 41 } } diff --git a/schema/testdata/validation_set_attribute_backlink_repeated_fields/schema.keel b/schema/testdata/validation_set_attribute_backlink_repeated_fields/schema.keel index 26e8693b3..155239ac4 100644 --- a/schema/testdata/validation_set_attribute_backlink_repeated_fields/schema.keel +++ b/schema/testdata/validation_set_attribute_backlink_repeated_fields/schema.keel @@ -1,12 +1,13 @@ model Record { fields { + name Text owner User? parent Record? children Record[] } actions { - create createRecordWithChildren() { + create createRecordWithChildren() with (name, children.name) { @set(record.children.owner = ctx.identity.user.records.owner) } } diff --git a/schema/testdata/validation_set_attribute_invalid_assignment_condition/errors.json b/schema/testdata/validation_set_attribute_invalid_assignment_condition/errors.json new file mode 100644 index 000000000..80ddedb32 --- /dev/null +++ b/schema/testdata/validation_set_attribute_invalid_assignment_condition/errors.json @@ -0,0 +1,123 @@ +{ + "errors": [ + { + "message": "Operator '==' not permitted on @set", + "hint": "Did you mean =?", + "code": "E022", + "pos": { + "filename": "testdata/validation_set_attribute_invalid_assignment_condition/schema.keel", + "offset": 532, + "line": 22, + "column": 33 + }, + "endPos": { + "filename": "testdata/validation_set_attribute_invalid_assignment_condition/schema.keel", + "offset": 534, + "line": 22, + "column": 35 + } + }, + { + "message": "The @set attribute cannot be a value condition and must express an assignment", + "hint": "For example, assign a value to a field on this model with @set(post.isActive = true)", + "code": "AttributeArgumentError", + "pos": { + "filename": "testdata/validation_set_attribute_invalid_assignment_condition/schema.keel", + "offset": 187, + "line": 10, + "column": 18 + }, + "endPos": { + "filename": "testdata/validation_set_attribute_invalid_assignment_condition/schema.keel", + "offset": 190, + "line": 10, + "column": 21 + } + }, + { + "message": "The @set attribute cannot be a value condition and must express an assignment", + "hint": "For example, assign a value to a field on this model with @set(post.isActive = true)", + "code": "AttributeArgumentError", + "pos": { + "filename": "testdata/validation_set_attribute_invalid_assignment_condition/schema.keel", + "offset": 273, + "line": 13, + "column": 18 + }, + "endPos": { + "filename": "testdata/validation_set_attribute_invalid_assignment_condition/schema.keel", + "offset": 280, + "line": 13, + "column": 25 + } + }, + { + "message": "The @set attribute cannot be a value condition and must express an assignment", + "hint": "For example, assign a value to a field on this model with @set(post.isActive = true)", + "code": "AttributeArgumentError", + "pos": { + "filename": "testdata/validation_set_attribute_invalid_assignment_condition/schema.keel", + "offset": 360, + "line": 16, + "column": 18 + }, + "endPos": { + "filename": "testdata/validation_set_attribute_invalid_assignment_condition/schema.keel", + "offset": 364, + "line": 16, + "column": 22 + } + }, + { + "message": "The @set attribute cannot be a value condition and must express an assignment", + "hint": "For example, assign a value to a field on this model with @set(post.isActive = true)", + "code": "AttributeArgumentError", + "pos": { + "filename": "testdata/validation_set_attribute_invalid_assignment_condition/schema.keel", + "offset": 434, + "line": 19, + "column": 18 + }, + "endPos": { + "filename": "testdata/validation_set_attribute_invalid_assignment_condition/schema.keel", + "offset": 448, + "line": 19, + "column": 32 + } + }, + { + "message": "The @set attribute cannot be a logical condition and must express an assignment", + "hint": "For example, assign a value to a field on this model with @set(post.isActive = true)", + "code": "AttributeArgumentError", + "pos": { + "filename": "testdata/validation_set_attribute_invalid_assignment_condition/schema.keel", + "offset": 517, + "line": 22, + "column": 18 + }, + "endPos": { + "filename": "testdata/validation_set_attribute_invalid_assignment_condition/schema.keel", + "offset": 539, + "line": 22, + "column": 40 + } + }, + { + "message": "A @set attribute can only consist of a single assignment expression", + "hint": "For example, assign a value to a field on this model with @set(post.isActive = true)", + "code": "AttributeArgumentError", + "pos": { + "filename": "testdata/validation_set_attribute_invalid_assignment_condition/schema.keel", + "offset": 606, + "line": 25, + "column": 18 + }, + "endPos": { + "filename": "testdata/validation_set_attribute_invalid_assignment_condition/schema.keel", + "offset": 651, + "line": 25, + "column": 63 + } + } + ] +} \ No newline at end of file diff --git a/schema/testdata/validation_set_attribute_invalid_assignment_condition/schema.keel b/schema/testdata/validation_set_attribute_invalid_assignment_condition/schema.keel new file mode 100644 index 000000000..09c7af4ca --- /dev/null +++ b/schema/testdata/validation_set_attribute_invalid_assignment_condition/schema.keel @@ -0,0 +1,28 @@ +model Post { + fields { + name Text? + published Boolean? + identity Identity? + } + + actions { + create ctxOnly() with (name, published) { + @set(ctx) + } + create literalOnly() with (name, published) { + @set("hello") + } + create trueOnly() with (name, published) { + @set(true) + } + create fieldOnly() with (name) { + @set(post.published) + } + create equality() with (name) { + @set(post.published == true) + } + create multipleConditions() { + @set(post.published = true and post.name = "hello") + } + } +} diff --git a/schema/testdata/validation_set_attribute_lhs_is_invalid/errors.json b/schema/testdata/validation_set_attribute_lhs_is_invalid/errors.json new file mode 100644 index 000000000..62da122c8 --- /dev/null +++ b/schema/testdata/validation_set_attribute_lhs_is_invalid/errors.json @@ -0,0 +1,140 @@ +{ + "errors": [ + { + "message": "'name' not found", + "hint": "Did you mean ctx, or post?", + "code": "E020", + "pos": { + "filename": "testdata/validation_set_attribute_lhs_is_invalid/schema.keel", + "offset": 197, + "line": 10, + "column": 18 + }, + "endPos": { + "filename": "testdata/validation_set_attribute_lhs_is_invalid/schema.keel", + "offset": 201, + "line": 10, + "column": 22 + } + }, + { + "message": "'n' not found", + "hint": "Did you mean ctx, or post?", + "code": "E020", + "pos": { + "filename": "testdata/validation_set_attribute_lhs_is_invalid/schema.keel", + "offset": 296, + "line": 13, + "column": 18 + }, + "endPos": { + "filename": "testdata/validation_set_attribute_lhs_is_invalid/schema.keel", + "offset": 297, + "line": 13, + "column": 19 + } + }, + { + "message": "'publisher' not found", + "hint": "Did you mean ctx, or post?", + "code": "E020", + "pos": { + "filename": "testdata/validation_set_attribute_lhs_is_invalid/schema.keel", + "offset": 898, + "line": 31, + "column": 18 + }, + "endPos": { + "filename": "testdata/validation_set_attribute_lhs_is_invalid/schema.keel", + "offset": 907, + "line": 31, + "column": 27 + } + }, + { + "message": "The @set attribute can only be used to set model fields", + "hint": "For example, assign a value to a field on this model with @set(post.isActive = true)", + "code": "AttributeArgumentError", + "pos": { + "filename": "testdata/validation_set_attribute_lhs_is_invalid/schema.keel", + "offset": 388, + "line": 16, + "column": 18 + }, + "endPos": { + "filename": "testdata/validation_set_attribute_lhs_is_invalid/schema.keel", + "offset": 395, + "line": 16, + "column": 25 + } + }, + { + "message": "The @set attribute can only be used to set model fields", + "hint": "For example, assign a value to a field on this model with @set(post.isActive = true)", + "code": "AttributeArgumentError", + "pos": { + "filename": "testdata/validation_set_attribute_lhs_is_invalid/schema.keel", + "offset": 483, + "line": 19, + "column": 18 + }, + "endPos": { + "filename": "testdata/validation_set_attribute_lhs_is_invalid/schema.keel", + "offset": 487, + "line": 19, + "column": 22 + } + }, + { + "message": "The @set attribute can only be used to set model fields", + "hint": "For example, assign a value to a field on this model with @set(post.isActive = true)", + "code": "AttributeArgumentError", + "pos": { + "filename": "testdata/validation_set_attribute_lhs_is_invalid/schema.keel", + "offset": 574, + "line": 22, + "column": 18 + }, + "endPos": { + "filename": "testdata/validation_set_attribute_lhs_is_invalid/schema.keel", + "offset": 593, + "line": 22, + "column": 37 + } + }, + { + "message": "The @set attribute can only be used to set model fields", + "hint": "For example, assign a value to a field on this model with @set(post.isActive = true)", + "code": "AttributeArgumentError", + "pos": { + "filename": "testdata/validation_set_attribute_lhs_is_invalid/schema.keel", + "offset": 693, + "line": 25, + "column": 18 + }, + "endPos": { + "filename": "testdata/validation_set_attribute_lhs_is_invalid/schema.keel", + "offset": 705, + "line": 25, + "column": 30 + } + }, + { + "message": "The @set attribute can only be used to set model fields", + "hint": "For example, assign a value to a field on this model with @set(post.isActive = true)", + "code": "AttributeArgumentError", + "pos": { + "filename": "testdata/validation_set_attribute_lhs_is_invalid/schema.keel", + "offset": 809, + "line": 28, + "column": 18 + }, + "endPos": { + "filename": "testdata/validation_set_attribute_lhs_is_invalid/schema.keel", + "offset": 827, + "line": 28, + "column": 36 + } + } + ] +} \ No newline at end of file diff --git a/schema/testdata/validation_set_attribute_lhs_is_invalid/schema.keel b/schema/testdata/validation_set_attribute_lhs_is_invalid/schema.keel new file mode 100644 index 000000000..7b13bf03e --- /dev/null +++ b/schema/testdata/validation_set_attribute_lhs_is_invalid/schema.keel @@ -0,0 +1,40 @@ +model Post { + fields { + name Text? + published Boolean? + identity Identity? + } + + actions { + create unknownIdentifier() with (name, published) { + @set(name = "hello") + } + create namedInput() with (n: Text, published) { + @set(n = post.name) + } + create literal() with (name, published) { + @set("hello" = post.name) + } + create null() with (name, published) { + @set(null = post.name) + } + create ctx() with (name, published) { + @set(ctx.isAuthenticated = post.published) + } + create ctxIdentity() with (name, published) { + @set(ctx.identity = post.identity) + } + create ctxIdentityEmail() with (name, published) { + @set(ctx.identity.email = "email") + } + create anotherModel() { + @set(publisher.name = "email") + } + } +} + +model Publisher { + fields { + name Text + } +} \ No newline at end of file diff --git a/schema/testdata/validation_set_attribute_lhs_not_within_write_scope/errors.json b/schema/testdata/validation_set_attribute_lhs_not_within_write_scope/errors.json new file mode 100644 index 000000000..1d707eed2 --- /dev/null +++ b/schema/testdata/validation_set_attribute_lhs_not_within_write_scope/errors.json @@ -0,0 +1,106 @@ +{ + "errors": [ + { + "message": "Cannot set a field which is beyond scope of the data being created or updated", + "hint": "Use a field which is part of a model being created or updated within this action's inputs", + "code": "AttributeArgumentError", + "pos": { + "filename": "testdata/validation_set_attribute_lhs_not_within_write_scope/schema.keel", + "offset": 209, + "line": 11, + "column": 18 + }, + "endPos": { + "filename": "testdata/validation_set_attribute_lhs_not_within_write_scope/schema.keel", + "offset": 228, + "line": 11, + "column": 37 + } + }, + { + "message": "Cannot set a field which is beyond scope of the data being created or updated", + "hint": "Use a field which is part of a model being created or updated within this action's inputs", + "code": "AttributeArgumentError", + "pos": { + "filename": "testdata/validation_set_attribute_lhs_not_within_write_scope/schema.keel", + "offset": 313, + "line": 14, + "column": 18 + }, + "endPos": { + "filename": "testdata/validation_set_attribute_lhs_not_within_write_scope/schema.keel", + "offset": 333, + "line": 14, + "column": 38 + } + }, + { + "message": "Cannot set a field which is beyond scope of the data being created or updated", + "hint": "Use a field which is part of a model being created or updated within this action's inputs", + "code": "AttributeArgumentError", + "pos": { + "filename": "testdata/validation_set_attribute_lhs_not_within_write_scope/schema.keel", + "offset": 541, + "line": 21, + "column": 18 + }, + "endPos": { + "filename": "testdata/validation_set_attribute_lhs_not_within_write_scope/schema.keel", + "offset": 568, + "line": 21, + "column": 45 + } + }, + { + "message": "Cannot set a field which is beyond scope of the data being created or updated", + "hint": "Use a field which is part of a model being created or updated within this action's inputs", + "code": "AttributeArgumentError", + "pos": { + "filename": "testdata/validation_set_attribute_lhs_not_within_write_scope/schema.keel", + "offset": 744, + "line": 27, + "column": 18 + }, + "endPos": { + "filename": "testdata/validation_set_attribute_lhs_not_within_write_scope/schema.keel", + "offset": 771, + "line": 27, + "column": 45 + } + }, + { + "message": "Cannot set a field which is beyond scope of the data being created or updated", + "hint": "Use a field which is part of a model being created or updated within this action's inputs", + "code": "AttributeArgumentError", + "pos": { + "filename": "testdata/validation_set_attribute_lhs_not_within_write_scope/schema.keel", + "offset": 876, + "line": 30, + "column": 18 + }, + "endPos": { + "filename": "testdata/validation_set_attribute_lhs_not_within_write_scope/schema.keel", + "offset": 907, + "line": 30, + "column": 49 + } + }, + { + "message": "Cannot set a field which is beyond scope of the data being created or updated", + "hint": "Use a field which is part of a model being created or updated within this action's inputs", + "code": "AttributeArgumentError", + "pos": { + "filename": "testdata/validation_set_attribute_lhs_not_within_write_scope/schema.keel", + "offset": 999, + "line": 33, + "column": 18 + }, + "endPos": { + "filename": "testdata/validation_set_attribute_lhs_not_within_write_scope/schema.keel", + "offset": 1022, + "line": 33, + "column": 41 + } + } + ] +} \ No newline at end of file diff --git a/schema/testdata/validation_set_attribute_lhs_not_within_write_scope/schema.keel b/schema/testdata/validation_set_attribute_lhs_not_within_write_scope/schema.keel new file mode 100644 index 000000000..73ca0094f --- /dev/null +++ b/schema/testdata/validation_set_attribute_lhs_not_within_write_scope/schema.keel @@ -0,0 +1,69 @@ +model Post { + fields { + name Text? + published Boolean? + identity Identity? + publisher Publisher? + } + + actions { + create nestedData1() with (name) { + @set(post.publisher.name = "someName") + } + create nestedData2() with (name) { + @set(post.publisher.admin = ctx.identity) + } + create nestedData3() with ( + publisher.country.id, + publisher.departments.name, + publisher.departments.number, + ) { + @set(post.publisher.country.name = "some country") + } + create nestedData4() with ( + publisher.departments.name, + publisher.departments.number, + ) { + @set(post.publisher.country.name = "some country") + } + create nestedData5() with (publisher.country.id) { + @set(post.publisher.departments.name = "some department") + } + create nestedData6() with (name) { + @set(post.publisher.admin.id = ctx.identity.id) + } + } +} + +model Publisher { + fields { + name Text + owner Person + departments Department[] + country Country + admin Identity @unique + } +} + +model Department { + fields { + publisher Publisher + head Person? + name Text + number Text + } +} + +model Country { + fields { + name Text? + publishersCount Number? + } +} + +model Person { + fields { + name Text + identity Identity @unique + } +} diff --git a/schema/testdata/validation_set_expression_single_boolean_literal/errors.json b/schema/testdata/validation_set_expression_single_boolean_literal/errors.json deleted file mode 100644 index d25fc47e0..000000000 --- a/schema/testdata/validation_set_expression_single_boolean_literal/errors.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "errors": [ - { - "message": "Value-only conditions such as 'true' not permitted on @set", - "hint": "Please assign a value. Did you mean 'true = xxx'?", - "code": "E023", - "pos": { - "filename": "testdata/validation_set_expression_single_boolean_literal/schema.keel", - "offset": 118, - "line": 8, - "column": 12 - }, - "endPos": { - "filename": "testdata/validation_set_expression_single_boolean_literal/schema.keel", - "offset": 122, - "line": 8, - "column": 16 - } - } - ] -} \ No newline at end of file diff --git a/schema/testdata/validation_set_expression_single_boolean_literal/schema.keel b/schema/testdata/validation_set_expression_single_boolean_literal/schema.keel deleted file mode 100644 index 79cd3905c..000000000 --- a/schema/testdata/validation_set_expression_single_boolean_literal/schema.keel +++ /dev/null @@ -1,11 +0,0 @@ -model Post { - fields { - published Boolean - } - - actions { - create publishPost() with (published) { - @set(true) - } - } -} diff --git a/schema/validation/errorhandling/errors.go b/schema/validation/errorhandling/errors.go index 1c7082923..b9d9f28c6 100644 --- a/schema/validation/errorhandling/errors.go +++ b/schema/validation/errorhandling/errors.go @@ -32,7 +32,6 @@ const ( ErrorUniqueEnumGlobally = "E019" ErrorUnresolvableExpression = "E020" ErrorForbiddenExpressionAction = "E022" - ErrorForbiddenValueCondition = "E023" ErrorIncorrectArguments = "E024" ErrorInvalidSyntax = "E025" ErrorExpressionTypeMismatch = "E026" diff --git a/schema/validation/errorhandling/errors.yml b/schema/validation/errorhandling/errors.yml index f4d460306..15867df7f 100644 --- a/schema/validation/errorhandling/errors.yml +++ b/schema/validation/errorhandling/errors.yml @@ -59,9 +59,6 @@ en: E022: message: "Operator '{{ .Operator }}' not permitted on {{ .Attribute }}" hint: "{{ .Suggestion }}" - E023: - message: "Value-only conditions such as '{{ .Value }}' not permitted on {{ .Attribute }}" - hint: "Please assign a value. Did you mean '{{ .Suggestion }}'?" E024: message: "{{ .ActualArgsCount }} argument(s) provided to @{{ .AttributeName }} but expected {{ .ExpectedArgsCount }}" hint: '{{ if eq .Signature "()" }}@{{ .AttributeName }} doesn''t accept any arguments{{ else }}the signature of this attribute is @{{ .AttributeName }}{{ .Signature }}{{ end }}' diff --git a/schema/validation/attribute_arguments.go b/schema/validation/permission_attribute_arguments.go similarity index 98% rename from schema/validation/attribute_arguments.go rename to schema/validation/permission_attribute_arguments.go index 434317b7f..74d2f2dac 100644 --- a/schema/validation/attribute_arguments.go +++ b/schema/validation/permission_attribute_arguments.go @@ -11,7 +11,7 @@ import ( ) // Validates the arguments to any attribute expression. -func AttributeArgumentsRule(asts []*parser.AST, errs *errorhandling.ValidationErrors) Visitor { +func PermissionsAttributeArguments(asts []*parser.AST, errs *errorhandling.ValidationErrors) Visitor { var model *parser.ModelNode var action *parser.ActionNode var job *parser.JobNode diff --git a/schema/validation/rules/actions/create_required.go b/schema/validation/rules/actions/create_required.go index 773f21e4e..d0a2d0a5d 100644 --- a/schema/validation/rules/actions/create_required.go +++ b/schema/validation/rules/actions/create_required.go @@ -265,6 +265,10 @@ func satisfiedBySetExpr(rootModelName string, dotDelimPath string, modelName str } lhs := assignment.LHS + if lhs.Ident == nil { + continue + } + fragStrings := lo.Map(lhs.Ident.Fragments, func(frag *parser.IdentFragment, _ int) string { return frag.Fragment }) diff --git a/schema/validation/rules/attribute/attribute.go b/schema/validation/rules/attribute/attribute.go index fc31eaa8c..b41ff7505 100644 --- a/schema/validation/rules/attribute/attribute.go +++ b/schema/validation/rules/attribute/attribute.go @@ -113,24 +113,6 @@ func checkAttributes(attributes []*parser.AttributeNode, definedOn string, paren return } -func ValidateActionAttributeRule(asts []*parser.AST) (errs errorhandling.ValidationErrors) { - for _, model := range query.Models(asts) { - for _, action := range query.ModelActions(model) { - for _, attr := range action.Attributes { - if attr.Name.Value != parser.AttributeValidate { - continue - } - - errs.Concat( - validateActionAttributeWithExpression(asts, model, action, attr), - ) - } - } - } - - return -} - func ValidateFieldAttributeRule(asts []*parser.AST) (errs errorhandling.ValidationErrors) { for _, model := range query.Models(asts) { for _, field := range query.ModelFields(model) { @@ -251,7 +233,6 @@ func validateActionAttributeWithExpression( if attr.Name.Value == parser.AttributeSet { rules = append(rules, expression.OperatorAssignmentRule) - rules = append(rules, expression.PreventValueConditionRule) } else { rules = append(rules, expression.OperatorLogicalRule) } diff --git a/schema/validation/rules/expression/expression.go b/schema/validation/rules/expression/expression.go index 390feb5a2..f0f3cec95 100644 --- a/schema/validation/rules/expression/expression.go +++ b/schema/validation/rules/expression/expression.go @@ -190,31 +190,6 @@ func OperatorLogicalRule(asts []*parser.AST, expression *parser.Expression, cont return errors } -// Validates that no value conditions are used -func PreventValueConditionRule(asts []*parser.AST, expression *parser.Expression, context expressions.ExpressionContext) (errors []error) { - conditions := expression.Conditions() - - for _, condition := range conditions { - if condition.Type() == parser.ValueCondition { - errors = append(errors, - errorhandling.NewValidationError( - errorhandling.ErrorForbiddenValueCondition, - errorhandling.TemplateLiterals{ - Literals: map[string]string{ - "Value": condition.ToString(), - "Attribute": fmt.Sprintf("@%s", context.Attribute.Name.Value), - "Suggestion": fmt.Sprintf("%s = xxx", condition.ToString()), - }, - }, - condition, - ), - ) - } - } - - return errors -} - func InvalidOperatorForOperandsRule(asts []*parser.AST, condition *parser.Condition, context expressions.ExpressionContext, permittedOperators []string) (errors []error) { resolver := expressions.NewConditionResolver(condition, asts, &context) resolvedLHS, resolvedRHS, _ := resolver.Resolve() diff --git a/schema/validation/set_attribute_expression.go b/schema/validation/set_attribute_expression.go new file mode 100644 index 000000000..3342d88e9 --- /dev/null +++ b/schema/validation/set_attribute_expression.go @@ -0,0 +1,189 @@ +package validation + +import ( + "fmt" + + "github.com/iancoleman/strcase" + "github.com/samber/lo" + "github.com/teamkeel/keel/schema/expressions" + "github.com/teamkeel/keel/schema/node" + "github.com/teamkeel/keel/schema/parser" + "github.com/teamkeel/keel/schema/query" + "github.com/teamkeel/keel/schema/validation/errorhandling" +) + +func SetAttributeExpressionRules(asts []*parser.AST, errs *errorhandling.ValidationErrors) Visitor { + var model *parser.ModelNode + var action *parser.ActionNode + + return Visitor{ + EnterModel: func(m *parser.ModelNode) { + model = m + }, + LeaveModel: func(_ *parser.ModelNode) { + model = nil + }, + EnterAction: func(a *parser.ActionNode) { + action = a + }, + LeaveAction: func(_ *parser.ActionNode) { + action = nil + }, + EnterAttribute: func(attribute *parser.AttributeNode) { + if attribute == nil || attribute.Name.Value != parser.AttributeSet { + return + } + + if len(attribute.Arguments) != 1 || attribute.Arguments[0].Expression == nil { + return + } + + conditions := attribute.Arguments[0].Expression.Conditions() + + if len(conditions) > 1 { + errs.AppendError(makeSetExpressionError( + "A @set attribute can only consist of a single assignment expression", + fmt.Sprintf("For example, assign a value to a field on this model with @set(%s.isActive = true)", strcase.ToLowerCamel(model.Name.Value)), + attribute.Arguments[0].Expression, + )) + return + } + + expressionContext := expressions.ExpressionContext{ + Model: model, + Attribute: attribute, + Action: action, + } + + if conditions[0].Type() == parser.ValueCondition { + errs.AppendError(makeSetExpressionError( + "The @set attribute cannot be a value condition and must express an assignment", + fmt.Sprintf("For example, assign a value to a field on this model with @set(%s.isActive = true)", strcase.ToLowerCamel(model.Name.Value)), + conditions[0], + )) + return + } + + if conditions[0].Type() == parser.LogicalCondition { + errs.AppendError(makeSetExpressionError( + "The @set attribute cannot be a logical condition and must express an assignment", + fmt.Sprintf("For example, assign a value to a field on this model with @set(%s.isActive = true)", strcase.ToLowerCamel(model.Name.Value)), + conditions[0], + )) + return + } + + // We resolve whether the actual fragments exist somewhere else, + // but we need to exit here if they dont resolve. + resolver := expressions.NewConditionResolver(conditions[0], asts, &expressionContext) + _, _, err := resolver.Resolve() + if err != nil { + return + } + + lhs := conditions[0].LHS + + if lhs.Ident == nil { + errs.AppendError(makeSetExpressionError( + "The @set attribute can only be used to set model fields", + fmt.Sprintf("For example, assign a value to a field on this model with @set(%s.isActive = true)", strcase.ToLowerCamel(model.Name.Value)), + lhs, + )) + return + } + + // Drop the 'id' at the end if it exists + fragments := []*parser.IdentFragment{} + for _, fragment := range lhs.Ident.Fragments { + if fragment.Fragment != "id" { + fragments = append(fragments, fragment) + } + } + + incompatableInputs := []*parser.ActionInputNode{} + currentModel := model + var currentField *parser.FieldNode + + // Iterate through each fragment in the LHS operand, and ensure: + // - is starts at the root model + // - it is a field which is part of a model being created or updated (including nested creates) + for i, fragment := range fragments { + if i == 0 && fragment.Fragment != strcase.ToLowerCamel(model.Name.Value) { + errs.AppendError(makeSetExpressionError( + "The @set attribute can only be used to set model fields", + fmt.Sprintf("For example, assign a value to a field on this model with @set(%s.isActive = true)", strcase.ToLowerCamel(model.Name.Value)), + lhs, + )) + return + } + + if i > 0 { + currentField = query.ModelField(currentModel, fragment.Fragment) + currentModel = query.Model(asts, currentField.Type.Value) + } + + // The purpose of this part is to check that the nested field being set + // is part of the nested create inputs. You can set any field within the models + // being created. You cannot set fields on models which already reside in the database. + //if i > 1 { + if i < len(fragments)-1 { + + withinWriteScope := false + + if i < 2 { + withinWriteScope = true + } + + for _, input := range action.With { + if input.Label != nil { + continue + } + + if lo.Contains(incompatableInputs, input) { + continue + } + + if i > len(input.Type.Fragments)-1 { + withinWriteScope = i == len(fragments)-1 + continue + } + + if i == 0 { + withinWriteScope = true + continue + } + + if lhs.Ident.Fragments[i].Fragment == input.Type.Fragments[i-1].Fragment { + if input.Type.Fragments[i].Fragment != "id" { + withinWriteScope = true + } + } else { + incompatableInputs = append(incompatableInputs, input) + } + } + + if !withinWriteScope { + errs.AppendError(makeSetExpressionError( + "Cannot set a field which is beyond scope of the data being created or updated", + "Use a field which is part of a model being created or updated within this action's inputs", + lhs, + )) + return + } + } + } + + }, + } +} + +func makeSetExpressionError(message string, hint string, node node.ParserNode) *errorhandling.ValidationError { + return errorhandling.NewValidationErrorWithDetails( + errorhandling.AttributeArgumentError, + errorhandling.ErrorDetails{ + Message: message, + Hint: hint, + }, + node, + ) +} diff --git a/schema/validation/validation.go b/schema/validation/validation.go index e04fe88dd..a9d4a547f 100644 --- a/schema/validation/validation.go +++ b/schema/validation/validation.go @@ -45,7 +45,6 @@ var validatorFuncs = []validationFunc{ attribute.AttributeLocationsRule, attribute.SetWhereAttributeRule, - attribute.ValidateActionAttributeRule, attribute.ValidateFieldAttributeRule, attribute.UniqueAttributeArgsRule, @@ -70,10 +69,11 @@ var visitorFuncs = []VisitorFunc{ UniqueAttributeRule, OrderByAttributeRule, SortableAttributeRule, + SetAttributeExpressionRules, Jobs, ScheduleAttributeRule, DuplicateInputsRule, - AttributeArgumentsRule, + PermissionsAttributeArguments, FunctionDisallowedBehavioursRule, OnAttributeRule, RelationshipsRules,