Generated Application API Reference

Every application that SDCStudio generates from a published data model ships with a Django REST Framework app named api. It is the same in the Lightweight and Enterprise profiles. This page documents that API as the generated code defines it: four POST routes for uploading model files and for validating and storing Governed Data Records.

How to generate and run an application is covered in the App Generation guide.

Base path

The routes are defined in api/urls.py with the URL namespace api:

Route View
upload/zip/ ZIPUploadView
upload/schema/ SchemaUploadView
upload/rdf/ RDFUploadView
validate/xml/ XMLValidationView

The generated project's urls.py mounts the API at /api/v1/, beside the home page, admin/, docs/, accounts/ and each data model app. Every route requires a valid X-API-Key; a request without one is refused with 401 and a WWW-Authenticate: X-API-Key header.

With the default docker-compose.yml, the web service listens on port 18000, so the base URL on the host is http://localhost:18000/api/v1/.

Only POST is defined on each route. There are no list or retrieve endpoints, no pagination and no query filtering.

Authentication

The X-API-Key header

api/authentication.py defines APIKeyAuthentication, and every view sets it as its only authentication class. The class reads the X-API-Key request header, hashes the value with SHA-256 and looks the hash up in the APIKey table.

  • A header whose hash matches an active key authenticates the request. The key's last_used_at and request_count are updated on every call.
  • A header whose hash matches no key is rejected with 401 and the body {"detail": "Invalid API key"}.
  • A header whose key has been deactivated is rejected with 401 and the body {"detail": "API key has been deactivated"}.
  • The 401 response carries WWW-Authenticate: X-API-Key.

When the header is absent, the authentication class returns no user, and the request is refused with 401: the generated settings.py sets REST_FRAMEWORK with APIKeyAuthentication as the default authentication and IsAuthenticated as the default permission, and every view sets IsAuthenticated too.

Creating a key

Keys are created in the generated application's Django admin.

  1. Open /admin/ and sign in.
  2. Under API, open API Keys and click Add API Key.
  3. Enter Organization name and Contact email, then click Save.
  4. Copy the key from the success message. It is shown once. Only its SHA-256 hash and its first twelve characters are stored.

A key has the form sdc4_ followed by a URL-safe random string. Existing keys show only the stored prefix. To revoke a key, open it and clear Is active; the row stays for the audit trail.

Response envelope

Every view returns one of two JSON shapes, defined in api/serializers.py as FileUploadResponseSerializer and ErrorResponseSerializer.

Success:

{
  "success": true,
  "data": { }
}

Error:

{
  "success": false,
  "error": {
    "code": "FILE_REQUIRED",
    "message": "No file provided in request",
    "details": {}
  }
}

The code, message and details values for each route are listed below, exactly as the views build them.

Storage locations

Uploaded files are written through Django's default storage, whose root is MEDIA_ROOT, set to mediafiles/ in the generated project. Schema and RDF files go to mediafiles/dmlib/, which is also where validation looks for a model's XSD. ZIP files go to mediafiles/zip/.

POST upload/zip/

Stores a ZIP archive, such as another generated application, in mediafiles/zip/. The view only stores the file; it does not install the application. Installation is a separate step, done with the install_app management command or the Install SDC4 App page in the admin.

Request: multipart/form-data with one part named file.

curl -X POST \
  -H "X-API-Key: your-api-key" \
  -F "file=@app.zip" \
  http://localhost:18000/api/v1/upload/zip/

The view checks that the file name ends in .zip and that Python's zipfile can open it and pass an integrity test.

Response 201 Created, with data as ZIPUploadDataSerializer defines it:

{
  "success": true,
  "data": {
    "filename": "app.zip",
    "path": "zip/app.zip",
    "size": 1048576
  }
}

Errors, all 400 Bad Request:

code message details
FILE_REQUIRED No file provided in request {}
INVALID_FILE_TYPE File must be a ZIP archive {"filename": ...}
INVALID_ZIP File is not a valid ZIP archive {"filename": ...}

POST upload/schema/

Stores one or more model files in mediafiles/dmlib/.

