Skip to content

Commit

Permalink
Merge pull request #1650 from weather-gov/jt/json-api
Browse files Browse the repository at this point in the history
JSON:API
  • Loading branch information
jamestranovich-noaa committed Aug 30, 2024
2 parents fdede32 + 6bc0705 commit 582c3d2
Show file tree
Hide file tree
Showing 27 changed files with 980 additions and 2 deletions.
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"drupal/core-composer-scaffold": "^10.3",
"drupal/core-project-message": "^10.3",
"drupal/core-recommended": "^10.3",
"drupal/jsonapi_extras": "^3.25",
"drupal/key_asymmetric": "^1.1",
"drupal/log_stdout": "^1.5",
"drupal/metatag": "^2.0",
Expand Down
173 changes: 171 additions & 2 deletions composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions docs/dev/contributed-modules.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,3 +150,9 @@ We use this module to get a specific phone number field type, along with validat
_Added July 2024_

This module gives us a physical address field type, along with configuration options and validation. Used initially for WFO Information.

### [`jsonapi_extras`](https://www.drupal.org/project/jsonapi_extras)

_Added August 2024_

We use this module to disable resource access by default, via the API endpoint. We also use this module to disable certain fields within permitted Drupal content types for the API endpoint. Please see [./json-api.md](the JSON:API documentation) for more information.
69 changes: 69 additions & 0 deletions docs/dev/json-api-upload-example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
#!/usr/bin/env python

import requests # https://pypi.org/project/requests/
import subprocess
import base64
import os
import sys

# This is a sample python script to demonstrate how to upload a WFO daily
# situation report, which is produced as a PDF. This script is for educational
# purposes only and is intended to aid as a helper for integration.

# configuration options
endpoint = 'http://localhost:8080'
user = 'uploader'
password = 'uploader'
pdf_filename = 'test.pdf'

# if we don't have one already, create a pdf for uploading; requires magick
if not os.path.exists(pdf_filename):
_ = subprocess.run(['magick', 'xc:none', '-page', 'Letter', pdf_filename])

pdf_data = open(pdf_filename, 'rb').read()

# first step: upload the pdf file
headers = {
'Accept': 'application/vnd.api+json',
'Content-Type': 'application/octet-stream',
'Content-Disposition': f'file; filename="{pdf_filename}"',
}
resp = requests.post(f'{endpoint}/jsonapi/node/wfo_pdf_upload/field_wfo_sitrep',
data=pdf_data,
headers=headers,
auth=(user, password))
if resp.status_code != 201:
print(f'could not upload pdf: {resp.text}')
sys.exit(1)
print(f'successfully uploaded pdf: {resp.json()}')

# second step: grab the ID of the file we just uploaded
uploaded_file_id = resp.json()['data']['id']

# last step: create the wfo_pdf_upload type
payload = {
'data': {
'type': 'node--wfo_pdf_upload',
'attributes': {
'title': pdf_filename,
},
'relationships': {
'field_wfo_sitrep': {
'data': {
'type': 'file--file',
'id': uploaded_file_id,
}
}
}
}
}
headers = {
'Accept': 'application/vnd.api+json',
'Content-Type': 'application/vnd.api+json',
}
resp = requests.post(f'{endpoint}/jsonapi/node/wfo_pdf_upload', json=payload, headers=headers, auth=(user, password))
if resp.status_code != 201:
print(f'could not finish wfo pdf upload: {resp.text}')
sys.exit(1)

print(f'created a new WFO daily situation report: {resp.json()}')
69 changes: 69 additions & 0 deletions docs/dev/json-api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# JSON:API

## Modules

We have enabled and configured the core Drupal [`jsonapi`](https://www.drupal.org/docs/core-modules-and-themes/core-modules/jsonapi-module/api-overview) module specifically to allow uploading of WFO daily situation reports, which are produced as PDFs.

We are also enabling the following core Drupal modules:

- [`basic_auth`](https://www.drupal.org/docs/8/core/modules/basic_auth/overview) for authentication
- [`jsonapi_extras`](https://www.drupal.org/project/jsonapi_extras) by default, prevent resources (and resource fields) from being accessible via the API
- `jsonapi_defaults` is a submodule of the above which adds defaults to API resource types
- [`serialization`](https://www.drupal.org/docs/8/core/modules/serialization/overview) for JSON support

## Configuration

A new content type, `wfo_pdf_upload` was created for WFO daily situation reports. The `field_wfo_sitrep` field holds the PDF file.

We have configured JSON:API to only display `wfo_pdf_upload`s and `file`s. (The latter is needed because we need to upload the PDF and then link the PDF to the `wfo_pdf_upload`.) Furthermore, `wfo_pdf_upload` is configured to only allow the `title` and `field_wfo_sitrep` fields to be shown or set. `file` is configured to only allow the `uri` field to be shown.

We have also configured a new user type, `uploader`, which has no permissions except to create new `wfo_pdf_upload`s.

Because JSON:API follows Drupal entity permissions, JSON:API also respects the user permissions for that entity type. This permission system is not sufficient in and of itself (for example, `anonymous` users could browse the JSON API, including viewing `file`s and `wfo_pdf_upload`s, because `anonymous` users can `view published content`.). So, to further restrict access, we have added a `JsonApiLimitingRouteSubscriber` that mandates an `uploader` role for the API. All other users will get a `403` response.

# Example

Uploading a WFO sitrep is a two step process. Here, we use `curl` as an example. We also assume an `uploader` user type is used with the password `uploader` (please do not create this example user for any public site).

First, we upload the PDF itself to the `wfo_pdf_upload/field_wfo_sitrep` field (note: you can create a blank PDF by using [`imagemagick`](https://imagemagick.org/index.php): `magick xc:none -page Letter test.pdf`):

curl -sL \
--user uploader:uploader \
-H 'Accept: application/vnd.api+json' \
-H 'Content-Type: application/octet-stream' \
-H 'Content-Disposition: file; filename="test.pdf"' \
-d @test.pdf \
http://localhost:8080/jsonapi/node/wfo_pdf_upload/field_wfo_sitrep

The response should be a 201 with JSON information about the newly uploaded file attributes. We want the `id` of the newly uploaded file for the next step. (You can use `jq` and add a pipe: `| jq "data.id"` above to more easily retrieve the resulting `id`.)

Let's assume the newly created PDF `id` is `9986844e-c190-428a-b152-7c3a03244b71`.

Second, we create the `wfo_pdf_upload` entity itself and link the PDF `id`:

curl -sL \
--user uploader:uploader \
-H 'Accept: application/vnd.api+json' \
-H 'Content-Type: application/vnd.api+json' \
-d '{"data": {
"type": "node--wfo_pdf_upload",
"attributes": {
"title": "test.pdf"
},
"relationships": {
"field_wfo_sitrep": {
"data": {
"type": "file--file",
"id": "9986844e-c190-428a-b152-7c3a03244b71"
}
}
}
}
}' \
http://localhost:8080/jsonapi/node/wfo_pdf_upload

The response should also be a 201 with JSON information about the newly created `wfo_pdf_upload` entity.

# Integration Example

A sample [Python script](../json-api-upload-example.py) is provided to help to aid in integration. Note that this script depends on the [requests](https://pypi.org/project/requests/) library.
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
status: 0
pattern: ''
escape: false
preserve_titles: false
save: false
chunk: 50
dependencies:
config:
- node.type.wfo_pdf_upload
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
uuid: 6431aada-45e3-41ed-a433-2099e55ce421
langcode: en
status: true
dependencies:
config:
- node.type.wfo_pdf_upload
id: node.wfo_pdf_upload.promote
field_name: promote
entity_type: node
bundle: wfo_pdf_upload
label: 'Promoted to front page'
description: ''
required: false
translatable: true
default_value:
-
value: 0
default_value_callback: ''
settings:
on_label: 'On'
off_label: 'Off'
field_type: boolean
Loading

0 comments on commit 582c3d2

Please sign in to comment.