Skip to content

Conversation

@megatnt1122
Copy link
Collaborator

@megatnt1122 megatnt1122 commented Dec 18, 2025

Ticket

#1522

Description

Logging improvements to schema router and included tests for them

Tasks

  • - A description of the PR has been provided, and a diagram included if it is a new feature.
  • - Formatter has been run
  • - CHANGELOG comment has been added
  • - Labels have been assigned to the pr
  • - A reviwer has been added
  • - A user has been assigned to work on the pr
  • - If new feature a unit test has been added

Summary by Sourcery

Improve logging for schema router endpoints and add unit tests to validate schema CRUD and search behaviour.

Enhancements:

  • Add structured start/success/failure logging to schema create, update, revise, delete, view, and search endpoints with correlation metadata.

Tests:

  • Introduce foxx schema router unit test suite covering create, update, revise, delete, view, and search endpoints, and register it in the CMake-based test configuration.

@megatnt1122 megatnt1122 added Type: Refactor Imlplementation change, same functionality Priority: Low Lower priority work. Component: Foxx Foxx Arango micro services. labels Dec 18, 2025
@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Dec 18, 2025

Reviewer's Guide

Adds structured request lifecycle logging (start/success/failure) to all schema router endpoints and introduces focused Foxx tests for the schema router, wiring them into the CMake Foxx test suite.

Sequence diagram for schema router request lifecycle logging

sequenceDiagram
    actor Client
    participant SchemaRouter
    participant Logger
    participant ArangoDB
    participant ErrorHandler

    Client->>SchemaRouter: HTTP POST /schema/create
    SchemaRouter->>Logger: logRequestStarted(client, correlationId, httpVerb, routePath, status, description)
    SchemaRouter->>ArangoDB: _executeTransaction(...)
    alt Transaction success
        ArangoDB-->>SchemaRouter: sch
        SchemaRouter->>Logger: logRequestSuccess(client, correlationId, httpVerb, routePath, status, description, extra)
        SchemaRouter-->>Client: 200 OK [schema payload]
    else Transaction failure
        ArangoDB-->>SchemaRouter: error e
        SchemaRouter->>Logger: logRequestFailure(client, correlationId, httpVerb, routePath, status, description, extra, error)
        SchemaRouter->>ErrorHandler: handleException(e, res)
        ErrorHandler-->>Client: Error response
    end
Loading

File-Level Changes

Change Details Files
Introduce structured logging for schema router HTTP endpoints across create, update, revise, delete, view, and search operations.
  • Import shared logger and define a basePath constant used in log metadata.
  • Add logRequestStarted calls at the beginning of each handler with client, correlationId, verb, routePath, and description.
  • Add logRequestSuccess calls after successful DB transactions including selective schema metadata in the extra field.
  • Add logRequestFailure calls in catch blocks including error details and any available schema/context in extra.
core/database/foxx/api/schema_router.js
Ensure variables used for logging are scoped outside transactional callbacks so they are visible in success/failure logging paths.
  • Hoist sch, sch_new, sch_old, sch, and result variables to the top of route handlers instead of redeclaring them within inner scopes.
  • Adjust assignments from var declarations to plain assignments so outer-scoped variables capture DB results for logging.
core/database/foxx/api/schema_router.js
Add unit tests for schema router behavior and wire them into the Foxx CMake test configuration.
  • Create schema_router.test.js to cover create, update, revise, delete, view, and search endpoints end-to-end against ArangoDB collections.
  • Set up test fixtures to create/truncate needed collections and insert a fake admin user.
  • Register foxx_schema_router test in CMakeLists and ensure it uses the Foxx fixture set so tests run alongside existing Foxx router tests.
core/database/foxx/tests/schema_router.test.js
core/database/CMakeLists.txt

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey there - I've reviewed your changes - here's some feedback:

  • In several catch blocks (/create, /update, /revise, /delete, /view, /search) the failure logging accesses fields on variables that may still be null/undefined (e.g., sch.own_id, sch_new.own_id, sch_old._id, result), which risks throwing a secondary error and hiding the original; consider guarding these with null checks or building extra defensively.
  • The failure logging payloads are inconsistent (e.g., some include error: e while /delete and /view do not); if downstream consumers rely on a consistent structure, it would be helpful to normalize the fields included in success/failure logs across all endpoints.
  • The extra fields in logging for some endpoints include the full schema object or potentially large def payloads, which may be noisy or sensitive; consider restricting logged fields to identifiers/metadata only or truncating large objects to keep logs lean and safer.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In several catch blocks (`/create`, `/update`, `/revise`, `/delete`, `/view`, `/search`) the failure logging accesses fields on variables that may still be `null`/`undefined` (e.g., `sch.own_id`, `sch_new.own_id`, `sch_old._id`, `result`), which risks throwing a secondary error and hiding the original; consider guarding these with null checks or building `extra` defensively.
