Auto API¶
Uvicore has an automatic CRUD Model Router for API usage which can be enabled in your http/routes/api.py
like so
# http/routes/api.py
def register(self, route: ApiRouter):
# ...
# Include dynamic model CRUD API endpoints (the "auto API")!
# These routes are automatically protected by model.crud style permissions.
route.include(ModelRouter)
# ...
For every uvicore model, multiple API endpoints are automatically created to perform CRUD operations on the list of models via /users
or a single model via /users/{id}
. You can see these new routes when viewing the OpenAPI doc endpoint at http://localhost:5000/api/docs
Permissions¶
When a uvicore model is created, a set of model.crud
style permissions are automatically generated and stored in the permissions
database table. For example
entity | name |
---|---|
users | users.create |
users | users.read |
users | users.update |
users | users.delete |
When visiting GET
http://localhost:5000/api/users
the user must have the users.read
scope. Scopes are the same as these permission strings.
Note
Permissions and Scopes are the same thing. You limit all endpoints using the scopes=[]
List which are linked to the user/groups/roles
in the database as permissions
.
Each auto API endpoint is limited by the proper scope.
HTTP GET
requires theusers.read
scope.HTTP POST
requires theusers.create
scope.HTTP PUT
requires theusers.update
scope.HTTP PATCH
requires theusers.update
scope.HTTP DELETE
requires theusers.delete
scope.
Making Some Endpoints Public¶
If you want some auto API endpoints public and some private, even down to public GET vs POST you should keep the automatic CRUD scopes enabled and instead link UserID 1 (the anonymous user) to a role and link up the proper permissions for that role. Public users are actually assigned a real uvicore user called anonymous, so user groups, roles and permissions apply to that public anonymous user just like any other user.
Make it all Public¶
If you wanted ALL auto API endpoints to be wide open to the public, with no limiting scopes (permissions) at all, use the options
parameter and set the scopes
key to a blank List.
# http/routes/api.py
def register(self, route: ApiRouter):
# ...
# Include dynamic model CRUD API endpoints (the "auto API")!
# These routes are automatically protected by model.crud style permissions.
route.include(ModelRouter, options={
'scopes': []
})
# ...
Tip
Although the auto API endpoints now have no scopes themselves, they still obey any higher order scopes
you may have defined in your top level router or controller files.
Custom scopes without CRUD scopes¶
If you wanted to remove the automatic CRUD scopes (permissions) from all auto API endpoints and instead define your own List of scopes for all endpoints.
# http/routes/api.py
def register(self, route: ApiRouter):
# ...
# Include dynamic model CRUD API endpoints (the "auto API")!
# These routes are automatically protected by model.crud style permissions.
route.include(ModelRouter, options={
'scopes': ['authenticated', 'autoapi_user']
})
# ...
Notice these scopes apply to every single endpoint. User must be authenticated and have the autoapi_user
permission. They can hit ALL auto API endpoints with all HTTP verbs.
This is only handy if you want to give out ALL functionality to a set of users. This is not a granular per HTTP verb approach. To define a custom set of permissions for each HTTP verb use a scopes dictionary instead. Like so
# http/routes/api.py
def register(self, route: ApiRouter):
# ...
# Include dynamic model CRUD API endpoints (the "auto API")!
# These routes are automatically protected by model.crud style permissions.
route.include(ModelRouter, options={
'scopes': {
'create': ['autoapi.create'],
'read': ['autoapi.read'],
'update': ['autoapi.update'],
'delete': ['autoapi.delete'],
}
})
# ...
Although this is granular from an HTTP verb standpoint, it still applies to every single endpoint.
Tip
Any higher order scopes
defined in top level routes or controllers are also obeyed
Extend existing CRUD scopes¶
If you wanted to extend/append your own scopes to the existing automatic model.crud
styles scopes, wrap it in a @route.group
decorator
# http/routes/api.py
def register(self, route: ApiRouter):
# ...
# Include dynamic model CRUD API endpoints (the "auto API")!
# These routes are automatically protected by model.crud style permissions.
@route.group(scopes=['authenticated', 'autoapi_user']):
def autoapi():
route.include(ModelRouter)
# ...
users.read
) and also the authenticated
and autoapi_user
scope.
Tip
Any higher order scopes
defined in top level routes or controllers are also obeyed
Notes¶
Notes from within the model_router.py, moved here
REST Notes
There are certain verbs which must NOT carry a body/payload. Instead they act on a single resource defined in the URL. These verbs are GET/HEAD/DELETE/OPTIONS. Which means you can only DELETE a /{id} resource, never bulk with some sort of DELETE body payload. Body in DELETE is technically allowed, but is generally ignored by clients, proxies and should not be used. Get obviously has no body whatsoever, only URL and possibly queryParameters.
encode/httpx for example does not allow body data on GET/HEAD/DELETE/OPTIONS as expected.
Further, some verbs that do except body payload should still only ever act on a single resource defined in the url /{id}. Like PUT. PUT should never do bulk inserts. PUT acts on a single resource to UPDATE that resource using the URL /{id}. PUT can have a body/payload, which is the FULL resource to UPDATE.
At first I created my DELETE to accept
https://www.restapitutorial.com/lessons/httpmethods.html
PUT should be idempotent always, if it increments a counter, its NOT idempotent and POST should be used
GET
Has NO body/payload
/user To get entire collection
/user/{id} to get a single item
queryParams are OK on either / or /{id}
POST - creating, but also a catch-all verb
Has body/payload
/user To create a new user (not idempotent), body can be one or many items
/user/delete Custom, can have a body payload with complex query of WHAT to delete
https://stackoverflow.com/questions/299628/is-an-entity-body-allowed-for-an-http-delete-request
https://lists.w3.org/Archives/Public/ietf-http-wg/2020JanMar/0123.html
https://stackoverflow.com/questions/21863326/delete-multiple-records-using-rest
https://www.mscharhag.com/p/rest-api-design
https://www.mscharhag.com/api-design/http-post-put-patch
GET
HEAD
OPTIONS
TRACE
POST is for new records
POST /spaces
Not idempotent as it will continue to create new resources
If new post, return 200
If endpoint has no response but created, return 204 (no content but success)
PUT is for updating existing records whos ID is in the url
PUT /spaces/123
Not for partial updates, expects the FULL object
Idempotent
PATCH
PATCH /spaces/123
Like PUT, but can be partial object, or could be full, either way
Updates only records defined in the partial object
{
'creator': 4
}
BULK updates?
PATCH /spaces?where={"something": "other"}
Takes a partial JSON blob, with the data you want to update in BULK
{
'something': 'new',
'title': 'all get the same title'
}
DELETE
DELETE /spaces/123
Deletes one space by ID
maybe a new permission string spaces.update_bulk?
BULK?
DELETE /spaces?where={"creator_id": 1}
maybe a body with confirm code?
{
env API_BULK_DELETE_CODE: 1234123412341234123412341234
confirm: yes sir code 123423412341234
}
or maybe a new permission string, spaces.delete_bulk?
URL query notes
Include
-------
include=sections.topics
Where AND
----------
where={"id": 1}
where={"id": 1, "name": "test"}
where={"id": [">", 1]}
where={"id": ["in", ["one", "two"]]}
where={"id": [">", 5], "name": "asdf", "email": ["like", "asdf"]}
Where OR
--------
or_where=(id,1)+(id,3)
Group By
--------
Order By
--------
order_by=[{"id": "ASC"}, {"name": "DESC"}]
Paging
------
page=1
size=10
translates to ORM limit and offset
Cache
-----
cache=60 in seconds