-
Notifications
You must be signed in to change notification settings - Fork 2
feat(SITES-44690): add TaskManagementConnection, Ticket, TicketSuggestion, IdempotencyKey, and OAuthNonce data models #1702
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 11 commits
0d752bc
1670b27
ed28a09
470893a
9759dd1
fd9d00f
956066e
c259842
f16ed27
7066e87
1eadf94
9b00cf5
93ee359
e647141
ee44973
8a8f0f2
15613d5
33e771e
c06aac4
63b0b73
c6d15cf
cb77699
3a5cbc2
a331cc4
7be1031
e2c95cb
1d86ea2
332d9c5
eca5cb3
ad35864
623841d
84c7399
176dd8c
86f44bb
7e6fa58
15d0e43
787ea52
1702148
2a5e0dc
e54e75f
3c3dcc5
b772da1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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). | ||
| // - 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`); | ||
| } | ||
| } | ||
|
|
||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. issue (blocking): 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; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
issue (blocking): The
jira_corpmetadata schema here is architecturally unreachable. Theproviderattribute in the connection schema constrains valid values toObject.values(TaskManagementConnection.PROVIDERS)which is only['jira_cloud']. A record withprovider: '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_corpentry and its tests untilPROVIDERSis extended, or (b) addJIRA_CORP: 'jira_corp'toPROVIDERSnow if it is genuinely needed for the auth-service pre-validation path (and document that the validator is called independently of the model layer).