Skip to content

feat(SITES-44690): add TaskManagementConnection, Ticket, TicketSuggestion, IdempotencyKey, and OAuthNonce data models#1702

Merged
prithipalpatwal merged 42 commits into
mainfrom
feat/SITES-44690-data-models
Jul 2, 2026
Merged

feat(SITES-44690): add TaskManagementConnection, Ticket, TicketSuggestion, IdempotencyKey, and OAuthNonce data models#1702
prithipalpatwal merged 42 commits into
mainfrom
feat/SITES-44690-data-models

Conversation

@prithipalpatwal

@prithipalpatwal prithipalpatwal commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Jira: SITES-44690

Summary

Adds five data models to spacecat-shared-data-access for the Jira integration feature (SITES-44690).

TaskManagementConnection

  • Represents an OAuth connection from an org to a task-management provider (e.g. Jira Cloud)
  • Status lifecycle: active → disabled / requires_reauth / error → disconnected (soft-delete)
  • Methods: isActive(), markActive(), markRequiresReauth(), markDisabled(), markError(),
    markDisconnected()
  • displayName and instanceUrl are mutable (updated on re-auth by auth-service)
  • metadata JSONB: { cloudId (required UUID), scopes (optional string[]) } — validated by validateMetadata()
  • updatedBy suppressed via postgrestIgnore (DB has no updated_by column)
  • Collection: findActiveByOrganizationAndProvider(orgId, provider)

Ticket

  • Represents a Jira issue created by ASO via a TaskManagementConnection
  • taskManagementConnectionIdpostgrestField: 'connection_id' (DB column name override)
  • Stores externalTicketId, ticketKey, ticketUrl, ticketStatus, ticketProvider, createdBy
  • Optional FK to Opportunity for single-suggestion queries without JOIN
  • updatedBy suppressed via postgrestIgnore (DB has no updated_by column)

TicketSuggestion

  • M:N bridge between Suggestions and Tickets (1:1 enforced in v1 via UNIQUE(suggestion_id) at DB level)
  • Append-only: allowUpdates(false) — DB grants no UPDATE privilege
  • updatedAt/updatedBy suppressed via postgrestIgnore (append-only, no DB columns)
  • GSI on suggestionId powers findBySuggestionId()

IdempotencyKey

  • Deduplicates concurrent or retried requests using the Stripe idempotency pattern
  • Status lifecycle: processing → completed | failed
  • key and endpoint are readOnly (set at creation, never changed)
  • updatedBy suppressed via postgrestIgnore (DB has no updated_by column)
  • Collection: findActiveKey(key, orgId) filters by expires_at > NOW(), deleteExpired() sweeps expired rows

OAuthNonce

  • Single-use nonce store for HMAC-signed OAuth state token replay prevention
  • Append-only: allowUpdates(false) — no UPDATE privilege
  • updatedAt/updatedBy suppressed via postgrestIgnore (append-only, no DB columns)
  • Custom delete({ nonce }) enforces expiry check (expires_at > NOW()) for atomic consume-on-use

🤖 Generated with Claude Code

TaskManagementConnection:
- stores per-org Jira OAuth connection state (active / requires_reauth / disconnected)
- metadata jsonb carries cloudId, siteUrl, scopeKey without schema churn
- schema GSI on (organizationId, provider, status) powers the O(1) lookup
  the ticket-creation API needs before every request
- isActive() / markRequiresReauth() business-level methods encode the
  connection lifecycle in one place

Ticket:
- records each Jira issue created by ASO: ticketId (Jira numeric ID for
  future updates), ticketKey (human key), ticketUrl, ticketStatus
- optional opportunityId FK keeps door open for ticket-less future flows

Both models follow monorepo conventions: SchemaBuilder, BaseModel/Collection,
index.js + index.d.ts re-exports, registered in EntityRegistry.

Co-authored-by: Cursor <cursoragent@cursor.com>
Add JSDoc note explaining that DISCONNECTED unifies the architecture
spec's 'disabled' (admin action) and 'error' (irrecoverable failure)
into a single terminal state for v1 simplicity; the two-state
distinction is deferred to v2 when admin controls are added.

Co-authored-by: Cursor <cursoragent@cursor.com>
@github-actions

Copy link
Copy Markdown

This PR will trigger a minor release when merged.

prithipalpatwal and others added 9 commits June 22, 2026 22:09
…tion tests

Two test fixes to unblock PR #1702 CI:

1. electrodb-service-construction.test.js: increase timeout from 2000ms
   (Mocha default) to 10000ms. Adding 2 entities (43 total, up from 41
   on main) pushed full-registry Service construction past 2000ms on
   CI's slower runners. The schemas are valid — the test passes with a
   realistic timeout (locally: ~1700ms per run).

2. Add task-management-connection.model+collection tests:
   - isActive() returns true for 'active', false for other statuses
   - markRequiresReauth() updates status and calls save(); propagates errors
   - findActiveByOrganizationAndProvider() delegates to the GSI lookup
     with status='active'; throws ValidationError on bad organizationId
     or missing provider

   These tests bring task-management-connection coverage to 100%
   (model.js + collection.js + index.js), matching the package threshold.

Co-authored-by: Cursor <cursoragent@cursor.com>
Both fields are required by the architecture spec (PR #150) and were
previously omitted without justification:

- ticketProvider (readOnly): denormalized from the connection so the
  audit record is self-contained even if the connection is later deleted
- createdBy (readOnly): IMS user ID of the person who created the ticket,
  sourced from the JWT sub claim at request time

Also updates ticket.model.js JSDoc to document the two new fields and
the remaining intentional v1 deviations (no status_synced_at, no
TicketSuggestion bridge model).

TypeScript declarations updated with getTicketProvider() and getCreatedBy().

Co-authored-by: Cursor <cursoragent@cursor.com>
Adds the ticket_suggestions bridge table model per architecture spec
(PR #150). Links a specific Suggestion to the Ticket created for it.

Key design decisions:
- UNIQUE (suggestion_id) enforced at the DB level (SQL migration #3)
  prevents the same suggestion from being ticketed twice in v1.
  In v2, relax to UNIQUE (suggestion_id, ticket_id) for grouped mode.
- suggestionId is a logical TEXT reference — suggestions live in
  DynamoDB/ElectroDB, not PostgreSQL, so no Postgres FK is declared.
- GSI on suggestionId auto-generates findBySuggestionId() for the
  "has this suggestion already been ticketed?" check.
- opportunityId is denormalized on the bridge row to avoid a JOIN
  through the suggestion store for opportunity-scoped queries.

Co-authored-by: Cursor <cursoragent@cursor.com>
…or/markDisconnected

Aligns TaskManagementConnection with the full spec (PR #150) status enum:
  active | disabled | requires_reauth | error | disconnected

Previously only active, requires_reauth, disconnected were defined, missing
spec's 'disabled' (admin-disabled) and 'error' (repeated API failures).

'disconnected' is a v1 soft-delete extension — the spec hard-deletes the row;
v1 keeps it with status='disconnected' for audit history.

New methods: markDisabled(), markError(), markDisconnected().
Tests updated to cover all five statuses and new mark* methods.

Co-authored-by: Cursor <cursoragent@cursor.com>
Implements spec §Metadata Validation Strategy:
- metadata-validator.js: per-provider schemas for jira_cloud (cloudId UUID,
  siteName required, siteUrl optional https) and jira_corp (baseUrl https).
  Enforces additionalProperties: false. Throws ValidationError on any violation.
- Unknown providers are rejected — no silent passthrough.
- validateMetadata() exported from the task-management-connection index so
  auth-service (connection creation path) can call it before any DB write.
- Unit tests: 10 cases covering required fields, UUID format, https scheme,
  extra properties, and unknown provider.

Co-authored-by: Cursor <cursoragent@cursor.com>
…ion doc

- Fix index.d.ts: add missing `export type * from './ticket-suggestion'`
- Fix TaskManagementConnection index.d.ts: add markDisabled/markError/markDisconnected
  method signatures and new getConnectedBy/getDisplayName/getInstanceUrl getters
- Fix metadata-validator: jira_cloud metadata is now { cloudId, scopes? } per
  PR #720 (mysticat-data-service) — siteName/siteUrl moved to dedicated
  display_name and instance_url columns, not stored in metadata JSONB
- Fix metadata-validator: remove dead v === undefined branch in jira_corp
  projectCategory validator; add tests for all uncovered branches (100% coverage)
- Fix task-management-connection.schema.js: add instanceUrl, displayName,
  connectedBy attributes matching NOT NULL columns in PR #720 Postgres schema
- Fix ticket.model.js: remove stale "No TicketSuggestion bridge model" comment —
  TicketSuggestion IS included in this PR
- Update model test fixture to use aligned metadata { cloudId, scopes }

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…nection

metadata JSONB is { cloudId, scopes } per PR #720, not { cloudId, siteUrl, scopeKey }.
Display fields live in instanceUrl/displayName columns.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@prithipalpatwal prithipalpatwal changed the title feat(SITES-44690): add TaskManagementConnection and Ticket data models feat(SITES-44690): add TaskManagementConnection, Ticket, and TicketSuggestion data models Jun 24, 2026

@MysticatBot MysticatBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey @prithipalpatwal,

Verdict: Request changes - four structural issues to address before merge; the code quality and patterns are solid otherwise.
Complexity: HIGH - large diff (1300+ lines, 22 files); database model schema with multiple entity relationships.
Changes: Adds TaskManagementConnection, Ticket, and TicketSuggestion data models with status lifecycle, metadata validation, and cascade relationships (22 files).
Note: CI checks are currently failing - expected per PR body (integration tests require mysticat-data-service PR #720).

Must fix before merge

  1. [Important] Ticket schema missing has_many TicketSuggestions with removeDependents: true - orphan records on cascade delete - ticket/ticket.schema.js (details inline)
  2. [Important] jira_corp metadata schema defined but unreachable through model layer - metadata-validator.js / task-management-connection.model.js (details inline)
  3. [Important] validateMetadata not wired into schema validation - invalid metadata can reach DB - task-management-connection.schema.js (details inline)
  4. [Important] metadata attribute default: {} conflicts with provider validation contracts - task-management-connection.schema.js (details inline)
Non-blocking (5): minor issues and suggestions
  • nit: instanceUrl uses weak startsWith('https://') check while ticketUrl uses the stronger isValidUrl() from shared-utils - inconsistent validation strength - task-management-connection.schema.js
  • nit: Ticket.DEFAULT_STATUS = 'To Do' uses a display-label string with spaces while all other status enums use snake_case identifiers - ticket/ticket.model.js:6
  • suggestion: ticketProvider on Ticket is unconstrained string while provider on TaskManagementConnection uses enum validation - consider type: Object.values(TaskManagementConnection.PROVIDERS) - ticket/ticket.schema.js
  • suggestion: scopes array in metadata validator has no length limit on the array or individual strings - add reasonable bounds - metadata-validator.js
  • suggestion: Consider adding markActive() convenience method for the re-auth reconnection flow - task-management-connection.model.js

Skill: pr-review | Model: us.anthropic.claude-opus-4-6-v1[1m] | Duration: 2m 42s | Cost: $8.82 | Commit: 1eadf949525929b3f5f245daf2e448bc4df71106
If this code review was useful, please react with 👍. Otherwise, react with 👎.

* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (blocking): The TicketSuggestion schema declares belongs_to Ticket, creating a FK relationship. However, this Ticket schema does not declare has_many TicketSuggestions with removeDependents: true. When a Ticket is deleted (via TaskManagementConnection cascade), TicketSuggestion records will be orphaned.

The cascade path is: Connection removal -> Tickets deleted -> TicketSuggestion rows left dangling. findBySuggestionId() would then return a TicketSuggestion whose getTicket() fails or returns null, making it appear a suggestion was ticketed when the ticket no longer exists.

Fix: Add .addReference('has_many', 'TicketSuggestions', ['createdAt'], { removeDependents: true }) to this schema.

// enforced as UUID format by a DB CHECK constraint.
// - scopes (optional) is the array from the Atlassian accessible-resources response;
// stored so permission gaps can be detected without re-calling Atlassian (e.g. missing
// manage:jira-webhook when v2 webhooks land).

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (blocking): The jira_corp metadata schema here is architecturally unreachable. The provider attribute in the connection schema constrains valid values to Object.values(TaskManagementConnection.PROVIDERS) which is only ['jira_cloud']. A record with provider: 'jira_corp' cannot be created through the model path.

This creates dead code that implies runtime coverage of a path that cannot execute. It will confuse the next engineer who touches this.

Fix: Either (a) remove the jira_corp entry and its tests until PROVIDERS is extended, or (b) add JIRA_CORP: 'jira_corp' to PROVIDERS now if it is genuinely needed for the auth-service pre-validation path (and document that the validator is called independently of the model layer).

})
// display_name column (PR #720): human-readable site name from Atlassian accessible-resources.
// Set by auth-service at OAuth callback time; never user-provided.
.addAttribute('displayName', {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (blocking): The metadata attribute uses type: 'any' with no validate callback. The exported validateMetadata() is never called from within the model/schema layer, meaning a TaskManagementConnection can be persisted with arbitrary metadata that violates the provider contract.

The current design relies on caller discipline with zero enforcement at the data layer. One missed call site and invalid metadata reaches the DB.

Fix: Override save() in the model to call validateMetadata(this.getProvider(), this.getMetadata()) before persisting. This follows the pattern used by other models in this codebase for co-presence invariants. Alternatively, wire it into the schema validate callback if the framework supports cross-field access.

// Set by auth-service at OAuth callback time; never user-provided.
.addAttribute('displayName', {
type: 'string',
required: true,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (blocking): The default: {} on metadata conflicts with the validation contract. For jira_cloud, an empty object is invalid (missing required cloudId). If a connection is ever created relying on this default, it will pass schema validation but silently store invalid state.

Since required: true already prevents null/undefined, the default serves no purpose other than masking an omission that should be a hard error.

Fix: Remove default: {} so that omitting metadata triggers a validation failure at creation time.

@MysticatBot MysticatBot added ai-reviewed Reviewed by AI complexity:high High complexity PR labels Jun 24, 2026

@MysticatBot MysticatBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey @prithipalpatwal,

Verdict: Request changes - four data-integrity and validation issues remain unaddressed since the prior review, plus one newly identified cascade gap.
Complexity: HIGH - large diff (1300+ lines, 22 files); database model schema with multiple entity relationships.
Changes: Adds TaskManagementConnection, Ticket, and TicketSuggestion data models with status lifecycle, metadata validation, and cascade relationships (22 files).
Note: CI checks are currently failing - expected per PR body (integration tests require mysticat-data-service PR #720).

Must fix before merge

  1. [Important] Ticket schema missing has_many TicketSuggestions with removeDependents: true - orphan records on cascade delete - ticket/ticket.schema.js:20 (details inline)
  2. [Important] Organization schema missing has_many TaskManagementConnections - cascade from org deletion will orphan connections and their child tickets - organization/organization.schema.js (not in diff - one-line addition needed: .addReference('has_many', 'TaskManagementConnections', ['updatedAt'], { removeDependents: true }) following the existing Sites/Projects/Entitlements pattern)
  3. [Important] instanceUrl uses weak startsWith('https://') while sibling ticketUrl uses isValidUrl() from shared-utils - inconsistent validation strength - task-management-connection.schema.js:48 (details inline)
  4. [Important] metadata attribute default: {} conflicts with validateMetadata('jira_cloud', {}) which requires cloudId - creates schema-valid but business-invalid state - task-management-connection.schema.js:60 (details inline)
Non-blocking (5): minor issues and suggestions
  • nit: jira_corp metadata schema in metadata-validator.js is unreachable through model layer (PROVIDERS enum only has jira_cloud) - add a comment noting it is forward-looking, or remove until PROVIDERS is extended
  • suggestion: Wire validateMetadata into the schema validate callback or add a prominent JSDoc on the metadata attribute noting that callers MUST call it externally - the current implicit contract is invisible to future contributors - task-management-connection.schema.js:57
  • nit: scopes array in metadata validator has no length limit on the array or individual strings - add reasonable bounds (e.g., max 100 entries, max 256 chars each) - metadata-validator.js:48
  • nit: validateMetadata does not guard against non-object input (null/undefined/string) - will throw TypeError instead of clean ValidationError - metadata-validator.js:73
  • suggestion: Add the 3 new entities to the README's "Current exported entities" list - packages/spacecat-shared-data-access/README.md

Skill: pr-review | Model: us.anthropic.claude-opus-4-6-v1[1m] | Duration: 3m 3s | Cost: $8.14 | Commit: 9b00cf55343c95dd6c9bc628a44fb8c76b584255
If this code review was useful, please react with 👍. Otherwise, react with 👎.

import SchemaBuilder from '../base/schema.builder.js';
import Ticket from './ticket.model.js';
import TicketCollection from './ticket.collection.js';

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (blocking): The cascade chain is incomplete. TaskManagementConnection correctly declares has_many Tickets with removeDependents: true, but this Ticket schema has no corresponding has_many TicketSuggestions. When a Ticket is deleted (via Connection cascade), TicketSuggestion rows are orphaned. The findBySuggestionId() GSI will then return bridge records pointing at deleted tickets, making it appear a suggestion is still ticketed.

The Opportunity schema in this codebase uses the identical pattern: .addReference("has_many", "Suggestions", ["updatedAt"], { removeDependents: true }).

Fix: Add .addReference('has_many', 'TicketSuggestions', ['updatedAt'], { removeDependents: true }) to this schema.

// calls route through the fixed Atlassian gateway keyed on cloudId from metadata).
.addAttribute('instanceUrl', {
type: 'string',
required: true,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (blocking): instanceUrl validates with value.startsWith("https://") which accepts malformed values like https://, https:// , or strings with control characters. The Ticket schema uses the stricter isValidUrl() from @adobe/spacecat-shared-utils for ticketUrl. Since instanceUrl is stored permanently and rendered in UIs, it should use the same validation.

Fix:

import { isValidUrl } from '@adobe/spacecat-shared-utils';
// ...
validate: (value) => isValidUrl(value),

})
// metadata JSONB (PR #720): provider-specific structured data.
// jira_cloud: { cloudId (required UUID), scopes (optional string array) }.
// siteName and siteUrl are NOT stored here — they live in displayName/instanceUrl above.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (blocking): metadata has default: {} but validateMetadata('jira_cloud', {}) throws because cloudId is required. Since required: true already ensures callers must provide the field, this default only fires when metadata is omitted - creating a record that passes schema validation but violates the business invariant. Any reader trusting metadata.cloudId on such a record will encounter undefined.

Fix: Remove default: {}. The required: true constraint already enforces that callers provide metadata explicitly.

prithipalpatwal and others added 9 commits June 24, 2026 16:56
… schema

Adds the externalInstanceId attribute (required, readOnly) to align with the
new external_instance_id column in mysticat-data-service PR #720.

- Add externalInstanceId attribute to schema (string, required, readOnly)
- Add externalInstanceId to mockRecord in model test
- Add schema test file covering the new attribute

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…sues

- Add OAuthNonce model (schema, model, collection, index) with custom
  delete({ nonce }) method for atomic single-use replay prevention
- Register OAuthNonce in EntityRegistry and models/index.js
- Fix TicketSuggestion.opportunityId: required: false → required: true
  (matches DB NOT NULL constraint; avoids opaque 500 on create)
- Fix Ticket.ticketStatus: required: true → required: false, default: null
  (DB column is nullable; status is set by async sync job, not on create)
- Remove Ticket.DEFAULT_STATUS = 'To Do' (Jira-specific constant no
  longer referenced; provider defaults belong in the client layer)
- Add TODO tracking lastUsedAt/errorMessage missing from
  TaskManagementConnection schema (follow-up)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Ticket schema: add has_many TicketSuggestions with removeDependents to prevent orphan bridge rows on ticket deletion
- Organization schema: add has_many TaskManagementConnections for cascade and traversal support
- TaskManagementConnection schema: fix instanceUrl validation from startsWith to isValidUrl (proper URL parsing)
- TaskManagementConnection schema: remove metadata default {} — empty object bypassed cloudId required check at creation time
- TaskManagementConnection model: add markActive() for auth-service re-auth path to restore requires_reauth connections

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…-44690)

jira_corp is not in PROVIDERS enum and has no v1 implementation path.
Remove dead schema and its tests; add v1 comment pointing to where v2
providers should be added.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…anagementConnection schema

Both columns already exist in the DB migration (mysticat-data-service PR #720)
but were missing from the ORM schema. Adds the attributes so the model API
is complete and consistent with the database.

Co-authored-by: Cursor <cursoragent@cursor.com>
…ment models (SITES-44690)

- Add OAuthNonce and OAuthNonceCollection TypeScript declarations with
  atomic delete method signature
- Add getTaskManagementConnections() to Organization interface
- Add getExternalInstanceId() and markActive() to TaskManagementConnection
- Export oauth-nonce types from models barrel

Co-authored-by: Cursor <cursoragent@cursor.com>
…ts, remove unused imports

- Rename ticketId attribute to externalTicketId to resolve ORM PK
  collision (entityNameToIdName('Ticket') = 'ticketId' clashed with
  the Jira internal ID attribute)
- Remove unused Opportunity and TaskManagementConnection imports
  from Ticket TS declarations
- Add unit tests for Ticket and TicketSuggestion schemas

Co-authored-by: Cursor <cursoragent@cursor.com>
prithipalpatwal and others added 9 commits July 2, 2026 00:38
…alues and check pk

getIndexes() returns an object map, not an array; suggestionId is in pk, not sk.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…idator

Lines 72-73 were uncovered; add null and array cases to hit the guard.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…delete

Lines 35-36 were uncovered; add test for empty nonce input.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… TaskManagementConnection

Both fields are updated by auth-service on re-auth (user may reconnect to a
different Jira site or Atlassian may rename the site). readOnly: true prevented
setDisplayName() and setInstanceUrl() from being generated, causing a runtime
TypeError on the re-auth path in spacecat-auth-service PR #595.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…cript types for new entities

The DB tickets table uses connection_id (not task_management_connection_id).
entityNameToIdName('TaskManagementConnection') generates taskManagementConnectionId
which auto-maps to task_management_connection_id — wrong column, runtime failure.
Override with postgrestField: 'connection_id' to match the actual DB migration (#720).

Also adds OAuthNonce, TaskManagementConnection, Ticket, TicketSuggestion to the
DataAccess TypeScript interface in index.d.ts so TS consumers get proper types.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…hose DB columns

The SchemaBuilder auto-adds updatedAt and updatedBy attributes with defaults.
These get included in every INSERT payload via toDbRecord/toDbMap. None of the
4 new tables have an updated_by column; ticket_suggestions and oauth_nonces
also lack updated_at. Without suppression, every INSERT fails with
"column updated_by does not exist" from PostgREST.

Fix: override those attributes with postgrestIgnore: true in each schema so
createFieldMaps excludes them from toDbMap and they are never sent to PostgREST.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…s(false) (SITES-44690)

Both tables have no UPDATE grant in the DB. Without allowUpdates(false), setters are
generated and a mutate+save() call fails with an opaque PostgREST 403 instead of a
clean ValidationError at the model layer. Aligns with the pattern used by Audit and
AccessGrantLog.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…new entities (SITES-44690)

- Add setDisplayName/setInstanceUrl to TaskManagementConnection.d.ts (both attrs
  are mutable since PR 623841d removed readOnly, but setters were missing from
  the TS interface, causing type errors for TypeScript consumers)
- Add getOrganization() to TaskManagementConnection.d.ts (belongs_to reference
  getter was generated at runtime but not declared)
- Add getOrganization(), getTaskManagementConnection(), getOpportunity(),
  getTicketSuggestions() to Ticket.d.ts (reference getters missing; pattern
  verified against Opportunity.d.ts which includes getSite()/getAudit())
- Fix schema comment: allByOrganizationIdAndProviderAndStatus →
  findByOrganizationIdAndProviderAndStatus (collection calls findBy, not allBy)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

@MysticatBot MysticatBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey @prithipalpatwal,

Verdict: Request changes - one data-integrity gap in OAuthNonce expiry enforcement at consume time.
Complexity: HIGH - large diff (2200+ lines, 36 files); database model schema with multiple entity relationships.
Changes: Adds TaskManagementConnection, Ticket, TicketSuggestion, and OAuthNonce data models with status lifecycle, metadata validation, and cascade relationships (36 files).
Note: CI checks are currently failing - expected per PR body (integration tests require mysticat-data-service PR #720).

Must fix before merge

  1. [Important] OAuthNonce delete() does not enforce expiry - expired nonces can still be consumed successfully - oauth-nonce.collection.js:34 (details inline)
Non-blocking (3): minor issues and suggestions
  • suggestion: ticketProvider on Ticket is an unconstrained string while provider on TaskManagementConnection uses enum validation - consider constraining with type: Object.values(TaskManagementConnection.PROVIDERS) for consistency - ticket.schema.js:51
  • suggestion: scopes array in metadata validator has no length limit on the array or individual strings - add reasonable bounds to prevent JSONB bloat - metadata-validator.js:48
  • nit: suggestionId and opportunityId on TicketSuggestion have no format validation (e.g. minimum length or UUID check) - low risk since fields are readOnly but would prevent empty-ish values - ticket-suggestion.schema.js:14

Previously flagged, now resolved

  • Ticket schema now has has_many TicketSuggestions with removeDependents: true
  • Organization schema now has has_many TaskManagementConnections
  • instanceUrl now validates with isValidUrl() (consistent with ticketUrl)
  • metadata attribute is required: true with no default (no conflict with validateMetadata contracts)
  • validateMetadata wired into schema via set hook on metadata attribute
  • validateMetadata guards against non-object input (null, array, string)
  • OAuthNonceCollection.delete() validates nonce input before query

Skill: pr-review | Model: us.anthropic.claude-opus-4-6-v1[1m] | Duration: 0m 5s | Cost: $9.48 | Commit: 7e6fa58d3a99707c6d964bd49fc1c4a9e91a3688
If this code review was useful, please react with 👍. Otherwise, react with 👎.

throw new Error('nonce is required and must be a non-empty string');
}
const { data, error } = await this.postgrestService
.from(this.tableName)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (blocking): The delete() method consumes a nonce without checking expires_at. The mysticat-data-service migration (PR #720) documents the intended consume pattern as:

DELETE FROM oauth_nonces WHERE nonce = $1 AND expires_at > NOW() RETURNING id

The current implementation only does .delete().eq("nonce", nonce).select() without an expiry filter. This means an expired nonce that has not yet been swept by the cleanup job can still be consumed successfully, returning 1 to the caller. The auth-service would then proceed with the OAuth callback despite the state token being outside its validity window.

The 10-minute TTL is a security control (limits replay window), not just a space-management hint. Without enforcement at consume time, the nonce TTL is advisory.

Fix: Add an expiry filter to the PostgREST chain:

const { data, error } = await this.postgrestService
  .from(this.tableName)
  .delete()
  .eq('nonce', nonce)
  .gt('expires_at', new Date().toISOString())
  .select();

This ensures expired nonces are rejected at consume time regardless of cleanup job timing.

…44690)

The delete query now filters by `expires_at > NOW()` so expired nonces
return 0 (rejected) even if the row has not been swept by the cleanup job.
This matches the intended behaviour described in the DB migration:
  DELETE FROM oauth_nonces WHERE nonce = $1 AND expires_at > NOW()

Without this check, an attacker who obtained an expired state token could
replay it during the window between expiry and the next cleanup sweep.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

@MysticatBot MysticatBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey @prithipalpatwal,

Verdict: Approve - the expiry enforcement fix is correct; all prior blocking findings resolved.
Complexity: HIGH - large diff (2200+ lines, 36 files); database model schema with multiple entity relationships.
Changes: Adds TaskManagementConnection, Ticket, TicketSuggestion, and OAuthNonce data models with status lifecycle, metadata validation, and cascade relationships (36 files).

Previously flagged, now resolved

  • OAuthNonce delete() now enforces expiry via .gt('expires_at', now) - expired nonces return 0 (rejected)
  • Ticket schema has has_many TicketSuggestions with removeDependents: true
  • Organization schema has has_many TaskManagementConnections
  • instanceUrl validates with isValidUrl() (consistent with ticketUrl)
  • metadata is required: true with no default (no conflict with validateMetadata contracts)
  • validateMetadata wired into schema via set hook on metadata attribute
  • validateMetadata guards against non-object input (null, array, string)
  • OAuthNonceCollection.delete() validates nonce input before query

Skill: pr-review | Model: us.anthropic.claude-opus-4-6-v1[1m] | Duration: 3m 39s | Cost: $1.71 | Commit: 15d0e43576d85d57410ed640ea84a5ed0fd789e7
If this code review was useful, please react with 👍. Otherwise, react with 👎.

prithipalpatwal and others added 3 commits July 2, 2026 13:00
Adds IdempotencyKey entity (schema, model, collection) for the idempotency_keys
table from mysticat-data-service PR #720. Replaces raw PostgREST access in the
API service with a proper data model that provides camelCase field mapping,
validation, and enum-typed status values.

Key design decisions:
- belongs_to Organization (FK with cascade)
- key + endpoint are readOnly (set at creation, never changed)
- status enum: processing | completed | failed (default: processing)
- updatedBy suppressed via postgrestIgnore (DB has no updated_by column)
- Collection methods: findActiveKey(key, orgId) filters by expires_at > NOW(),
  deleteExpired() sweeps rows where expires_at < NOW()

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…rror (SITES-44690)

PostgREST returns plain error objects ({ code, message }), not Error
instances. The IT all-collections-methods-coverage test expects thrown
errors to be instanceof Error. Wrap with DataAccessError (consistent
with how BaseCollection handles PostgREST errors internally).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

@MysticatBot MysticatBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey @prithipalpatwal,

Verdict: Approve - IdempotencyKey model follows established patterns well; two minor TypeScript definition gaps noted.
Complexity: HIGH - large diff (2200+ lines, 42 files); database model schema with multiple entity relationships.
Changes: Adds TaskManagementConnection, Ticket, TicketSuggestion, IdempotencyKey, and OAuthNonce data models with status lifecycle, metadata validation, and cascade relationships (42 files).

Non-blocking (2): minor issues and suggestions
  • suggestion: expiresAt is not marked readOnly in the schema, so BaseModel generates a setter, but setExpiresAt is absent from index.d.ts - either add readOnly: true (idempotency keys should not have their expiry extended post-creation) or add setExpiresAt(value: string): IdempotencyKey to the type definitions - idempotency-key/index.d.ts
  • nit: getResponse() is typed as object | null but the schema declares type: 'any' - if the response payload is always a JSON object this is fine for the domain, but consider Record<string, unknown> | null or unknown for strict TS consumers - idempotency-key/index.d.ts

Previously flagged, now resolved

  • OAuthNonce delete() now enforces expiry via .gt('expires_at', now) - expired nonces return 0 (rejected)
  • Ticket schema has has_many TicketSuggestions with removeDependents: true
  • Organization schema has has_many TaskManagementConnections
  • instanceUrl validates with isValidUrl() (consistent with ticketUrl)
  • metadata is required: true with no default (no conflict with validateMetadata contracts)
  • validateMetadata wired into schema via set hook on metadata attribute
  • validateMetadata guards against non-object input (null, array, string)
  • OAuthNonceCollection.delete() validates nonce input before query

Skill: pr-review | Model: us.anthropic.claude-opus-4-6-v1[1m] | Duration: 0m 22s | Cost: $4.46 | Commit: 2a5e0dc27b52e5d06ee28cb36baa940ed39104de
If this code review was useful, please react with 👍. Otherwise, react with 👎.

…en response type (SITES-44690)

- expiresAt: add readOnly: true — expiry must not be extended post-creation,
  that would undermine the idempotency contract
- response: change TS type from object | null to Record<string, unknown> | null
  for strict TypeScript consumers

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

@MysticatBot MysticatBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey @prithipalpatwal,

Verdict: Approve - the readOnly fix and type tightening are correct; all prior blocking findings remain resolved.
Complexity: HIGH - large diff (2200+ lines, 42 files); database model schema with multiple entity relationships.
Changes: Marks IdempotencyKey.expiresAt as readOnly and tightens response type to Record<string, unknown> (3 files).

Previously flagged, now resolved

  • OAuthNonce delete() now enforces expiry via .gt('expires_at', now) - expired nonces return 0 (rejected)
  • Ticket schema has has_many TicketSuggestions with removeDependents: true
  • Organization schema has has_many TaskManagementConnections
  • instanceUrl validates with isValidUrl() (consistent with ticketUrl)
  • metadata is required: true with no default (no conflict with validateMetadata contracts)
  • validateMetadata wired into schema via set hook on metadata attribute
  • validateMetadata guards against non-object input (null, array, string)
  • OAuthNonceCollection.delete() validates nonce input before query
  • IdempotencyKey.expiresAt now marked readOnly (prevents expiry extension post-creation)
  • Response type tightened to Record<string, unknown> | null (stricter than bare object)

Skill: pr-review | Model: us.anthropic.claude-opus-4-6-v1[1m] | Duration: 1m 24s | Cost: $2.56 | Commit: e54e75f9e72abc7c71b703f5812a80e3723a8de2
If this code review was useful, please react with 👍. Otherwise, react with 👎.

@prithipalpatwal prithipalpatwal changed the title feat(SITES-44690): add TaskManagementConnection, Ticket, and TicketSuggestion data models feat(SITES-44690): add TaskManagementConnection, Ticket, TicketSuggestion, IdempotencyKey, and OAuthNonce data models Jul 2, 2026
@prithipalpatwal prithipalpatwal merged commit 2ca5096 into main Jul 2, 2026
5 checks passed
@prithipalpatwal prithipalpatwal deleted the feat/SITES-44690-data-models branch July 2, 2026 09:38
solaris007 pushed a commit that referenced this pull request Jul 2, 2026
## [@adobe/spacecat-shared-data-access-v3.81.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-data-access-v3.80.0...@adobe/spacecat-shared-data-access-v3.81.0) (2026-07-02)

### Features

* **SITES-44690:** add TaskManagementConnection, Ticket, TicketSuggestion, IdempotencyKey, and OAuthNonce data models ([#1702](#1702)) ([2ca5096](2ca5096))
@solaris007

Copy link
Copy Markdown
Member

🎉 This PR is included in version @adobe/spacecat-shared-data-access-v3.81.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

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

Labels

ai-reviewed Reviewed by AI complexity:high High complexity PR released

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants