feat(SITES-44690): add TaskManagementConnection, Ticket, TicketSuggestion, IdempotencyKey, and OAuthNonce data models#1702
Conversation
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>
|
This PR will trigger a minor release when merged. |
…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>
There was a problem hiding this comment.
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
- [Important] Ticket schema missing
has_many TicketSuggestionswithremoveDependents: true- orphan records on cascade delete -ticket/ticket.schema.js(details inline) - [Important]
jira_corpmetadata schema defined but unreachable through model layer -metadata-validator.js/task-management-connection.model.js(details inline) - [Important]
validateMetadatanot wired into schema validation - invalid metadata can reach DB -task-management-connection.schema.js(details inline) - [Important]
metadataattributedefault: {}conflicts with provider validation contracts -task-management-connection.schema.js(details inline)
Non-blocking (5): minor issues and suggestions
- nit:
instanceUrluses weakstartsWith('https://')check whileticketUrluses the strongerisValidUrl()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:
ticketProvideron Ticket is unconstrained string whileprovideron TaskManagementConnection uses enum validation - considertype: Object.values(TaskManagementConnection.PROVIDERS)-ticket/ticket.schema.js - suggestion:
scopesarray 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. |
There was a problem hiding this comment.
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). |
There was a problem hiding this comment.
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', { |
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
- [Important] Ticket schema missing
has_many TicketSuggestionswithremoveDependents: true- orphan records on cascade delete -ticket/ticket.schema.js:20(details inline) - [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) - [Important]
instanceUrluses weakstartsWith('https://')while siblingticketUrlusesisValidUrl()from shared-utils - inconsistent validation strength -task-management-connection.schema.js:48(details inline) - [Important]
metadataattributedefault: {}conflicts withvalidateMetadata('jira_cloud', {})which requirescloudId- creates schema-valid but business-invalid state -task-management-connection.schema.js:60(details inline)
Non-blocking (5): minor issues and suggestions
- nit:
jira_corpmetadata schema inmetadata-validator.jsis unreachable through model layer (PROVIDERS enum only hasjira_cloud) - add a comment noting it is forward-looking, or remove until PROVIDERS is extended - suggestion: Wire
validateMetadatainto the schemavalidatecallback or add a prominent JSDoc on themetadataattribute noting that callers MUST call it externally - the current implicit contract is invisible to future contributors -task-management-connection.schema.js:57 - nit:
scopesarray 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:
validateMetadatadoes 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'; | ||
|
|
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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.
… 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>
…to feat/SITES-44690-data-models
…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>
…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>
There was a problem hiding this comment.
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
- [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:
ticketProvideron Ticket is an unconstrained string whileprovideron TaskManagementConnection uses enum validation - consider constraining withtype: Object.values(TaskManagementConnection.PROVIDERS)for consistency -ticket.schema.js:51 - suggestion:
scopesarray 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:
suggestionIdandopportunityIdon 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 TicketSuggestionswithremoveDependents: true - Organization schema now has
has_many TaskManagementConnections instanceUrlnow validates withisValidUrl()(consistent withticketUrl)metadataattribute isrequired: truewith no default (no conflict withvalidateMetadatacontracts)validateMetadatawired into schema viasethook on metadata attributevalidateMetadataguards 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) |
There was a problem hiding this comment.
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 idThe 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>
There was a problem hiding this comment.
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 TicketSuggestionswithremoveDependents: true - Organization schema has
has_many TaskManagementConnections instanceUrlvalidates withisValidUrl()(consistent withticketUrl)metadataisrequired: truewith no default (no conflict withvalidateMetadatacontracts)validateMetadatawired into schema viasethook on metadata attributevalidateMetadataguards 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 👎.
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>
There was a problem hiding this comment.
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:
expiresAtis not markedreadOnlyin the schema, so BaseModel generates a setter, butsetExpiresAtis absent fromindex.d.ts- either addreadOnly: true(idempotency keys should not have their expiry extended post-creation) or addsetExpiresAt(value: string): IdempotencyKeyto the type definitions -idempotency-key/index.d.ts - nit:
getResponse()is typed asobject | nullbut the schema declarestype: 'any'- if the response payload is always a JSON object this is fine for the domain, but considerRecord<string, unknown> | nullorunknownfor 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 TicketSuggestionswithremoveDependents: true - Organization schema has
has_many TaskManagementConnections instanceUrlvalidates withisValidUrl()(consistent withticketUrl)metadataisrequired: truewith no default (no conflict withvalidateMetadatacontracts)validateMetadatawired into schema viasethook on metadata attributevalidateMetadataguards 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>
There was a problem hiding this comment.
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 👎.
## [@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))
|
🎉 This PR is included in version @adobe/spacecat-shared-data-access-v3.81.0 🎉 The release is available on: Your semantic-release bot 📦🚀 |
Jira: SITES-44690
Summary
Adds five data models to
spacecat-shared-data-accessfor the Jira integration feature (SITES-44690).TaskManagementConnection
active → disabled / requires_reauth / error → disconnected(soft-delete)isActive(),markActive(),markRequiresReauth(),markDisabled(),markError(),markDisconnected()displayNameandinstanceUrlare mutable (updated on re-auth by auth-service)metadataJSONB:{ cloudId (required UUID), scopes (optional string[]) }— validated byvalidateMetadata()updatedBysuppressed viapostgrestIgnore(DB has noupdated_bycolumn)findActiveByOrganizationAndProvider(orgId, provider)Ticket
taskManagementConnectionId→postgrestField: 'connection_id'(DB column name override)externalTicketId,ticketKey,ticketUrl,ticketStatus,ticketProvider,createdByOpportunityfor single-suggestion queries without JOINupdatedBysuppressed viapostgrestIgnore(DB has noupdated_bycolumn)TicketSuggestion
UNIQUE(suggestion_id)at DB level)allowUpdates(false)— DB grants no UPDATE privilegeupdatedAt/updatedBysuppressed viapostgrestIgnore(append-only, no DB columns)suggestionIdpowersfindBySuggestionId()IdempotencyKey
processing → completed | failedkeyandendpointare readOnly (set at creation, never changed)updatedBysuppressed viapostgrestIgnore(DB has noupdated_bycolumn)findActiveKey(key, orgId)filters byexpires_at > NOW(),deleteExpired()sweeps expired rowsOAuthNonce
allowUpdates(false)— no UPDATE privilegeupdatedAt/updatedBysuppressed viapostgrestIgnore(append-only, no DB columns)delete({ nonce })enforces expiry check (expires_at > NOW()) for atomic consume-on-use🤖 Generated with Claude Code