- The failure logging payloads are inconsistent (e.g., some include `error: e` while `/delete` and `/view` do not); if downstream consumers rely on a consistent structure, it would be helpful to normalize the fields included in success/failure logs across all endpoints.
- The `extra` fields in logging for some endpoints include the full schema object or potentially large `def` payloads, which may be noisy or sensitive; consider restricting logged fields to identifiers/metadata only or truncating large objects to keep logs lean and safer.

## Individual Comments

### Comment 1
<location> `core/database/foxx/api/schema_router.js:83-92` </location>
<code_context>

 router
     .post("/create", function (req, res) {
+        let sch = null;
         try {
+            logger.logRequestStarted({
</code_context>

<issue_to_address>
**issue (bug_risk):** Guard against using `sch` in the failure logger when it may still be null/undefined.

In `/create`, `sch` starts as `null` and is only set inside the transaction. If an error occurs before `sch = g_db.sch.save(...)` completes, the `catch` block will access `sch.own_id`, `sch.id`, and `sch.desc`, causing a new exception and hiding the original failure. Please either log `sch` directly (without field access) or only construct `extra` when `sch` is non-null.
</issue_to_address>

### Comment 2
<location> `core/database/foxx/tests/schema_router.test.js:10` </location>
<code_context>
+
+const schema_base_url = `${baseUrl}/schema`;
+
+describe("simple test for schema/create", () => {
+    before(() => {
+        // Ensure the required collections exist and are empty
</code_context>

<issue_to_address>
**suggestion (testing):** Add negative-path tests to cover error handling and edge cases for each endpoint

Right now the tests only cover the happy paths for create, update, revise, delete, view, and search, so the error-handling and logging paths aren’t exercised. Please add a few targeted negative tests, for example:

- `/create`: missing required fields (e.g. no `id` or `def`), and non-admin user if applicable.
- `/update` and `/revise`: `id` without `:version` suffix (expect `ERR_INVALID_PARAM` and correct HTTP status), and `id` with a non-existent version (expect `ERR_NOT_FOUND`).
- `/delete`: invalid `id` format and non-existent schema.
- `/view`: non-existent schema and invalid `id` format.
- `/search`: invalid query parameters (e.g. out-of-range paging) if validated by the implementation.

This will exercise the failure branches and their logging to help prevent regressions in validation and routing logic.

Suggested implementation:

```javascript
const { db } = require("@arangodb");

const schema_base_url = `${baseUrl}/schema`;

describe("schema router negative paths", () => {
    it("should reject /create without required fields", () => {
        const res = request.post({
            url: `${schema_base_url}/create`,
            body: {
                // missing id and/or def on purpose
                name: "invalid schema without id and def",
            },
            json: true,
        });

        // Adjust expected status code (e.g. 400 or 422) to match implementation
        expect(res.statusCode).to.be.oneOf([400, 422]);
        expect(res.json).to.have.property("error").that.is.true;
    });

    it("should reject /create with missing def", () => {
        const res = request.post({
            url: `${schema_base_url}/create`,
            body: {
                id: "sch/missing-def",
                // def is missing
            },
            json: true,
        });

        expect(res.statusCode).to.be.oneOf([400, 422]);
        expect(res.json).to.have.property("error").that.is.true;
    });

    it("should reject /update when id has no :version suffix", () => {
        const res = request.put({
            url: `${schema_base_url}/update`,
            body: {
                id: "sch/invalid-format-no-version",
                def: { foo: "bar" },
            },
            json: true,
        });

        expect(res.statusCode).to.be.oneOf([400, 422]);
        expect(res.json).to.have.property("error").that.is.true;
        // Optionally assert error code string / number if available:
        // expect(res.json).to.have.property("errorNum", "ERR_INVALID_PARAM");
    });

    it("should return not found for /update with non-existent version", () => {
        const res = request.put({
            url: `${schema_base_url}/update`,
            body: {
                id: "sch/non-existent:9999",
                def: { foo: "bar" },
            },
            json: true,
        });

        expect(res.statusCode).to.be.oneOf([404, 410]);
        expect(res.json).to.have.property("error").that.is.true;
        // expect(res.json).to.have.property("errorNum", "ERR_NOT_FOUND");
    });

    it("should reject /revise when id has no :version suffix", () => {
        const res = request.post({
            url: `${schema_base_url}/revise`,
            body: {
                id: "sch/invalid-format-no-version",
                def: { foo: "bar" },
            },
            json: true,
        });

        expect(res.statusCode).to.be.oneOf([400, 422]);
        expect(res.json).to.have.property("error").that.is.true;
    });

    it("should return not found for /revise with non-existent version", () => {
        const res = request.post({
            url: `${schema_base_url}/revise`,
            body: {
                id: "sch/non-existent:9999",
                def: { foo: "bar" },
            },
            json: true,
        });

        expect(res.statusCode).to.be.oneOf([404, 410]);
        expect(res.json).to.have.property("error").that.is.true;
    });

    it("should reject /delete with invalid id format", () => {
        const res = request.del({
            url: `${schema_base_url}/delete`,
            body: {
                id: "invalid-id-format-without-slash-or-version",
            },
            json: true,
        });

        expect(res.statusCode).to.be.oneOf([400, 422]);
        expect(res.json).to.have.property("error").that.is.true;
    });

    it("should return not found for /delete of non-existent schema", () => {
        const res = request.del({
            url: `${schema_base_url}/delete`,
            body: {
                id: "sch/non-existent:1",
            },
            json: true,
        });

        expect(res.statusCode).to.be.oneOf([404, 410]);
        expect(res.json).to.have.property("error").that.is.true;
    });

    it("should return not found for /view of non-existent schema", () => {
        const res = request.get({
            url: `${schema_base_url}/view`,
            qs: { id: "sch/non-existent:1" },
            json: true,
        });

        expect(res.statusCode).to.be.oneOf([404, 410]);
        expect(res.json).to.have.property("error").that.is.true;
    });

    it("should reject /view with invalid id format", () => {
        const res = request.get({
            url: `${schema_base_url}/view`,
            qs: { id: "invalid-id-format" },
            json: true,
        });

        expect(res.statusCode).to.be.oneOf([400, 422]);
        expect(res.json).to.have.property("error").that.is.true;
    });

    it("should reject /search with invalid paging parameters", () => {
        const res = request.get({
            url: `${schema_base_url}/search`,
            qs: {
                // Assume offset/limit (or page/size) are validated in the implementation
                offset: -1,
                limit: 0,
            },
            json: true,
        });

        expect(res.statusCode).to.be.oneOf([400, 422]);
        expect(res.json).to.have.property("error").that.is.true;
    });
});

describe("simple test for schema/create", () => {

```

` what to customize (exact status codes, error fields, auth headers, etc).

Here are the edits:

<file_operations>
<file_operation operation="edit" file_path="core/database/foxx/tests/schema_router.test.js">
<<<<<<< SEARCH
const { db } = require("@arangodb");

const schema_base_url = `${baseUrl}/schema`;

describe("simple test for schema/create", () => {
=======
const { db } = require("@arangodb");

const schema_base_url = `${baseUrl}/schema`;

describe("schema router negative paths", () => {
    it("should reject /create without required fields", () => {
        const res = request.post({
            url: `${schema_base_url}/create`,
            body: {
                // missing id and/or def on purpose
                name: "invalid schema without id and def",
            },
            json: true,
        });

        // Adjust expected status code (e.g. 400 or 422) to match implementation
        expect(res.statusCode).to.be.oneOf([400, 422]);
        expect(res.json).to.have.property("error").that.is.true;
    });

    it("should reject /create with missing def", () => {
        const res = request.post({
            url: `${schema_base_url}/create`,
            body: {
                id: "sch/missing-def",
                // def is missing
            },
            json: true,
        });

        expect(res.statusCode).to.be.oneOf([400, 422]);
        expect(res.json).to.have.property("error").that.is.true;
    });

    it("should reject /update when id has no :version suffix", () => {
        const res = request.put({
            url: `${schema_base_url}/update`,
            body: {
                id: "sch/invalid-format-no-version",
                def: { foo: "bar" },
            },
            json: true,
        });

        expect(res.statusCode).to.be.oneOf([400, 422]);
        expect(res.json).to.have.property("error").that.is.true;
        // Optionally assert error code string / number if available:
        // expect(res.json).to.have.property("errorNum", "ERR_INVALID_PARAM");
    });

    it("should return not found for /update with non-existent version", () => {
        const res = request.put({
            url: `${schema_base_url}/update`,
            body: {
                id: "sch/non-existent:9999",
                def: { foo: "bar" },
            },
            json: true,
        });

        expect(res.statusCode).to.be.oneOf([404, 410]);
        expect(res.json).to.have.property("error").that.is.true;
        // expect(res.json).to.have.property("errorNum", "ERR_NOT_FOUND");
    });

    it("should reject /revise when id has no :version suffix", () => {
        const res = request.post({
            url: `${schema_base_url}/revise`,
            body: {
                id: "sch/invalid-format-no-version",
                def: { foo: "bar" },
            },
            json: true,
        });

        expect(res.statusCode).to.be.oneOf([400, 422]);
        expect(res.json).to.have.property("error").that.is.true;
    });

    it("should return not found for /revise with non-existent version", () => {
        const res = request.post({
            url: `${schema_base_url}/revise`,
            body: {
                id: "sch/non-existent:9999",
                def: { foo: "bar" },
            },
            json: true,
        });

        expect(res.statusCode).to.be.oneOf([404, 410]);
        expect(res.json).to.have.property("error").that.is.true;
    });

    it("should reject /delete with invalid id format", () => {
        const res = request.del({
            url: `${schema_base_url}/delete`,
            body: {
                id: "invalid-id-format-without-slash-or-version",
            },
            json: true,
        });

        expect(res.statusCode).to.be.oneOf([400, 422]);
        expect(res.json).to.have.property("error").that.is.true;
    });

    it("should return not found for /delete of non-existent schema", () => {
        const res = request.del({
            url: `${schema_base_url}/delete`,
            body: {
                id: "sch/non-existent:1",
            },
            json: true,
        });

        expect(res.statusCode).to.be.oneOf([404, 410]);
        expect(res.json).to.have.property("error").that.is.true;
    });

    it("should return not found for /view of non-existent schema", () => {
        const res = request.get({
            url: `${schema_base_url}/view`,
            qs: { id: "sch/non-existent:1" },
            json: true,
        });

        expect(res.statusCode).to.be.oneOf([404, 410]);
        expect(res.json).to.have.property("error").that.is.true;
    });

    it("should reject /view with invalid id format", () => {
        const res = request.get({
            url: `${schema_base_url}/view`,
            qs: { id: "invalid-id-format" },
            json: true,
        });

        expect(res.statusCode).to.be.oneOf([400, 422]);
        expect(res.json).to.have.property("error").that.is.true;
    });

    it("should reject /search with invalid paging parameters", () => {
        const res = request.get({
            url: `${schema_base_url}/search`,
            qs: {
                // Assume offset/limit (or page/size) are validated in the implementation
                offset: -1,
                limit: 0,
            },
            json: true,
        });

        expect(res.statusCode).to.be.oneOf([400, 422]);
        expect(res.json).to.have.property("error").that.is.true;
    });
});

describe("simple test for schema/create", () => {
>>>>>>> REPLACE
</file_operation>
</file_operations>

<additional_changes>
1. Align the exact expected status codes with the implementation. For example, if the Foxx router uses `422` for validation errors instead of `400`, or uses `404` vs `410` for not-found semantics, update the `expect(res.statusCode).to.be.oneOf([...])` assertions to specific values.
2. Replace the generic `error` property assertions with whatever your Foxx error payload actually exposes (e.g. `errorNum`, `errorMessage`, or `code`). If your implementation uses symbolic codes like `ERR_INVALID_PARAM` and `ERR_NOT_FOUND`, add `expect` checks for those fields.
3. If your routes require authentication or an admin user, add the appropriate headers (e.g. `headers: { "x-user-id": "...", "x-role": "user" }`) and then add an extra negative test per endpoint using a non-admin or missing-auth context to exercise authorization failures.
4. If your actual route signatures differ (e.g. `/schema` vs `/schema/create`, or different HTTP verbs / parameter names), adjust the `url`, `body`, and `qs` usage in each test to match the real router definitions.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +83 to +92
let sch = null;
try {
logger.logRequestStarted({
client: req.queryParams?.client,
correlationId: req.headers["x-correlation-id"],
httpVerb: "POST",
routePath: basePath + "/create",
status: "Started",
description: "Create schema",
});
Copy link
Contributor

Choose a reason for hiding this comment

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

issue (bug_risk): Guard against using sch in the failure logger when it may still be null/undefined.

In /create, sch starts as null and is only set inside the transaction. If an error occurs before sch = g_db.sch.save(...) completes, the catch block will access sch.own_id, sch.id, and sch.desc, causing a new exception and hiding the original failure. Please either log sch directly (without field access) or only construct extra when sch is non-null.

@JoshuaSBrown JoshuaSBrown removed their assignment Dec 26, 2025
Copy link
Collaborator

@JoshuaSBrown JoshuaSBrown left a comment

Choose a reason for hiding this comment

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

A few comments:

  1. in line with what we have been doing else where include the schema id in the description
  2. secondly, limit the char printed output in the desc if you are going to print it
  3. 3rd, the pub and sys attributes indicate visibility where it makes sense and they are defined they should be output.
  4. never, include the def attribute in the output, this will be much larger than the desc. So don't just dump the schema document into the log output.

Comment on lines +164 to +168
extra: {
own_id: sch.own_id,
id: sch.id,
desc: sch.desc,
},
Copy link
Collaborator

Choose a reason for hiding this comment

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

Will this fail if sch is null?

Copy link
Collaborator

Choose a reason for hiding this comment

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

We don't want to include the desc. Or if we do, limit the char output. Incude pub and sys attributes if possible.

httpVerb: "POST",
routePath: basePath + "/create",
status: "Started",
description: "Create schema",
Copy link
Collaborator

Choose a reason for hiding this comment

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

It looks like the schema id is required, add it to the description.

httpVerb: "POST",
routePath: basePath + "/create",
status: "Success",
description: "Create schema",
Copy link
Collaborator

Choose a reason for hiding this comment

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

Add the schema id to the description

Copy link
Collaborator

Choose a reason for hiding this comment

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

The pub, and sys attributes are important for understanding the visibility of the schema that should be output, they should exist even though they are optional because they are default = True.

httpVerb: "POST",
routePath: basePath + "/create",
status: "Failure",
description: "Create schema",
Copy link
Collaborator

Choose a reason for hiding this comment

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

Add the schema id to the description.

httpVerb: "POST",
routePath: basePath + "/update",
status: "Failure",
description: "Update schema",
Copy link
Collaborator

Choose a reason for hiding this comment

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

Add the schema id to the description

httpVerb: "POST",
routePath: basePath + "/delete",
status: "Started",
description: `Delete schema. ID: ${req.queryParams.id}`,
Copy link
Collaborator

Choose a reason for hiding this comment

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

You did it here, not sure why you didn't do it on the other routes.

routePath: basePath + "/view",
status: "Failure",
description: `View schema. ID: ${req.queryParams.id}`,
extra: sch,
Copy link
Collaborator

Choose a reason for hiding this comment

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

This will print too much information, we don't want to include the desc. Or if we do, limit the char output.

Copy link
Collaborator

Choose a reason for hiding this comment

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

And we definately don't want to include the 'def' this will be bigger thant the desc.

own_id: sch.own_id,
own_nm: sch.own_nm,
id: sch.id,
desc: sch.desc,
Copy link
Collaborator

Choose a reason for hiding this comment

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

This will print too much information, we don't want to include the desc. Or if we do, limit the char output. Include pub, sys attributes.

httpVerb: "POST",
routePath: basePath + "/revise",
status: "Success",
description: "Revise schema",
Copy link
Collaborator

Choose a reason for hiding this comment

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

Include schema id

routePath: basePath + "/update",
status: "Success",
description: "Update schema",
extra: sch_new,
Copy link
Collaborator

Choose a reason for hiding this comment

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

We don't want to include the desc. Or if we do, limit the char output. Incude pub and sys attributes if possible.

Copy link
Collaborator

Choose a reason for hiding this comment

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

And we definately don't want to include the 'def' this will be bigger thant the desc.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Component: Foxx Foxx Arango micro services. Priority: Low Lower priority work. Type: Refactor Imlplementation change, same functionality

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants