Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[CZID-8379] Mutations for creating entities #32

Merged
merged 10 commits into from
Aug 18, 2023
2 changes: 1 addition & 1 deletion entities/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ local-gqlschema: ## Export this app's GQL schema.

.PHONY: local-token
local-token: ## Copy an auth token for this local dev env to the system clipboard
TOKEN=$$($(docker_compose) run entities ./cli/gqlcli.py auth generate-token 111 --project 444:admin --expiration 99999); echo $$TOKEN | pbcopy; echo $$TOKEN
TOKEN=$$($(docker_compose) run entities ./cli/gqlcli.py auth generate-token 111 --project 444:admin --expiration 99999); echo '{"Authorization":"Bearer '$$TOKEN'"}' | tee >(pbcopy)
robertaboukhalil marked this conversation as resolved.
Show resolved Hide resolved

.PHONY: fix-poetry-lock
fix-poetry-lock: ## Fix poetry lockfile after merge conflict & repairing pyproject.toml
Expand Down
72 changes: 51 additions & 21 deletions entities/api/core/gql_loaders.py
Original file line number Diff line number Diff line change
@@ -1,39 +1,29 @@
import uuid
import typing
import strawberry
import database.models as db
from collections import defaultdict
from typing import Any, Mapping, Tuple, Optional

from database.models import Base
from sqlalchemy import tuple_
from sqlalchemy import ColumnElement, ColumnExpressionArgument, tuple_
from sqlalchemy.orm import RelationshipProperty
from sqlalchemy.ext.asyncio import AsyncSession
from strawberry.dataloader import DataLoader
from cerbos.sdk.client import CerbosClient
from cerbos.sdk.model import Principal, ResourceDesc
from thirdparty.cerbos_sqlalchemy.query import get_query

from fastapi import Depends

import typing

import database.models as db
import strawberry
from sqlalchemy.ext.asyncio import AsyncSession
import uuid

from api.core.deps import (
require_auth_principal,
get_cerbos_client,
get_db_session,
)
from database.models import Base
from thirdparty.cerbos_sqlalchemy.query import get_query
from api.core.deps import require_auth_principal, get_cerbos_client, get_db_session
from api.core.strawberry_extensions import DependencyExtension
from sqlalchemy import ColumnExpressionArgument, ColumnElement

robertaboukhalil marked this conversation as resolved.
Show resolved Hide resolved

