Skip to content
Snippets Groups Projects
Commit 5efa00a42260 authored by Frank Bessou's avatar Frank Bessou :spider_web:
Browse files

feat: add a "transaction" route

parent 4b108a84c899
No related branches found
No related tags found
1 merge request!51Add a new API for transactions
Pipeline #201150 passed with warnings
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
from yams import ValidationError, UnknownType from yams import ValidationError, UnknownType
from cubicweb_api.httperrors import get_http_error, get_http_500_error from cubicweb_api.httperrors import get_http_error, get_http_500_error
from cubicweb_api.transaction import InvalidTransaction
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -24,7 +25,7 @@ ...@@ -24,7 +25,7 @@
# RQL errors -> 400 # RQL errors -> 400
if isinstance( if isinstance(
wrapped_exception, wrapped_exception,
(ValidationError, QueryError, UnknownType, RQLException), (ValidationError, QueryError, UnknownType, RQLException, InvalidTransaction),
): ):
return get_http_error( return get_http_error(
400, 400,
......
...@@ -26,6 +26,7 @@ ...@@ -26,6 +26,7 @@
post: post:
description: Executes the given RQL query description: Executes the given RQL query
requestBody: requestBody:
required: true
content: content:
application/json: application/json:
schema: schema:
...@@ -36,18 +37,10 @@ ...@@ -36,18 +37,10 @@
content: content:
application/json: application/json:
schema: schema:
type: array $ref: '#/components/schemas/ResultSet'
items:
type: array
items:
oneOf:
- type: 'string'
nullable: true
- type: 'number'
- type: 'boolean'
'400': '400':
description: The given RQL was badly formatted description: The given RQL was badly formatted
content: content:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/ErrorSchema' $ref: '#/components/schemas/ErrorSchema'
...@@ -48,9 +41,30 @@ ...@@ -48,9 +41,30 @@
'400': '400':
description: The given RQL was badly formatted description: The given RQL was badly formatted
content: content:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/ErrorSchema' $ref: '#/components/schemas/ErrorSchema'
/transaction:
x-pyramid-route-name: transaction
post:
description: Execute several queries in a single transaction
requestBody:
required: true
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/RqlTransactionParams'
responses:
'200':
description: This instance's Schema
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/ResultSet'
/login: /login:
x-pyramid-route-name: login x-pyramid-route-name: login
post: post:
...@@ -135,6 +149,6 @@ ...@@ -135,6 +149,6 @@
type: object type: object
default: {} default: {}
additionalProperties: additionalProperties:
nullable: true $ref: '#/components/schemas/RqlParamsValue'
required: required:
- query - query
...@@ -139,5 +153,39 @@ ...@@ -139,5 +153,39 @@
required: required:
- query - query
RqlParamsValue:
oneOf:
- type: string
nullable: true
- type: number
- type: boolean
RqlTransactionParams:
type: object
properties:
query:
type: string
minLength: 1
params:
type: object
default: {}
additionalProperties:
$ref: '#/components/schemas/RqlTransactionParamsValue'
required:
- query
RqlTransactionParamsValue:
oneOf:
- type: string
nullable: true
- type: number
- type: boolean
- type: object
properties:
queryIndex:
type: number
row:
type: number
column:
type: number
additionalProperties: false
LoginParams: LoginParams:
type: object type: object
properties: properties:
...@@ -173,6 +221,16 @@ ...@@ -173,6 +221,16 @@
- code - code
- title - title
- error - error
ResultSet:
type: array
items:
type: array
items:
oneOf:
- type: 'string'
nullable: true
- type: 'number'
- type: 'boolean'
CurrentUser: CurrentUser:
type: object type: object
nullable: true nullable: true
......
...@@ -30,6 +30,7 @@ ...@@ -30,6 +30,7 @@
) )
from cubicweb_api.openapi.openapi import setup_openapi from cubicweb_api.openapi.openapi import setup_openapi
from cubicweb_api.util import get_cw_repo from cubicweb_api.util import get_cw_repo
from cubicweb_api.transaction import Transaction
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -41,6 +42,7 @@ ...@@ -41,6 +42,7 @@
schema = "schema" schema = "schema"
rql = "rql" rql = "rql"
transaction = "transaction"
login = "login" login = "login"
current_user = "current_user" current_user = "current_user"
help = "help" help = "help"
...@@ -48,8 +50,8 @@ ...@@ -48,8 +50,8 @@
def get_route_name(route_name: ApiRoutes) -> str: def get_route_name(route_name: ApiRoutes) -> str:
""" """
Generates a unique route name using the api prefix to prevent clashes with routes Generates a unique route name using the api
from other cubes. prefix to prevent clashes with routes from other cubes.
:param route_name: The route name base :param route_name: The route name base
:return: The generated route name :return: The generated route name
...@@ -75,7 +77,8 @@ ...@@ -75,7 +77,8 @@
) )
def schema_route(self): def schema_route(self):
""" """
See the openapi/openapi_template.yml file for more information on this route. See the openapi/openapi_template.yml
file for more information about this route.
""" """
repo = get_cw_repo(self.request) repo = get_cw_repo(self.request)
exporter = JSONSchemaExporter() exporter = JSONSchemaExporter()
...@@ -85,7 +88,8 @@ ...@@ -85,7 +88,8 @@
@view_config(route_name=get_route_name(ApiRoutes.rql), anonymous_or_connected=True) @view_config(route_name=get_route_name(ApiRoutes.rql), anonymous_or_connected=True)
def rql_route(self): def rql_route(self):
""" """
See the openapi/openapi_template.yml file for more information on this route. See the openapi/openapi_template.yml
file for more information about this route.
""" """
request_params = self.request.openapi_validated.body request_params = self.request.openapi_validated.body
query: str = request_params["query"] query: str = request_params["query"]
...@@ -95,7 +99,22 @@ ...@@ -95,7 +99,22 @@
return rset return rset
@view_config( @view_config(
route_name=get_route_name(ApiRoutes.transaction),
request_method="POST",
anonymous_or_connected=True,
)
def transaction_view(self):
"""
See the openapi/openapi_template.yml
file for more information about this route.
"""
queries = self.request.openapi_validated.body
transaction = Transaction(queries)
rsets = [rset.rows for rset in transaction.execute(self.request.cw_cnx)]
return rsets
@view_config(
route_name=get_route_name(ApiRoutes.login), route_name=get_route_name(ApiRoutes.login),
) )
def login_route(self): def login_route(self):
""" """
...@@ -98,8 +117,9 @@ ...@@ -98,8 +117,9 @@
route_name=get_route_name(ApiRoutes.login), route_name=get_route_name(ApiRoutes.login),
) )
def login_route(self): def login_route(self):
""" """
See the openapi/openapi_template.yml file for more information on this route. See the openapi/openapi_template.yml
file for more information about this route.
""" """
request_params = self.request.openapi_validated.body request_params = self.request.openapi_validated.body
login: str = request_params["login"] login: str = request_params["login"]
...@@ -125,7 +145,8 @@ ...@@ -125,7 +145,8 @@
) )
def current_user(self): def current_user(self):
""" """
See the openapi/openapi_template.yml file for more information on this route. See the openapi/openapi_template.yml
file for more information about this route.
""" """
user = self.request.cw_cnx.user user = self.request.cw_cnx.user
return {"eid": user.eid, "login": user.login, "dcTitle": user.dc_title()} return {"eid": user.eid, "login": user.login, "dcTitle": user.dc_title()}
......
...@@ -122,6 +122,94 @@ ...@@ -122,6 +122,94 @@
"title": "Unauthorized", "title": "Unauthorized",
} }
def test_200_on_transaction_when_authenticated(self):
self.webapp.post(
f"{BASE_URL[:-1]}{API_PATH_DEFAULT_PREFIX}/v1/login",
params=json.dumps({"login": self.admlogin, "password": self.admpassword}),
content_type="application/json",
status=204,
)
queries = [
{
"query": "INSERT CWUser U: U login %(login)s, U upassword 'AZJEJAZO'",
"params": {"login": "ginger"},
},
{
"query": "INSERT CWGroup G: G name %(name)s",
"params": {"name": "chickens"},
},
{
"query": "SET U in_group G WHERE U eid %(ginger_eid)s, G eid %(chickens_eid)s",
"params": {
"ginger_eid": {"queryIndex": 0, "row": 0, "column": 0},
"chickens_eid": {"queryIndex": 1, "row": 0, "column": 0},
},
},
]
response = self.webapp.post(
f"{BASE_URL[:-1]}{API_PATH_DEFAULT_PREFIX}/v1/transaction",
params=json.dumps(queries),
content_type="application/json",
status=200,
).json
assert len(response) == 3
assert isinstance(response[0][0][0], int)
assert isinstance(response[1][0][0], int)
assert isinstance(response[2][0][0], int)
assert isinstance(response[2][0][1], int)
def test_400_on_invalid_transactions(self):
queries = [
{
"query": "INSERT CWUser U: U login %(login)s, U upassword 'AZJEJAZO'",
"params": {"login": {"queryIndex": 0, "row": 0, "column": 0}},
},
]
response = self.webapp.post(
f"{BASE_URL[:-1]}{API_PATH_DEFAULT_PREFIX}/v1/transaction",
params=json.dumps(queries),
content_type="application/json",
status=400,
).json
assert response == {
"message": "A query reference index refers to a request which has not yet "
"been executed",
"data": None,
"title": "InvalidTransaction",
}
def test_400_on_invalid_transactions_query_index(self):
queries = [
{
"query": "INSERT CWUser U: U login %(login)s, U upassword 'AZJEJAZO'",
"params": {
"login": {"queryIndex": "not a number", "row": 0, "column": 0}
},
},
]
response = self.webapp.post(
f"{BASE_URL[:-1]}{API_PATH_DEFAULT_PREFIX}/v1/transaction",
params=json.dumps(queries),
content_type="application/json",
status=400,
).json
assert response == {
"data": [
{
"exception": "ValidationError",
"message": "{'queryIndex': 'not a number', 'row': 0, 'column': 0} is not "
"valid under any of the given schemas",
}
],
"message": "Your request could not be validated against the openapi "
"specification.",
"title": "OpenApiValidationError",
}
def test_successful_login_returns_204(self): def test_successful_login_returns_204(self):
self.webapp.post( self.webapp.post(
f"{BASE_URL[:-1]}{API_PATH_DEFAULT_PREFIX}/v1/login", f"{BASE_URL[:-1]}{API_PATH_DEFAULT_PREFIX}/v1/login",
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment