@adobe/spacecat-shared-data-access 3.80.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/package.json +1 -1
- package/src/models/async-job/index.d.ts +11 -0
- package/src/models/base/entity.registry.js +15 -0
- package/src/models/idempotency-key/idempotency-key.collection.js +89 -0
- package/src/models/idempotency-key/idempotency-key.model.js +41 -0
- package/src/models/idempotency-key/idempotency-key.schema.js +51 -0
- package/src/models/idempotency-key/index.d.ts +41 -0
- package/src/models/idempotency-key/index.js +19 -0
- package/src/models/index.d.ts +5 -0
- package/src/models/index.js +5 -0
- package/src/models/oauth-nonce/index.d.ts +26 -0
- package/src/models/oauth-nonce/index.js +19 -0
- package/src/models/oauth-nonce/oauth-nonce.collection.js +59 -0
- package/src/models/oauth-nonce/oauth-nonce.model.js +35 -0
- package/src/models/oauth-nonce/oauth-nonce.schema.js +50 -0
- package/src/models/organization/index.d.ts +3 -1
- package/src/models/organization/organization.schema.js +1 -0
- package/src/models/preflight/index.d.ts +4 -7
- package/src/models/preflight/preflight.schema.js +13 -20
- package/src/models/task-management-connection/index.d.ts +82 -0
- package/src/models/task-management-connection/index.js +21 -0
- package/src/models/task-management-connection/metadata-validator.js +97 -0
- package/src/models/task-management-connection/task-management-connection.collection.js +61 -0
- package/src/models/task-management-connection/task-management-connection.model.js +119 -0
- package/src/models/task-management-connection/task-management-connection.schema.js +111 -0
- package/src/models/ticket/index.d.ts +40 -0
- package/src/models/ticket/index.js +19 -0
- package/src/models/ticket/ticket.collection.js +30 -0
- package/src/models/ticket/ticket.model.js +44 -0
- package/src/models/ticket/ticket.schema.js +72 -0
- package/src/models/ticket-suggestion/index.d.ts +26 -0
- package/src/models/ticket-suggestion/index.js +19 -0
- package/src/models/ticket-suggestion/ticket-suggestion.collection.js +29 -0
- package/src/models/ticket-suggestion/ticket-suggestion.model.js +36 -0
- package/src/models/ticket-suggestion/ticket-suggestion.schema.js +53 -0
- package/src/service/index.d.ts +10 -0
|
@@ -17,6 +17,11 @@ import SchemaBuilder from '../base/schema.builder.js';
|
|
|
17
17
|
import Preflight from './preflight.model.js';
|
|
18
18
|
import PreflightCollection from './preflight.collection.js';
|
|
19
19
|
|
|
20
|
+
// SITES-47254: `startedAt`, `result`, and `error` live only on AsyncJob now —
|
|
21
|
+
// the underlying preflights table no longer carries them. Consumers fetch the
|
|
22
|
+
// joined AsyncJob (e.g., `await preflight.getAsyncJob()`) for lifecycle
|
|
23
|
+
// internals; `status` and `endedAt` remain here as a denormalized cache the
|
|
24
|
+
// projector keeps in sync.
|
|
20
25
|
const schema = new SchemaBuilder(Preflight, PreflightCollection)
|
|
21
26
|
.addReference('belongs_to', 'Site', [], { required: true })
|
|
22
27
|
.addReference('belongs_to', 'AsyncJob', [], { required: true })
|
|
@@ -30,34 +35,22 @@ const schema = new SchemaBuilder(Preflight, PreflightCollection)
|
|
|
30
35
|
required: true,
|
|
31
36
|
default: Preflight.Status.IN_PROGRESS,
|
|
32
37
|
})
|
|
33
|
-
// `createdBy`
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
38
|
+
// `createdBy` uses type 'any' because ElectroDB's `map` type requires a
|
|
39
|
+
// `properties` schema for every sub-key, and the validate function below
|
|
40
|
+
// already enforces the precise shape — a duplicate `properties` declaration
|
|
41
|
+
// adds nothing. Declaring `type: 'map'` here without `properties` was the
|
|
42
|
+
// original definition and throws `InvalidAttributeDefinition` at Service
|
|
43
|
+
// construction, blocking any downstream consumer that builds a v1
|
|
44
|
+
// `new Service(EntityRegistry.getEntities())` (e.g. spacecat-api-service
|
|
45
|
+
// `fixes.test.js`).
|
|
41
46
|
.addAttribute('createdBy', {
|
|
42
47
|
type: 'any',
|
|
43
48
|
required: true,
|
|
44
49
|
validate: (value) => isObject(value) && typeof value.email === 'string' && value.email.length > 0,
|
|
45
50
|
})
|
|
46
|
-
.addAttribute('startedAt', {
|
|
47
|
-
type: 'string',
|
|
48
|
-
validate: (value) => !value || isIsoDate(value),
|
|
49
|
-
})
|
|
50
51
|
.addAttribute('endedAt', {
|
|
51
52
|
type: 'string',
|
|
52
53
|
validate: (value) => !value || isIsoDate(value),
|
|
53
|
-
})
|
|
54
|
-
.addAttribute('result', {
|
|
55
|
-
type: 'any',
|
|
56
|
-
validate: (value) => !value || isObject(value),
|
|
57
|
-
})
|
|
58
|
-
.addAttribute('error', {
|
|
59
|
-
type: 'any',
|
|
60
|
-
validate: (value) => !value || (isObject(value) && typeof value.code === 'string' && value.code.length > 0 && typeof value.message === 'string' && value.message.length > 0),
|
|
61
54
|
});
|
|
62
55
|
|
|
63
56
|
export default schema.build();
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 Adobe. All rights reserved.
|
|
3
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
*
|
|
7
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
+
* governing permissions and limitations under the License.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type {
|
|
14
|
+
BaseCollection, BaseModel, Organization, Ticket,
|
|
15
|
+
} from '../index';
|
|
16
|
+
|
|
17
|
+
export interface TaskManagementConnection extends BaseModel {
|
|
18
|
+
/** Returns true when the connection is healthy and ready to create tickets. */
|
|
19
|
+
isActive(): boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Persists status = 'requires_reauth'. Call this after a failed token refresh
|
|
22
|
+
* so the UI can prompt the user to reconnect.
|
|
23
|
+
*/
|
|
24
|
+
markRequiresReauth(): Promise<TaskManagementConnection>;
|
|
25
|
+
/** Persists status = 'disabled'. */
|
|
26
|
+
markDisabled(): Promise<TaskManagementConnection>;
|
|
27
|
+
/** Persists status = 'error' after repeated API failures. */
|
|
28
|
+
markError(): Promise<TaskManagementConnection>;
|
|
29
|
+
/** Persists status = 'active' after a successful re-authorization. */
|
|
30
|
+
markActive(): Promise<TaskManagementConnection>;
|
|
31
|
+
/** Persists status = 'disconnected' (soft-delete on user revoke). */
|
|
32
|
+
markDisconnected(): Promise<TaskManagementConnection>;
|
|
33
|
+
|
|
34
|
+
getConnectedAt(): string | null;
|
|
35
|
+
getConnectedBy(): string;
|
|
36
|
+
getDisplayName(): string;
|
|
37
|
+
getErrorMessage(): string | null;
|
|
38
|
+
getExternalInstanceId(): string;
|
|
39
|
+
getInstanceUrl(): string;
|
|
40
|
+
getLastUsedAt(): string | null;
|
|
41
|
+
getMetadata(): object;
|
|
42
|
+
getOrganization(): Promise<Organization>;
|
|
43
|
+
getOrganizationId(): string;
|
|
44
|
+
getProvider(): string;
|
|
45
|
+
getStatus(): string;
|
|
46
|
+
getTickets(): Promise<Ticket[]>;
|
|
47
|
+
|
|
48
|
+
setConnectedAt(timestamp: string): TaskManagementConnection;
|
|
49
|
+
setDisplayName(name: string): TaskManagementConnection;
|
|
50
|
+
setErrorMessage(message: string | null): TaskManagementConnection;
|
|
51
|
+
setInstanceUrl(url: string): TaskManagementConnection;
|
|
52
|
+
setLastUsedAt(timestamp: string): TaskManagementConnection;
|
|
53
|
+
setMetadata(metadata: object): TaskManagementConnection;
|
|
54
|
+
setStatus(status: string): TaskManagementConnection;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface TaskManagementConnectionCollection extends BaseCollection<TaskManagementConnection> {
|
|
58
|
+
/**
|
|
59
|
+
* Returns the active connection for an org + provider pair used by the
|
|
60
|
+
* ticket-creation API before every ticket request, or null if none exists.
|
|
61
|
+
*/
|
|
62
|
+
findActiveByOrganizationAndProvider(
|
|
63
|
+
organizationId: string,
|
|
64
|
+
provider: string,
|
|
65
|
+
): Promise<TaskManagementConnection | null>;
|
|
66
|
+
|
|
67
|
+
allByOrganizationId(organizationId: string): Promise<TaskManagementConnection[]>;
|
|
68
|
+
allByOrganizationIdAndProvider(
|
|
69
|
+
organizationId: string,
|
|
70
|
+
provider: string,
|
|
71
|
+
): Promise<TaskManagementConnection[]>;
|
|
72
|
+
allByOrganizationIdAndProviderAndStatus(
|
|
73
|
+
organizationId: string,
|
|
74
|
+
provider: string,
|
|
75
|
+
status: string,
|
|
76
|
+
): Promise<TaskManagementConnection[]>;
|
|
77
|
+
findByOrganizationIdAndProviderAndStatus(
|
|
78
|
+
organizationId: string,
|
|
79
|
+
provider: string,
|
|
80
|
+
status: string,
|
|
81
|
+
): Promise<TaskManagementConnection | null>;
|
|
82
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 Adobe. All rights reserved.
|
|
3
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
*
|
|
7
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
+
* governing permissions and limitations under the License.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import TaskManagementConnection from './task-management-connection.model.js';
|
|
14
|
+
import TaskManagementConnectionCollection from './task-management-connection.collection.js';
|
|
15
|
+
import { validateMetadata } from './metadata-validator.js';
|
|
16
|
+
|
|
17
|
+
export {
|
|
18
|
+
TaskManagementConnection,
|
|
19
|
+
TaskManagementConnectionCollection,
|
|
20
|
+
validateMetadata,
|
|
21
|
+
};
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 Adobe. All rights reserved.
|
|
3
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
*
|
|
7
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
+
* governing permissions and limitations under the License.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { ValidationError } from '../../errors/index.js';
|
|
14
|
+
|
|
15
|
+
// UUID regex used by the spec for cloudId format validation.
|
|
16
|
+
const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Per-provider metadata schemas (mirrors spec §Metadata Validation Strategy).
|
|
20
|
+
*
|
|
21
|
+
* Each schema defines:
|
|
22
|
+
* required — fields that MUST be present
|
|
23
|
+
* properties — per-field validators (functions that return an error string or null)
|
|
24
|
+
* allowed — exhaustive list of permitted keys (enforces additionalProperties: false)
|
|
25
|
+
*
|
|
26
|
+
* Design: plain JS instead of ajv so no new production dependency is needed.
|
|
27
|
+
* The logic is equivalent to the spec's JSON Schema: required fields, a UUID
|
|
28
|
+
* pattern constraint, and additionalProperties: false.
|
|
29
|
+
*/
|
|
30
|
+
// v1: only jira_cloud is supported. Add jira_corp, asana, workfront schemas here
|
|
31
|
+
// when the corresponding provider value is added to TaskManagementConnection.PROVIDERS.
|
|
32
|
+
const METADATA_SCHEMAS = {
|
|
33
|
+
jira_cloud: {
|
|
34
|
+
// Aligns with mysticat-data-service PR #720:
|
|
35
|
+
// - cloudId (required) is Atlassian's stable workspace UUID used to build API URLs;
|
|
36
|
+
// enforced as UUID format by a DB CHECK constraint.
|
|
37
|
+
// - scopes (optional) is the array from the Atlassian accessible-resources response;
|
|
38
|
+
// stored so permission gaps can be detected without re-calling Atlassian (e.g. missing
|
|
39
|
+
// manage:jira-webhook when v2 webhooks land).
|
|
40
|
+
// - siteName and siteUrl are NOT stored in metadata — they live in the dedicated
|
|
41
|
+
// display_name and instance_url columns (see PR #720 mysticat-data-service).
|
|
42
|
+
required: ['cloudId'],
|
|
43
|
+
allowed: new Set(['cloudId', 'scopes']),
|
|
44
|
+
properties: {
|
|
45
|
+
cloudId: (v) => (UUID_REGEX.test(v) ? null : 'cloudId must be a valid UUID'),
|
|
46
|
+
scopes: (v) => (Array.isArray(v) && v.every((s) => typeof s === 'string')
|
|
47
|
+
? null
|
|
48
|
+
: 'scopes must be an array of strings'),
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Validates provider-specific connection metadata before a DB write.
|
|
55
|
+
*
|
|
56
|
+
* Called on connection INSERT and UPDATE (auth-service path and future edit API).
|
|
57
|
+
* Unknown providers are rejected — no silent passthrough.
|
|
58
|
+
*
|
|
59
|
+
* @param {string} provider - e.g. 'jira_cloud'
|
|
60
|
+
* @param {object} metadata - The JSONB metadata object to validate
|
|
61
|
+
* @throws {ValidationError} On missing fields, wrong types, unknown keys, or unknown provider
|
|
62
|
+
*/
|
|
63
|
+
export function validateMetadata(provider, metadata) {
|
|
64
|
+
const schema = METADATA_SCHEMAS[provider];
|
|
65
|
+
if (!schema) {
|
|
66
|
+
// Providers without a schema (asana, workfront) are v2 placeholders — reject
|
|
67
|
+
// all writes until a schema is defined so incomplete data never reaches the DB.
|
|
68
|
+
throw new ValidationError(`No metadata schema for provider: ${provider}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (metadata === null || metadata === undefined || typeof metadata !== 'object' || Array.isArray(metadata)) {
|
|
72
|
+
throw new ValidationError('metadata must be a non-null object');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const { required, allowed, properties } = schema;
|
|
76
|
+
|
|
77
|
+
for (const field of required) {
|
|
78
|
+
if (metadata[field] === undefined || metadata[field] === null) {
|
|
79
|
+
throw new ValidationError(`metadata.${field} is required`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
for (const [field, validate] of Object.entries(properties)) {
|
|
84
|
+
if (metadata[field] !== undefined) {
|
|
85
|
+
const err = validate(metadata[field]);
|
|
86
|
+
if (err) {
|
|
87
|
+
throw new ValidationError(`Invalid metadata: ${err}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// additionalProperties: false — reject any key not in the allowed set
|
|
93
|
+
const extraKeys = Object.keys(metadata).filter((k) => !allowed.has(k));
|
|
94
|
+
if (extraKeys.length > 0) {
|
|
95
|
+
throw new ValidationError(`Unexpected metadata properties for ${provider}: ${extraKeys.join(', ')}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 Adobe. All rights reserved.
|
|
3
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
*
|
|
7
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
+
* governing permissions and limitations under the License.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { isValidUUID } from '@adobe/spacecat-shared-utils';
|
|
14
|
+
|
|
15
|
+
import { ValidationError } from '../../errors/index.js';
|
|
16
|
+
import BaseCollection from '../base/base.collection.js';
|
|
17
|
+
import TaskManagementConnection from './task-management-connection.model.js';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* TaskManagementConnectionCollection — manages TaskManagementConnection entities.
|
|
21
|
+
*
|
|
22
|
+
* Key query the ticket-creation API relies on:
|
|
23
|
+
* `findActiveByOrganizationAndProvider(orgId, provider)` — returns the single
|
|
24
|
+
* active connection for a given org + provider pair, or null if none exists.
|
|
25
|
+
*
|
|
26
|
+
* @class TaskManagementConnectionCollection
|
|
27
|
+
* @extends BaseCollection
|
|
28
|
+
*/
|
|
29
|
+
class TaskManagementConnectionCollection extends BaseCollection {
|
|
30
|
+
static COLLECTION_NAME = 'TaskManagementConnectionCollection';
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Returns the single active connection for an organization and provider, or
|
|
34
|
+
* null when the org has not connected that provider (or the connection is
|
|
35
|
+
* in a degraded / disconnected state).
|
|
36
|
+
*
|
|
37
|
+
* The API layer calls this before every ticket-creation request and returns
|
|
38
|
+
* 409 Conflict when no active connection is found.
|
|
39
|
+
*
|
|
40
|
+
* @param {string} organizationId - The organization UUID.
|
|
41
|
+
* @param {string} provider - The provider key, e.g. 'jira_cloud'.
|
|
42
|
+
* @returns {Promise<TaskManagementConnection|null>}
|
|
43
|
+
* @throws {ValidationError} When organizationId or provider is missing.
|
|
44
|
+
*/
|
|
45
|
+
async findActiveByOrganizationAndProvider(organizationId, provider) {
|
|
46
|
+
if (!isValidUUID(organizationId)) {
|
|
47
|
+
throw new ValidationError('organizationId must be a valid UUID', this);
|
|
48
|
+
}
|
|
49
|
+
if (!provider) {
|
|
50
|
+
throw new ValidationError('provider is required', this);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return this.findByOrganizationIdAndProviderAndStatus(
|
|
54
|
+
organizationId,
|
|
55
|
+
provider,
|
|
56
|
+
TaskManagementConnection.STATUSES.ACTIVE,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export default TaskManagementConnectionCollection;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 Adobe. All rights reserved.
|
|
3
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
*
|
|
7
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
+
* governing permissions and limitations under the License.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import BaseModel from '../base/base.model.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* TaskManagementConnection — one OAuth connection from an organization to a
|
|
17
|
+
* task-management provider (e.g. Jira Cloud).
|
|
18
|
+
*
|
|
19
|
+
* Status lifecycle (per architecture spec):
|
|
20
|
+
* active → tokens are valid, tickets can be created
|
|
21
|
+
* disabled → admin-disabled; no tickets until re-enabled
|
|
22
|
+
* requires_reauth → refresh token expired/revoked, user must reconnect
|
|
23
|
+
* error → repeated API failures; connection degraded
|
|
24
|
+
* disconnected → explicitly deleted by the user (v1 soft-delete)
|
|
25
|
+
*
|
|
26
|
+
* Provider-specific config lives in `metadata` as jsonb (jira_cloud: { cloudId, scopes }).
|
|
27
|
+
* Display fields (siteName, siteUrl) live in the dedicated displayName/instanceUrl columns.
|
|
28
|
+
*
|
|
29
|
+
* @class TaskManagementConnection
|
|
30
|
+
* @extends BaseModel
|
|
31
|
+
*/
|
|
32
|
+
class TaskManagementConnection extends BaseModel {
|
|
33
|
+
static ENTITY_NAME = 'TaskManagementConnection';
|
|
34
|
+
|
|
35
|
+
/** Supported task-management providers. */
|
|
36
|
+
static PROVIDERS = {
|
|
37
|
+
JIRA_CLOUD: 'jira_cloud',
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Connection health statuses (per architecture spec PR #150).
|
|
42
|
+
*
|
|
43
|
+
* DISCONNECTED is a v1 extension — it represents the "deleted" lifecycle
|
|
44
|
+
* event as a soft-delete so audit history is preserved. The spec hard-deletes
|
|
45
|
+
* the row; v1 keeps it with status='disconnected' until a GC job removes it.
|
|
46
|
+
*/
|
|
47
|
+
static STATUSES = {
|
|
48
|
+
ACTIVE: 'active',
|
|
49
|
+
DISABLED: 'disabled',
|
|
50
|
+
REQUIRES_REAUTH: 'requires_reauth',
|
|
51
|
+
ERROR: 'error',
|
|
52
|
+
DISCONNECTED: 'disconnected',
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Returns true when this connection is healthy and ready to create tickets.
|
|
57
|
+
*
|
|
58
|
+
* @returns {boolean}
|
|
59
|
+
*/
|
|
60
|
+
isActive() {
|
|
61
|
+
return this.getStatus() === TaskManagementConnection.STATUSES.ACTIVE;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Marks the connection as active. Called by auth-service after a successful
|
|
66
|
+
* re-authorization to restore a connection from requires_reauth state.
|
|
67
|
+
*
|
|
68
|
+
* @returns {Promise<TaskManagementConnection>}
|
|
69
|
+
*/
|
|
70
|
+
async markActive() {
|
|
71
|
+
this.setStatus(TaskManagementConnection.STATUSES.ACTIVE);
|
|
72
|
+
return this.save();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Marks the connection as requiring re-authentication (e.g. after a failed
|
|
77
|
+
* token refresh). Persists immediately so other services see the degraded
|
|
78
|
+
* state without waiting for the next GC cycle.
|
|
79
|
+
*
|
|
80
|
+
* @returns {Promise<TaskManagementConnection>}
|
|
81
|
+
*/
|
|
82
|
+
async markRequiresReauth() {
|
|
83
|
+
this.setStatus(TaskManagementConnection.STATUSES.REQUIRES_REAUTH);
|
|
84
|
+
return this.save();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Marks the connection as disabled (e.g. admin-disabled).
|
|
89
|
+
*
|
|
90
|
+
* @returns {Promise<TaskManagementConnection>}
|
|
91
|
+
*/
|
|
92
|
+
async markDisabled() {
|
|
93
|
+
this.setStatus(TaskManagementConnection.STATUSES.DISABLED);
|
|
94
|
+
return this.save();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Marks the connection as in an error state (repeated API failures).
|
|
99
|
+
*
|
|
100
|
+
* @returns {Promise<TaskManagementConnection>}
|
|
101
|
+
*/
|
|
102
|
+
async markError() {
|
|
103
|
+
this.setStatus(TaskManagementConnection.STATUSES.ERROR);
|
|
104
|
+
return this.save();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Marks the connection as disconnected (user-initiated soft-delete).
|
|
109
|
+
* v1 preserves the row for audit; a GC job handles eventual hard deletion.
|
|
110
|
+
*
|
|
111
|
+
* @returns {Promise<TaskManagementConnection>}
|
|
112
|
+
*/
|
|
113
|
+
async markDisconnected() {
|
|
114
|
+
this.setStatus(TaskManagementConnection.STATUSES.DISCONNECTED);
|
|
115
|
+
return this.save();
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export default TaskManagementConnection;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 Adobe. All rights reserved.
|
|
3
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
*
|
|
7
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
+
* governing permissions and limitations under the License.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/* c8 ignore start */
|
|
14
|
+
|
|
15
|
+
import { isIsoDate, isValidUrl } from '@adobe/spacecat-shared-utils';
|
|
16
|
+
|
|
17
|
+
import SchemaBuilder from '../base/schema.builder.js';
|
|
18
|
+
import TaskManagementConnection from './task-management-connection.model.js';
|
|
19
|
+
import TaskManagementConnectionCollection from './task-management-connection.collection.js';
|
|
20
|
+
import { validateMetadata } from './metadata-validator.js';
|
|
21
|
+
|
|
22
|
+
// Sort key [provider, status] on the Organization GSI lets the collection method
|
|
23
|
+
// findActiveByOrganizationAndProvider() resolve to a single DB call:
|
|
24
|
+
// findByOrganizationIdAndProviderAndStatus(orgId, 'jira_cloud', 'active')
|
|
25
|
+
const schema = new SchemaBuilder(TaskManagementConnection, TaskManagementConnectionCollection)
|
|
26
|
+
// task_management_connections table has updated_at but no updated_by column. Suppress
|
|
27
|
+
// updatedBy so it is not included in INSERTs or UPDATEs.
|
|
28
|
+
.addAttribute('updatedBy', { type: 'string', required: false, postgrestIgnore: true })
|
|
29
|
+
.addReference('belongs_to', 'Organization', ['provider', 'status'])
|
|
30
|
+
.addReference('has_many', 'Tickets', ['updatedAt'], { removeDependents: true })
|
|
31
|
+
.addAttribute('provider', {
|
|
32
|
+
type: Object.values(TaskManagementConnection.PROVIDERS),
|
|
33
|
+
required: true,
|
|
34
|
+
readOnly: true,
|
|
35
|
+
})
|
|
36
|
+
.addAttribute('status', {
|
|
37
|
+
type: Object.values(TaskManagementConnection.STATUSES),
|
|
38
|
+
required: true,
|
|
39
|
+
default: TaskManagementConnection.STATUSES.ACTIVE,
|
|
40
|
+
})
|
|
41
|
+
// display_name column (PR #720): human-readable site name from Atlassian accessible-resources.
|
|
42
|
+
// Set by auth-service at OAuth callback time; updated on re-auth (user may reconnect to a
|
|
43
|
+
// different Jira site or Atlassian may rename the site). Not readOnly — setDisplayName() needed.
|
|
44
|
+
.addAttribute('displayName', {
|
|
45
|
+
type: 'string',
|
|
46
|
+
required: true,
|
|
47
|
+
validate: (value) => typeof value === 'string' && value.length > 0 && value.length <= 255,
|
|
48
|
+
})
|
|
49
|
+
// instance_url column (PR #720): Jira site URL (https://*.atlassian.net).
|
|
50
|
+
// Display-only — never used as a request target (SSRF protection: all outbound
|
|
51
|
+
// calls route through the fixed Atlassian gateway keyed on cloudId from metadata).
|
|
52
|
+
// Updated on re-auth — not readOnly so setInstanceUrl() works.
|
|
53
|
+
.addAttribute('instanceUrl', {
|
|
54
|
+
type: 'string',
|
|
55
|
+
required: true,
|
|
56
|
+
validate: (value) => isValidUrl(value),
|
|
57
|
+
})
|
|
58
|
+
// connected_by column (PR #720): IMS user ID (JWT sub) of the person who completed OAuth.
|
|
59
|
+
.addAttribute('connectedBy', {
|
|
60
|
+
type: 'string',
|
|
61
|
+
required: true,
|
|
62
|
+
readOnly: true,
|
|
63
|
+
})
|
|
64
|
+
// connected_at column: when OAuth was last successfully completed.
|
|
65
|
+
// Set on initial connect, updated on re-auth. Differs from createdAt after reconnect.
|
|
66
|
+
.addAttribute('connectedAt', {
|
|
67
|
+
type: 'string',
|
|
68
|
+
required: false,
|
|
69
|
+
validate: (value) => !value || isIsoDate(value),
|
|
70
|
+
})
|
|
71
|
+
// external_instance_id column: provider-stable identifier for the remote workspace.
|
|
72
|
+
// jira_cloud → Atlassian cloudId UUID; jira_corp → normalized baseUrl (v2).
|
|
73
|
+
// Used as the dedup key in UNIQUE(organization_id, provider, external_instance_id).
|
|
74
|
+
// Never changes after connection is created — readOnly.
|
|
75
|
+
.addAttribute('externalInstanceId', {
|
|
76
|
+
type: 'string',
|
|
77
|
+
required: true,
|
|
78
|
+
readOnly: true,
|
|
79
|
+
validate: (value) => typeof value === 'string' && value.length > 0,
|
|
80
|
+
})
|
|
81
|
+
.addAttribute('lastUsedAt', {
|
|
82
|
+
type: 'string',
|
|
83
|
+
required: false,
|
|
84
|
+
validate: (value) => !value || isIsoDate(value),
|
|
85
|
+
})
|
|
86
|
+
.addAttribute('errorMessage', {
|
|
87
|
+
type: 'string',
|
|
88
|
+
required: false,
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
// metadata JSONB (PR #720): provider-specific structured data.
|
|
92
|
+
// jira_cloud: { cloudId (required UUID), scopes (optional string array) }.
|
|
93
|
+
// siteName and siteUrl are NOT stored here — they live in displayName/instanceUrl above.
|
|
94
|
+
// No default — callers must supply valid metadata (e.g. { cloudId: '...' } for
|
|
95
|
+
// jira_cloud). An empty-object default would silently bypass validateMetadata's
|
|
96
|
+
// required-field check at the schema level.
|
|
97
|
+
.addAttribute('metadata', {
|
|
98
|
+
type: 'any',
|
|
99
|
+
required: true,
|
|
100
|
+
set: (value, allAttrs) => {
|
|
101
|
+
// Validate metadata on every write — defence-in-depth alongside DB CHECK constraint.
|
|
102
|
+
// The provider attribute is readOnly, so it's always present in allAttrs after creation.
|
|
103
|
+
const provider = allAttrs?.provider;
|
|
104
|
+
if (provider) {
|
|
105
|
+
validateMetadata(provider, value);
|
|
106
|
+
}
|
|
107
|
+
return value;
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
export default schema.build();
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 Adobe. All rights reserved.
|
|
3
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
*
|
|
7
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
+
* governing permissions and limitations under the License.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type {
|
|
14
|
+
BaseCollection, BaseModel, Organization, Opportunity, TaskManagementConnection, TicketSuggestion,
|
|
15
|
+
} from '../index';
|
|
16
|
+
|
|
17
|
+
export interface Ticket extends BaseModel {
|
|
18
|
+
getCreatedBy(): string;
|
|
19
|
+
getOpportunity(): Promise<Opportunity | null>;
|
|
20
|
+
getOpportunityId(): string | undefined;
|
|
21
|
+
getOrganization(): Promise<Organization>;
|
|
22
|
+
getOrganizationId(): string;
|
|
23
|
+
getTaskManagementConnection(): Promise<TaskManagementConnection>;
|
|
24
|
+
getTaskManagementConnectionId(): string;
|
|
25
|
+
getExternalTicketId(): string;
|
|
26
|
+
getTicketKey(): string;
|
|
27
|
+
getTicketProvider(): string;
|
|
28
|
+
getTicketStatus(): string;
|
|
29
|
+
getTicketSuggestions(): Promise<TicketSuggestion[]>;
|
|
30
|
+
getTicketUrl(): string;
|
|
31
|
+
|
|
32
|
+
setTicketStatus(status: string): Ticket;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface TicketCollection extends BaseCollection<Ticket> {
|
|
36
|
+
allByOrganizationId(organizationId: string): Promise<Ticket[]>;
|
|
37
|
+
allByTaskManagementConnectionId(connectionId: string): Promise<Ticket[]>;
|
|
38
|
+
findByOpportunityId(opportunityId: string): Promise<Ticket | null>;
|
|
39
|
+
findByOpportunityIdAndTicketKey(opportunityId: string, ticketKey: string): Promise<Ticket | null>;
|
|
40
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 Adobe. All rights reserved.
|
|
3
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
*
|
|
7
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
+
* governing permissions and limitations under the License.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import Ticket from './ticket.model.js';
|
|
14
|
+
import TicketCollection from './ticket.collection.js';
|
|
15
|
+
|
|
16
|
+
export {
|
|
17
|
+
Ticket,
|
|
18
|
+
TicketCollection,
|
|
19
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 Adobe. All rights reserved.
|
|
3
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
*
|
|
7
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
+
* governing permissions and limitations under the License.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import BaseCollection from '../base/base.collection.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* TicketCollection — manages Ticket entities.
|
|
17
|
+
*
|
|
18
|
+
* Auto-generated index query methods (via schema GSIs):
|
|
19
|
+
* allByOrganizationId(orgId)
|
|
20
|
+
* allByTaskManagementConnectionId(connectionId)
|
|
21
|
+
* findByOpportunityId(opportunityId) — optional FK, nullable
|
|
22
|
+
*
|
|
23
|
+
* @class TicketCollection
|
|
24
|
+
* @extends BaseCollection
|
|
25
|
+
*/
|
|
26
|
+
class TicketCollection extends BaseCollection {
|
|
27
|
+
static COLLECTION_NAME = 'TicketCollection';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export default TicketCollection;
|