async def get_entities(
model: db.Entity,
session: AsyncSession,
cerbos_client: CerbosClient,
principal: Principal,
filters: Optional[list[ColumnExpressionArgument]],
order_by: Optional[list[tuple[ColumnElement[Any], ...]]],
filters: Optional[list[ColumnExpressionArgument]] = [],
robertaboukhalil marked this conversation as resolved.
Show resolved Hide resolved
order_by: Optional[list[tuple[ColumnElement[Any], ...]]] = [],
):
rd = ResourceDesc(model.__tablename__)
plan = cerbos_client.plan_resources("view", principal, rd)
Expand All @@ -54,6 +44,46 @@ async def get_entities(
return result.scalars().all()


# Returns function that helps create entities
async def create_entity(
principal: Principal = Depends(require_auth_principal),
session: AsyncSession = Depends(get_db_session, use_cache=False),
):
async def create(entity_model, gql_type, params):
# Auth
params["owner_user_id"] = int(principal.id)
allowed_collections = principal.attr["admin_projects"] + principal.attr["member_projects"]

# User must have permissions to the collection
collection_id = params.get("collection_id")
if not collection_id:
raise Exception("Missing collection ID")
if collection_id not in allowed_collections:
raise Exception("Unauthorized")

# TODO: User must have permissions to the sample
# sample_id = params.get("sample_id")
# if sample_id and sample_id not in allowed_samples:
# raise Exception("Unauthorized")

# Save to DB
new_entity = entity_model(**params)
session.add(new_entity)
await session.commit()

# Return GQL object to client (FIXME: is there a better way to convert `new_entity` to `gql_type`?)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ohhh, interesting - I think we're relying on strawberry_sqlalchemy_mapper to do this for us in the queries. I wonder if we can leverage the types it's already generated to do this automatically 🤔

params = {
**params,
"id": new_entity.entity_id,
"type": new_entity.type,
"producing_run_id": new_entity.producing_run_id,
"entity_id": new_entity.entity_id,
}
return gql_type(**params)

return create


class EntityLoader:
"""
Creates DataLoader instances on-the-fly for SQLAlchemy relationships
Expand Down
70 changes: 57 additions & 13 deletions entities/api/main.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,18 @@
import uuid
import typing
from database.connect import AsyncDB

import database.models as db
import strawberry
import uvicorn
from cerbos.sdk.client import CerbosClient
from cerbos.sdk.model import Principal
from fastapi import Depends, FastAPI
from strawberry.fastapi import GraphQLRouter
from thirdparty.strawberry_sqlalchemy_mapper import (
StrawberrySQLAlchemyMapper,
)
from api.core.gql_loaders import EntityLoader, get_base_loader

from api.core.deps import (
get_auth_principal,
get_cerbos_client,
get_engine,
)
from thirdparty.strawberry_sqlalchemy_mapper import StrawberrySQLAlchemyMapper
from api.core.gql_loaders import EntityLoader, get_base_loader, create_entity
from api.core.deps import get_auth_principal, get_cerbos_client, get_engine
from api.core.settings import APISettings
from api.core.strawberry_extensions import DependencyExtension

######################
# Strawberry-GraphQL #
Expand All @@ -42,12 +36,63 @@ class SequencingRead:
pass


# --------------------
# Queries
# --------------------


@strawberry.type
class Query:
samples: typing.List[Sample] = get_base_loader(db.Sample, Sample)
sequencing_reads: typing.List[SequencingRead] = get_base_loader(db.SequencingRead, SequencingRead)


# --------------------
# Mutations
# --------------------


@strawberry.type
class Mutation:
@strawberry.mutation(extensions=[DependencyExtension()])
async def create_sample(
self,
name: str,
location: str,
collection_id: int,
create_entity: any = Depends(create_entity),
) -> Sample:
if not name or not location:
raise Exception("Fields cannot be empty")
params = dict(name=name, location=location, collection_id=collection_id)
return await create_entity(entity_model=db.Sample, gql_type=Sample, params=params)

# FIXME: add auth in gql_loaders.py
@strawberry.mutation(extensions=[DependencyExtension()])
async def create_sequencing_read(
robertaboukhalil marked this conversation as resolved.
Show resolved Hide resolved
self,
nucleotide: str,
sequence: str,
protocol: str,
sample_id: uuid.UUID,
collection_id: int,
create_entity: any = Depends(create_entity),
) -> SequencingRead:
params = dict(
nucleotide=nucleotide,
sequence=sequence,
protocol=protocol,
sample_id=sample_id,
collection_id=collection_id,
)
return await create_entity(entity_model=db.SequencingRead, gql_type=SequencingRead, params=params)


# --------------------
# Initialize app
# --------------------


def get_context(
engine: AsyncDB = Depends(get_engine),
cerbos_client: CerbosClient = Depends(get_cerbos_client),
Expand All @@ -68,8 +113,7 @@ def get_context(
# start server with strawberry server app
schema = strawberry.Schema(
query=Query,
# mutation=Mutation,
# extensions=extensions,
mutation=Mutation,
types=additional_types,
)

Expand Down
47 changes: 35 additions & 12 deletions entities/api/schema.graphql
Original file line number Diff line number Diff line change
@@ -1,30 +1,53 @@
interface EntityInterface {
id: UUID!
type: String!
producingRunId: Int
ownerUserId: Int!
collectionId: Int!
}

type Mutation {
createSample(name: String!, location: String!, collectionId: Int!): Sample!
createSequencingRead(nucleotide: String!, sequence: String!, protocol: String!, sampleId: UUID!, collectionId: Int!): SequencingRead!
}

type Query {
getSample(id: ID!): Sample!
getAllSamples: [Sample!]!
getSequencingRead(id: ID!): SequencingRead!
getAllSequencingReads: [SequencingRead!]!
samples(id: UUID = null): [Sample!]!
sequencingReads(id: UUID = null): [SequencingRead!]!
}

type Sample {
id: Int!
id: UUID!
type: String!
producingRunId: Int
ownerUserId: Int
entityId: Int!
ownerUserId: Int!
collectionId: Int!
entityId: UUID!
name: String!
location: String!
sequencingReads: [SequencingRead!]!
sequencingReads: SequencingReadConnection!
}

type SequencingRead {
id: Int!
id: UUID!
type: String!
producingRunId: Int
ownerUserId: Int
entityId: Int!
ownerUserId: Int!
collectionId: Int!
entityId: UUID!
nucleotide: String!
sequence: String!
protocol: String!
sampleId: Int!
sampleId: UUID!
sample: Sample!
}

type SequencingReadConnection {
edges: [SequencingReadEdge!]!
}

type SequencingReadEdge {
node: SequencingRead!
}

scalar UUID
Loading
Loading