Request: multipart/form-data with one or more parts named files.

curl -X POST \
  -H "X-API-Key: your-api-key" \
  -F "files=@dm-abc123.xsd" \
  -F "files=@dm-abc123.html" \
  http://localhost:18000/api/v1/upload/schema/

Each file is checked separately:

  • the extension must be .xsd, .html or .owl;
  • the name must start with dm- and contain exactly one dot, that is the pattern dm-{ct_id}.{ext}.

A file that fails either check is reported in errors and skipped; the others are stored. The response is 201 Created whether or not any file was stored, with data as SchemaUploadDataSerializer defines it:

{
  "success": true,
  "data": {
    "stored_files": [
      {"filename": "dm-abc123.xsd", "path": "dmlib/dm-abc123.xsd", "size": 24576}
    ],
    "errors": [
      {"filename": "schema.xsd", "error": "Filename must match pattern: dm-{ct_id}.{ext}"}
    ]
  }
}

The per-file error strings are Invalid file extension. Allowed: .xsd, .html, .owl and Filename must match pattern: dm-{ct_id}.{ext}.

Error, 400 Bad Request:

code message details
FILES_REQUIRED No files provided in request {}

POST upload/rdf/

Stores a Turtle or RDF/XML file in mediafiles/dmlib/ and, in the Enterprise profile, also loads it into GraphDB.

Request: multipart/form-data with one part named file. The extension must be .ttl or .rdf.

curl -X POST \
  -H "X-API-Key: your-api-key" \
  -F "file=@dm-abc123.ttl" \
  http://localhost:18000/api/v1/upload/rdf/

After the file is stored, the view checks the settings GRAPHDB_URL and GRAPHDB_REPOSITORY. When both are set, it derives the model identifier from the file name by removing dm-, the extension and any _shacl suffix, builds the named graph urn:sdc4:dm-{ct_id}:schema, and posts the file to {GRAPHDB_URL}/repositories/{GRAPHDB_REPOSITORY}/statements with that graph as the context. A 200, 201 or 204 from GraphDB gives triplestore_status synced; anything else, including a connection error, gives failed. The file stays stored either way.

When the settings are not both present, triplestore_status is disabled and graph_uri is null. The Lightweight profile defines FUSEKI_URL rather than GRAPHDB_URL, so in a Lightweight application this route always reports disabled; it does not load the file into Fuseki.

Response 201 Created, with data as RDFUploadDataSerializer defines it:

{
  "success": true,
  "data": {
    "filename": "dm-abc123.ttl",
    "path": "dmlib/dm-abc123.ttl",
    "size": 15360,
    "triplestore_status": "synced",
    "graph_uri": "urn:sdc4:dm-abc123:schema"
  }
}

triplestore_status is one of synced, failed, disabled.

Errors, all 400 Bad Request:

code message details
FILE_REQUIRED No file provided in request {}
INVALID_FILE_TYPE File must be .ttl or .rdf {"filename": ...}

POST validate/xml/

Validates a Governed Data Record against its model's XSD and stores it.

Request: either multipart/form-data with one part named file, or a JSON or form body with a field xml_content holding the XML as a string.

curl -X POST \
  -H "X-API-Key: your-api-key" \
  -F "file=@instance.xml" \
  http://localhost:18000/api/v1/validate/xml/

What the view does

  1. Parses the XML with lxml. A parse failure is INVALID_XML.
  2. Reads the root element's local name, which must start with dm-. The rest of the name is the model identifier, dm_ct_id.
  3. Looks for mediafiles/dmlib/dm-{dm_ct_id}.xsd. If it is not there, the response is SCHEMA_NOT_FOUND. The file is placed there when the application is generated and can also be uploaded through upload/schema/.
  4. Validates the record with SDCValidator(schema_path).validate_and_recover(xml_content) from sdc4_shared/utils/sdc_validator.py. The validator builds the schema with sdcvalidator as XSD 1.1 and resolves the reference model sdc4.xsd locally: through mediafiles/dmlib/catalog.xml when it is present, otherwise from a sdc4.xsd beside the model's XSD, otherwise over the network.
  5. Decides the outcome:
  6. The record is valid: validation_status is valid.
  7. The record has semantic errors that the validator could recover by inserting exceptional values: validation_status is valid_with_ev, auto_corrected_fields lists the XPaths that were corrected, validation_errors maps each XPath to its reason, and the recovered XML is what gets stored.
  8. The record has structural errors that cannot be recovered, or validation itself raised: the response is VALIDATION_FAILED with status 422.
  9. Generates the record identifier with cuid2: i-{cuid} for a valid record, i-ev-{cuid} for a recovered one.
  10. Checks the DM registry (sdc4_shared/utils/dm_registry.py) for an installed app whose model declares DM_CT_ID equal to dm_ct_id, and reports the answer as dedicated_app. Either way the record is written to generic_storage.GenericInstance with its XML, label, validation status, errors and corrected fields, which the admin lists, and storage_type is generic.
  11. Reads dm_label from the record's dm-label element, falling back to DM-{dm_ct_id}.
  12. Sets rdf_sync_status to disabled. Records stored through this route are not written to the triplestore, in either profile; the data model apps upload RDF for their own records when they are saved.

Exceptional values

The API does not accept or return exceptional values as separate fields. They appear in three places:

  • in the stored XML, as the ExceptionalValue elements that the validator inserts during recovery;
  • in validation_status, which is valid_with_ev when any were inserted;
  • in the i-ev- prefix of instance_id and the auto_corrected_fields list.

Records stored as GenericInstance can be inspected in the admin under Generic Instance Storage.

Response

201 Created, with data as the view builds it (XMLValidationDataSerializer describes the same fields, except dedicated_app):

{
  "success": true,
  "data": {
    "instance_id": "i-ev-tz4a98xxat96iws9zmbrgj3a",
    "dm_ct_id": "abc123",
    "dm_label": "Blood Pressure",
    "validation_status": "valid_with_ev",
    "storage_type": "generic",
    "dedicated_app": false,
    "auto_corrected_fields": ["/sdc4:dm-abc123/sdc4:data/sdc4:ms-xyz/sdc4:xdstring-value"],
    "validation_errors": {"/sdc4:dm-abc123/sdc4:data/sdc4:ms-xyz/sdc4:xdstring-value": "..."},
    "rdf_sync_status": "disabled"
  }
}

Field values:

Field Values
validation_status valid, valid_with_ev
storage_type generic (always)
dedicated_app true when a data model app for this dm_ct_id is installed, else false
auto_corrected_fields list of XPath strings; empty when valid
validation_errors object mapping XPath to reason, or null when valid
rdf_sync_status disabled (always)

Errors

Status code message details
400 XML_REQUIRED No XML content provided {}
400 INVALID_XML Could not parse XML {"error": ...}
400 INVALID_XML Root element must be <sdc4:dm-{ct_id}> {"root_tag": ...}
404 SCHEMA_NOT_FOUND No schema found for dm-{ct_id} {"dm_ct_id": ...}
422 VALIDATION_FAILED XML failed structural validation {"errors": {xpath: reason}}
422 VALIDATION_FAILED XML validation failed {"error": ...}

Status codes used

Status Where
201 Created every successful call
400 Bad Request missing or malformed input, as listed per route
401 Unauthorized no X-API-Key, or one that is unknown or deactivated
404 Not Found validate/xml/ when the model's XSD is not in mediafiles/dmlib/
422 Unprocessable Entity validate/xml/ when the record fails structural validation or validation raises

Lightweight and Enterprise

The api app, its routes, serializers and authentication are identical in both profiles. The only differences come from settings:

  • Enterprise defines GRAPHDB_URL and GRAPHDB_REPOSITORY, so upload/rdf/ loads files into GraphDB.
  • Lightweight defines FUSEKI_URL instead. upload/rdf/ does not talk to Fuseki and reports disabled.
  • validate/xml/ reports rdf_sync_status disabled in both.

Tests

The app ships with api/tests.py. Run it inside the web container:

docker compose exec web python manage.py test api

Questions about the generated API: contact@axius-sdc.com