diff --git a/README.md b/README.md index d892e8c..980a7d3 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,46 @@ # Platformics ---- -Platformics is a GraphQL API framework that relies on code generation to implement a full featured GraphQL API on top of a PostgreSQL database, with support for authorization policy enforcement and file persistence via S3. + +Platformics is a GraphQL API framework that relies on code generation to implement a full featured GraphQL API on top of a PostgreSQL database, with support for authorization policy enforcement and file persistence via S3. It's built on top of the best available Python tools and frameworks! + +The libraries and tools that make Platformics work: + +![image](docs/images/platformics_libs.svg) + +### Links to these tools/libraries + - [LinkML](https://linkml.io/) - Schema modeling language + - [FastAPI](https://fastapi.tiangolo.com/) - Async HTTP router + - [Strawberry](https://strawberry.rocks/) - GraphQL Framework + - [Pydantic](https://docs.pydantic.dev/latest/) - Data validation + - [Cerbos](https://www.cerbos.dev/) - Authorization + - [SQLAlchemy](https://www.sqlalchemy.org/) - Database Access / ORM + - [factory_boy](https://factoryboy.readthedocs.io/en/stable/) - Test fixtures + - [Alembic](https://alembic.sqlalchemy.org/en/latest/) - Database migrations + +## Current Features +- [x] Express your schema in a straightforward YAML format +- [x] GraphQL Dataloader pattern (no n+1 queries!) +- [x] Authorization policy enforcement +- [x] Flexible Filtering +- [x] Data aggregation +- [x] Top-level pagination +- [x] Relationship traversal +- [x] DB migrations +- [x] Generated Test fixtures +- [x] pytest wiring +- [x] VSCode debugger integration +- [x] Authorized S3 file up/downloads +- [x] Add custom REST endpoints to generated API +- [x] Add custom GQL queries/mutations to generated API + +## Roadmap +- [ ] Plugin hooks to add business logic to generated GQL resolvers +- [ ] Support arbitrary class inheritance hierarchies ## How to set up your own platformics API 1. Copy the test_app boilerplate code to your own repository. 2. Edit `schema/schema.yml` to reflect your application's data model. 3. Run `make build` and then `make init` to build and run your own GraphQL API service. -4. Browse to http://localhost:9009 to interact with your api! +4. Browse to http://localhost:9009/graphql to interact with your api! 5. Run `make token` to generate an authorization token that you can use to interact with the API. The `make` target copies the necessary headers to the system clipboard. Paste the token into the `headers` section at the bottom of the GraphQL explorer API ## Iterating on your schema @@ -26,6 +60,10 @@ Platformics is a GraphQL API framework that relies on code generation to impleme 3. Click the "Run and Debug" icon in the icon bar on the right side of the VSCode window (or ctrl+shift+d). Then click the "start debugging" icon at the top of the run and debug panel (or press F5). This will launch a secondary instance of the API service that listens on port 9008. 4. Set all the breakpoints you want. Browse to the api at http://localhost:9008 to trigger them. Remember that the application restarts when files change, so you'll have to start and stop the debugger to pick up any changes you make! +## HOWTO +- [Extend the generated API](docs/HOWTO-extend-generated-api.md) +- [Customize Codegen templates](docs/HOWTO-customize-templates.md) + ## Contributing This project adheres to the Contributor Covenant code of conduct. By participating, you are expected to uphold this code. Please report unacceptable behavior to opensource@chanzuckerberg.com. diff --git a/docs/HOWTO-customize-templates.md b/docs/HOWTO-customize-templates.md new file mode 100644 index 0000000..c65b20a --- /dev/null +++ b/docs/HOWTO-customize-templates.md @@ -0,0 +1,23 @@ +# How To: Customize codegen templates +Platformics supports replacing one codegen template with another, either globally or for a specific type. + + +## Use a different template by default +1. Find the template that you want to replace in the [base platformics template directory](https://github.com/chanzuckerberg/platformics/tree/main/platformics/codegen/templates) +2. Create a directory that will contain your overriden templates, such as `template_overrides` +3. Copy that template to your overrides directory with the same path relative to `templates` in the platformics repo. For example, if you want to override `platformics/codegen/templates/database/models/class_name.py.j2`, copy it to `template_overrides/database/models/class_name.py.j2` +4. Modify the template as much as you want +5. When you run codegen, include the overrides folder as a parameter to the codegen tool. For example, update the `codegen` target in the `Makefile` for your project directory to look like: + ``` + $(docker_compose_run) $(APP_CONTAINER) platformics api generate --schemafile ./schema/schema.yaml --template-override-paths template_overrides --output-prefix . + ``` + +## Use a different template for a specific class +1. Find the template that you want to replace in the [base platformics template directory](https://github.com/chanzuckerberg/platformics/tree/main/platformics/codegen/templates). Note that this **only works for files named `class_name.*`**! +2. Create a directory that will contain your overriden templates, such as `template_overrides` +3. Copy that template to your overrides directory with the same path relative to `templates` in the platformics repo, but the **filename needs to reflect the camel_case class name**. For example, if you want to override `platformics/codegen/templates/database/models/class_name.py.j2`, for a class called `MyData`, copy it to `template_overrides/database/models/my_data.py.j2` +4. Modify the template as much as you want +5. When you run codegen, include the overrides folder as a parameter to the codegen tool. For example, update the `codegen` target in the `Makefile` for your project directory to look like: + ``` + $(docker_compose_run) $(APP_CONTAINER) platformics api generate --schemafile ./schema/schema.yaml --template-override-paths template_overrides --output-prefix . + ``` diff --git a/docs/custom_code.md b/docs/HOWTO-extend-generated-api.md similarity index 63% rename from docs/custom_code.md rename to docs/HOWTO-extend-generated-api.md index c8713ff..cafa85a 100644 --- a/docs/custom_code.md +++ b/docs/HOWTO-extend-generated-api.md @@ -1,36 +1,62 @@ -# How To: Extending Platformics Generated Code +# How To: Extending Platformics Generated API's Platformics will generate a GraphQL API that exposes CRUD operations for each entity type in your LinkML schema. It is often the case, however, that you will need to expose more functionality in your API than just CRUD. There are a few options for extending generated code to add custom business logic to your application. -## Adding root-level queries and mutations -Platformics will generate basic read and aggregate queries in `api/queries.py`. These are inherited by a query class in `custom_queries.py`, which will gain access to all the codegenned queries as well as allow you to implement your own queries. To add a new root level field (query), add the field to the query class in `custom_queries.py`: + +## Adding custom REST endpoints +Platformics primarily generates a GraphQL API, but there are certain functionalities (such as file downloads) that aren't practical to expose via GraphQL. Fortunately, Platformics is built on top of [FastAPI](https://fastapi.tiangolo.com/), a popular REST API framework. Adding custom endpoints is fairly straightforward: ```python -@strawberry.type -class Query(BaseQuery): - foo: str = my_custom_field +# your_app/main.py + +# This code is already in main.py (app is a standard FastAPI application) +app = get_app(settings, schema, models) + +# This adds an extra REST endpoint at GET http://localhost:9009/example +@app.get("/example") +def read_root(): + return {"Hello": "World"} + ``` -The resolver for the custom field should be decorated with `@strawberry.field`: + +## Adding root-level GraphQL queries and mutations +Platformics will generate basic read and aggregate queries in `api/queries.py`. These are inherited by a query class in `custom_queries.py`, which will gain access to all the codegenned queries as well as allow you to implement your own queries. To add a new root level field (query), add the field to the query class in `custom_queries.py`: + ```python +# your_app/custom_queries.py + +import strawberry +from api.queries import Query as BaseQuery + +# The resolver for the custom field should be decorated with `@strawberry.field`: @strawberry.field def my_custom_field(self) -> str: return "foo" + +@strawberry.type +class Query(BaseQuery): + foo: str = my_custom_field ``` Similarly, for custom mutations, root level mutations can be added to the `Mutation` class in `custom_mutations.py`, which inherits from the codegenned `Mutation` class in `api/mutations.py` ```python -@strawberry.type -class Mutation(BaseMutation): - bar: str = my_custom_mutation -``` -The resolver for the custom mutation should be decorated with `@strawberry.mutation`: -```python +# your_app/custom_mutations.py +import strawberry +from api.mutations import Mutation as BaseMutation + +# The resolver for the custom mutation should be decorated with `@strawberry.mutation`: @strawberry.mutation def my_custom_mutation(self) -> str: return "bar" + +@strawberry.type +class Mutation(BaseMutation): + bar: str = my_custom_mutation ``` + Both the custom query and mutation classes are imported into `main.py` and used by Strawberry to generate an API at runtime: ```python +# your_app/main.py from custom_mutations import Mutation from custom_queries import Query @@ -40,6 +66,7 @@ schema = strawberry.Schema(query=Query, mutation=Mutation, config=get_strawberry ### Overriding queries and mutations By creating a field with the same name as an existing codegenned query or mutation, you can override the codegenned resolver. ```python +# your_app/custom_mutations.py @strawberry.mutation def my_override_mutation(self) -> str: return "Sorry, I overrode the mutation" diff --git a/docs/images/platformics_libs.svg b/docs/images/platformics_libs.svg new file mode 100644 index 0000000..2b97709 --- /dev/null +++ b/docs/images/platformics_libs.svg @@ -0,0 +1 @@ + \ No newline at end of file