Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
0d752bc
feat(SITES-44690): add TaskManagementConnection and Ticket data models
prithipalpatwal Jun 22, 2026
1670b27
docs(data-models): clarify STATUSES.DISCONNECTED v1 scope intent
prithipalpatwal Jun 22, 2026
ed28a09
test(data-models): fix electrodb timeout and add TaskManagementConnec…
prithipalpatwal Jun 22, 2026
470893a
Merge branch 'main' into feat/SITES-44690-data-models
prithipalpatwal Jun 22, 2026
9759dd1
feat(data-models): add ticketProvider and createdBy to Ticket schema
prithipalpatwal Jun 22, 2026
fd9d00f
feat(data-models): add TicketSuggestion bridge model
prithipalpatwal Jun 22, 2026
956066e
fix(data-models): add disabled, error statuses + markDisabled/markErr…
prithipalpatwal Jun 23, 2026
c259842
feat(data-models): add metadata validation + export validateMetadata
prithipalpatwal Jun 23, 2026
f16ed27
Merge branch 'main' into feat/SITES-44690-data-models
prithipalpatwal Jun 24, 2026
7066e87
fix(data-models): align task-management models with PR #720 and solut…
prithipalpatwal Jun 24, 2026
1eadf94
fix(data-models): correct stale metadata comment in TaskManagementCon…
prithipalpatwal Jun 24, 2026
9b00cf5
Merge branch 'main' into feat/SITES-44690-data-models
prithipalpatwal Jun 24, 2026
93ee359
feat(data-models): add externalInstanceId to TaskManagementConnection…
prithipalpatwal Jun 24, 2026
e647141
Merge remote-tracking branch 'origin/feat/SITES-44690-data-models' in…
prithipalpatwal Jun 24, 2026
ee44973
feat(data-models): add OAuthNonce model and fix schema correctness is…
prithipalpatwal Jun 24, 2026
8a8f0f2
fix(data-models): address PR #1702 review blockers (SITES-44690)
prithipalpatwal Jun 24, 2026
15613d5
fix(data-models): remove unreachable jira_corp metadata schema (SITES…
prithipalpatwal Jun 24, 2026
33e771e
Merge remote-tracking branch 'origin/main' into feat/SITES-44690-data…
prithipalpatwal Jun 24, 2026
c06aac4
fix(data-models): add lastUsedAt and errorMessage attributes to TaskM…
prithipalpatwal Jun 25, 2026
63b0b73
fix(data-models): add missing TypeScript declarations for task-manage…
prithipalpatwal Jun 25, 2026
c6d15cf
fix(data-access): rename ticketId to externalTicketId, add schema tes…
prithipalpatwal Jun 25, 2026
cb77699
Merge branch 'main' into feat/SITES-44690-data-models
prithipalpatwal Jun 25, 2026
3a5cbc2
fix(data-access): bump IT data-service image to v5.48.1
prithipalpatwal Jun 25, 2026
a331cc4
fix(data-access): guard validateMetadata and OAuthNonce.delete agains…
prithipalpatwal Jun 26, 2026
7be1031
feat(data-access): add connectedAt attribute to TaskManagementConnection
prithipalpatwal Jun 26, 2026
e2c95cb
fix(data-access): use isIsoDate, wire metadata validation, fix copyri…
prithipalpatwal Jun 26, 2026
1d86ea2
Merge branch 'main' into feat/SITES-44690-data-models
prithipalpatwal Jul 1, 2026
332d9c5
fix(data-access): correct TicketSuggestion index test to use Object.v…
prithipalpatwal Jul 1, 2026
eca5cb3
test(data-access): cover null/array metadata branches in metadata-val…
prithipalpatwal Jul 1, 2026
ad35864
test(data-access): cover missing-nonce guard in OAuthNonceCollection.…
prithipalpatwal Jul 1, 2026
623841d
fix(data-access): remove readOnly from displayName and instanceUrl on…
prithipalpatwal Jul 1, 2026
84c7399
fix(data-access): map Ticket FK to connection_id column and add TypeS…
prithipalpatwal Jul 1, 2026
176dd8c
fix(data-access): suppress updatedAt/updatedBy for entities without t…
prithipalpatwal Jul 1, 2026
86f44bb
fix(data-access): mark OAuthNonce and TicketSuggestion as allowUpdate…
prithipalpatwal Jul 1, 2026
7e6fa58
fix(data-access): fix TypeScript declarations and schema comment for …
prithipalpatwal Jul 1, 2026
15d0e43
fix(data-access): enforce expiry check in OAuthNonce.delete() (SITES-…
prithipalpatwal Jul 1, 2026
787ea52
Merge branch 'main' into feat/SITES-44690-data-models
prithipalpatwal Jul 2, 2026
1702148
feat(data-access): add IdempotencyKey data model (SITES-44690)
prithipalpatwal Jul 2, 2026
2a5e0dc
fix(data-access): wrap IdempotencyKey PostgREST errors in DataAccessE…
prithipalpatwal Jul 2, 2026
e54e75f
fix(data-access): mark IdempotencyKey.expiresAt as readOnly and tight…
prithipalpatwal Jul 2, 2026
3c3dcc5
Merge branch 'main' into feat/SITES-44690-data-models
prithipalpatwal Jul 2, 2026
b772da1
Merge branch 'main' into feat/SITES-44690-data-models
prithipalpatwal Jul 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ import SentimentGuidelineCollection from '../sentiment-guideline/sentiment-guide
import SentimentTopicCollection from '../sentiment-topic/sentiment-topic.collection.js';
import AccessGrantLogCollection from '../access-grant-log/access-grant-log.collection.js';
import SiteImsOrgAccessCollection from '../site-ims-org-access/site-ims-org-access.collection.js';
import TaskManagementConnectionCollection from '../task-management-connection/task-management-connection.collection.js';
import TicketCollection from '../ticket/ticket.collection.js';
import TicketSuggestionCollection from '../ticket-suggestion/ticket-suggestion.collection.js';

import ApiKeySchema from '../api-key/api-key.schema.js';
import AsyncJobSchema from '../async-job/async-job.schema.js';
Expand Down Expand Up @@ -97,6 +100,9 @@ import SentimentGuidelineSchema from '../sentiment-guideline/sentiment-guideline
import SentimentTopicSchema from '../sentiment-topic/sentiment-topic.schema.js';
import AccessGrantLogSchema from '../access-grant-log/access-grant-log.schema.js';
import SiteImsOrgAccessSchema from '../site-ims-org-access/site-ims-org-access.schema.js';
import TaskManagementConnectionSchema from '../task-management-connection/task-management-connection.schema.js';
import TicketSchema from '../ticket/ticket.schema.js';
import TicketSuggestionSchema from '../ticket-suggestion/ticket-suggestion.schema.js';

/**
* EntityRegistry - A registry class responsible for managing entities, their schema and collection.
Expand Down Expand Up @@ -234,6 +240,9 @@ EntityRegistry.registerEntity(SentimentGuidelineSchema, SentimentGuidelineCollec
EntityRegistry.registerEntity(SentimentTopicSchema, SentimentTopicCollection);
EntityRegistry.registerEntity(AccessGrantLogSchema, AccessGrantLogCollection);
EntityRegistry.registerEntity(SiteImsOrgAccessSchema, SiteImsOrgAccessCollection);
EntityRegistry.registerEntity(TaskManagementConnectionSchema, TaskManagementConnectionCollection);
EntityRegistry.registerEntity(TicketSchema, TicketCollection);
EntityRegistry.registerEntity(TicketSuggestionSchema, TicketSuggestionCollection);
EntityRegistry.defaultEntities = { ...EntityRegistry.entities };

export default EntityRegistry;
3 changes: 3 additions & 0 deletions packages/spacecat-shared-data-access/src/models/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ export type * from './scrape-job';
export type * from './scrape-url';
export type * from './sentiment-guideline';
export type * from './sentiment-topic';
export type * from './task-management-connection';
export type * from './ticket';
export type * from './ticket-suggestion';
export type * from './site';
export type * from './site-candidate';
export type * from './site-enrollment';
Expand Down
3 changes: 3 additions & 0 deletions packages/spacecat-shared-data-access/src/models/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,6 @@ export * from './page-citability/index.js';
export * from './plg-onboarding/index.js';
export * from './sentiment-guideline/index.js';
export * from './sentiment-topic/index.js';
export * from './task-management-connection/index.js';
export * from './ticket/index.js';
export * from './ticket-suggestion/index.js';
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
* Copyright 2024 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* 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.
*/

import type { BaseCollection, BaseModel, Ticket } from '../index';

export interface TaskManagementConnection extends BaseModel {
/** Returns true when the connection is healthy and ready to create tickets. */
isActive(): boolean;
/**
* Persists status = 'requires_reauth'. Call this after a failed token refresh
* so the UI can prompt the user to reconnect.
*/
markRequiresReauth(): Promise<TaskManagementConnection>;
/** Persists status = 'disabled'. */
markDisabled(): Promise<TaskManagementConnection>;
/** Persists status = 'error' after repeated API failures. */
markError(): Promise<TaskManagementConnection>;
/** Persists status = 'disconnected' (soft-delete on user revoke). */
markDisconnected(): Promise<TaskManagementConnection>;

getConnectedBy(): string;
getDisplayName(): string;
getInstanceUrl(): string;
getMetadata(): object;
getOrganizationId(): string;
getProvider(): string;
getStatus(): string;
getTickets(): Promise<Ticket[]>;

setMetadata(metadata: object): TaskManagementConnection;
setStatus(status: string): TaskManagementConnection;
}

export interface TaskManagementConnectionCollection extends BaseCollection<TaskManagementConnection> {
/**
* Returns the active connection for an org + provider pair used by the
* ticket-creation API before every ticket request, or null if none exists.
*/
findActiveByOrganizationAndProvider(
organizationId: string,
provider: string,
): Promise<TaskManagementConnection | null>;

allByOrganizationId(organizationId: string): Promise<TaskManagementConnection[]>;
allByOrganizationIdAndProvider(
organizationId: string,
provider: string,
): Promise<TaskManagementConnection[]>;
allByOrganizationIdAndProviderAndStatus(
organizationId: string,
provider: string,
status: string,
): Promise<TaskManagementConnection[]>;
findByOrganizationIdAndProviderAndStatus(
organizationId: string,
provider: string,
status: string,
): Promise<TaskManagementConnection | null>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* Copyright 2024 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* 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.
*/

import TaskManagementConnection from './task-management-connection.model.js';
import TaskManagementConnectionCollection from './task-management-connection.collection.js';
import { validateMetadata } from './metadata-validator.js';

export {
TaskManagementConnection,
TaskManagementConnectionCollection,
validateMetadata,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/*
* Copyright 2024 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* 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.
*/

import { ValidationError } from '../../errors/index.js';

// UUID regex used by the spec for cloudId format validation.
const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;

/**
* Per-provider metadata schemas (mirrors spec §Metadata Validation Strategy).
*
* Each schema defines:
* required — fields that MUST be present
* properties — per-field validators (functions that return an error string or null)
* allowed — exhaustive list of permitted keys (enforces additionalProperties: false)
*
* Design: plain JS instead of ajv so no new production dependency is needed.
* The logic is equivalent to the spec's JSON Schema: required fields, a UUID
* pattern constraint, and additionalProperties: false.
*/
const METADATA_SCHEMAS = {
jira_cloud: {
// Aligns with mysticat-data-service PR #720:
// - cloudId (required) is Atlassian's stable workspace UUID used to build API URLs;
// 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).

// - siteName and siteUrl are NOT stored in metadata — they live in the dedicated
// display_name and instance_url columns (see PR #720 mysticat-data-service).
required: ['cloudId'],
allowed: new Set(['cloudId', 'scopes']),
properties: {
cloudId: (v) => (UUID_REGEX.test(v) ? null : 'cloudId must be a valid UUID'),
scopes: (v) => (Array.isArray(v) && v.every((s) => typeof s === 'string')
? null
: 'scopes must be an array of strings'),
},
},
jira_corp: {
required: ['baseUrl'],
allowed: new Set(['baseUrl', 'projectCategory']),
properties: {
baseUrl: (v) => (typeof v === 'string' && v.startsWith('https://') ? null : 'baseUrl must be a valid https:// URI'),
projectCategory: (v) => (typeof v === 'string' ? null : 'projectCategory must be a string'),
},
},
};

/**
* Validates provider-specific connection metadata before a DB write.
*
* Called on connection INSERT and UPDATE (auth-service path and future edit API).
* Unknown providers are rejected — no silent passthrough.
*
* @param {string} provider - e.g. 'jira_cloud'
* @param {object} metadata - The JSONB metadata object to validate
* @throws {ValidationError} On missing fields, wrong types, unknown keys, or unknown provider
*/
export function validateMetadata(provider, metadata) {
const schema = METADATA_SCHEMAS[provider];
if (!schema) {
// Providers without a schema (asana, workfront) are v2 placeholders — reject
// all writes until a schema is defined so incomplete data never reaches the DB.
throw new ValidationError(`No metadata schema for provider: ${provider}`);
}

const { required, allowed, properties } = schema;

for (const field of required) {
if (metadata[field] === undefined || metadata[field] === null) {
throw new ValidationError(`metadata.${field} is required`);
}
}

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): validateMetadata does not guard against non-object input. When metadata is null, undefined, a string, or a number, the function throws a TypeError (from metadata[field] access or Object.keys(metadata)) instead of a clean ValidationError.

Since this function is exported and called by auth-service on INSERT/UPDATE, a malformed request body will surface as an unhandled TypeError rather than a catchable validation rejection.

Fix:

// At the top of validateMetadata(), before the schema lookup:
if (metadata == null || typeof metadata !== 'object' || Array.isArray(metadata)) {
  throw new ValidationError('metadata must be a non-null object');
}

for (const [field, validate] of Object.entries(properties)) {
if (metadata[field] !== undefined) {
const err = validate(metadata[field]);
if (err) {
throw new ValidationError(`Invalid metadata: ${err}`);
}
}
}

// additionalProperties: false — reject any key not in the allowed set
const extraKeys = Object.keys(metadata).filter((k) => !allowed.has(k));
if (extraKeys.length > 0) {
throw new ValidationError(`Unexpected metadata properties for ${provider}: ${extraKeys.join(', ')}`);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* Copyright 2024 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* 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.
*/

import { isValidUUID } from '@adobe/spacecat-shared-utils';

import { ValidationError } from '../../errors/index.js';
import BaseCollection from '../base/base.collection.js';
import TaskManagementConnection from './task-management-connection.model.js';

/**
* TaskManagementConnectionCollection — manages TaskManagementConnection entities.
*
* Key query the ticket-creation API relies on:
* `findActiveByOrganizationAndProvider(orgId, provider)` — returns the single
* active connection for a given org + provider pair, or null if none exists.
*
* @class TaskManagementConnectionCollection
* @extends BaseCollection
*/
class TaskManagementConnectionCollection extends BaseCollection {
static COLLECTION_NAME = 'TaskManagementConnectionCollection';

/**
* Returns the single active connection for an organization and provider, or
* null when the org has not connected that provider (or the connection is
* in a degraded / disconnected state).
*
* The API layer calls this before every ticket-creation request and returns
* 409 Conflict when no active connection is found.
*
* @param {string} organizationId - The organization UUID.
* @param {string} provider - The provider key, e.g. 'jira_cloud'.
* @returns {Promise<TaskManagementConnection|null>}
* @throws {ValidationError} When organizationId or provider is missing.
*/
async findActiveByOrganizationAndProvider(organizationId, provider) {
if (!isValidUUID(organizationId)) {
throw new ValidationError('organizationId must be a valid UUID', this);
}
if (!provider) {
throw new ValidationError('provider is required', this);
}

return this.findByOrganizationIdAndProviderAndStatus(
organizationId,
provider,
TaskManagementConnection.STATUSES.ACTIVE,
);
}
}

export default TaskManagementConnectionCollection;
Loading
Loading