@adobe/spacecat-shared-data-access 3.79.1 → 3.81.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.
Files changed (37) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/package.json +1 -1
  3. package/src/models/base/entity.registry.js +15 -0
  4. package/src/models/feedback-event/feedback-event.constants.js +249 -0
  5. package/src/models/feedback-event/index.d.ts +85 -0
  6. package/src/models/feedback-event/index.js +34 -0
  7. package/src/models/idempotency-key/idempotency-key.collection.js +89 -0
  8. package/src/models/idempotency-key/idempotency-key.model.js +41 -0
  9. package/src/models/idempotency-key/idempotency-key.schema.js +51 -0
  10. package/src/models/idempotency-key/index.d.ts +41 -0
  11. package/src/models/idempotency-key/index.js +19 -0
  12. package/src/models/index.d.ts +5 -0
  13. package/src/models/index.js +6 -0
  14. package/src/models/oauth-nonce/index.d.ts +26 -0
  15. package/src/models/oauth-nonce/index.js +19 -0
  16. package/src/models/oauth-nonce/oauth-nonce.collection.js +59 -0
  17. package/src/models/oauth-nonce/oauth-nonce.model.js +35 -0
  18. package/src/models/oauth-nonce/oauth-nonce.schema.js +50 -0
  19. package/src/models/organization/index.d.ts +3 -1
  20. package/src/models/organization/organization.schema.js +1 -0
  21. package/src/models/task-management-connection/index.d.ts +82 -0
  22. package/src/models/task-management-connection/index.js +21 -0
  23. package/src/models/task-management-connection/metadata-validator.js +97 -0
  24. package/src/models/task-management-connection/task-management-connection.collection.js +61 -0
  25. package/src/models/task-management-connection/task-management-connection.model.js +119 -0
  26. package/src/models/task-management-connection/task-management-connection.schema.js +111 -0
  27. package/src/models/ticket/index.d.ts +40 -0
  28. package/src/models/ticket/index.js +19 -0
  29. package/src/models/ticket/ticket.collection.js +30 -0
  30. package/src/models/ticket/ticket.model.js +44 -0
  31. package/src/models/ticket/ticket.schema.js +72 -0
  32. package/src/models/ticket-suggestion/index.d.ts +26 -0
  33. package/src/models/ticket-suggestion/index.js +19 -0
  34. package/src/models/ticket-suggestion/ticket-suggestion.collection.js +29 -0
  35. package/src/models/ticket-suggestion/ticket-suggestion.model.js +36 -0
  36. package/src/models/ticket-suggestion/ticket-suggestion.schema.js +53 -0
  37. package/src/service/index.d.ts +10 -0
@@ -30,6 +30,8 @@ export type * from './import-url';
30
30
  export type * from './key-event';
31
31
  export type * from './latest-audit';
32
32
  export type * from './opportunity';
33
+ export type * from './idempotency-key';
34
+ export type * from './oauth-nonce';
33
35
  export type * from './organization';
34
36
  export type * from './page-citability';
35
37
  export type * from './page-intent';
@@ -40,6 +42,9 @@ export type * from './scrape-job';
40
42
  export type * from './scrape-url';
41
43
  export type * from './sentiment-guideline';
42
44
  export type * from './sentiment-topic';
45
+ export type * from './task-management-connection';
46
+ export type * from './ticket';
47
+ export type * from './ticket-suggestion';
43
48
  export type * from './site';
44
49
  export type * from './site-candidate';
45
50
  export type * from './site-enrollment';
@@ -26,6 +26,7 @@ export * from './geo-experiment/index.js';
26
26
  export * from './fix-entity/index.js';
27
27
  export * from './fix-entity-suggestion/index.js';
28
28
  export * from './experiment/index.js';
29
+ export * from './feedback-event/index.js';
29
30
  export * from './import-job/index.js';
30
31
  export * from './import-url/index.js';
31
32
  export * from './key-event/index.js';
@@ -53,3 +54,8 @@ export * from './page-citability/index.js';
53
54
  export * from './plg-onboarding/index.js';
54
55
  export * from './sentiment-guideline/index.js';
55
56
  export * from './sentiment-topic/index.js';
57
+ export * from './idempotency-key/index.js';
58
+ export * from './oauth-nonce/index.js';
59
+ export * from './task-management-connection/index.js';
60
+ export * from './ticket/index.js';
61
+ export * from './ticket-suggestion/index.js';
@@ -0,0 +1,26 @@
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 { BaseCollection, BaseModel } from '../index';
14
+
15
+ export interface OAuthNonce extends BaseModel {
16
+ getNonce(): string;
17
+ getExpiresAt(): string;
18
+ }
19
+
20
+ export interface OAuthNonceCollection extends BaseCollection<OAuthNonce> {
21
+ /**
22
+ * Atomically deletes a nonce by its value.
23
+ * Returns the number of rows deleted (1 = consumed, 0 = not found or already consumed).
24
+ */
25
+ delete(keys: { nonce: string }): Promise<number>;
26
+ }
@@ -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 OAuthNonce from './oauth-nonce.model.js';
14
+ import OAuthNonceCollection from './oauth-nonce.collection.js';
15
+
16
+ export {
17
+ OAuthNonce,
18
+ OAuthNonceCollection,
19
+ };
@@ -0,0 +1,59 @@
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
+ * OAuthNonceCollection — manages OAuthNonce records.
17
+ *
18
+ * @class OAuthNonceCollection
19
+ * @extends BaseCollection
20
+ */
21
+ class OAuthNonceCollection extends BaseCollection {
22
+ static COLLECTION_NAME = 'OAuthNonceCollection';
23
+
24
+ /**
25
+ * Atomically consumes a nonce: deletes it only if it exists AND has not expired.
26
+ *
27
+ * Mirrors the intended DB operation from the migration:
28
+ * DELETE FROM oauth_nonces WHERE nonce = $1 AND expires_at > NOW() RETURNING id
29
+ *
30
+ * Returns the number of rows deleted:
31
+ * 1 = consumed (nonce was valid and not expired)
32
+ * 0 = not found, already consumed, OR expired
33
+ *
34
+ * Used by auth-service to enforce single-use replay prevention at OAuth callback time.
35
+ * An expired nonce must return 0 (rejected) even if the row still exists in the DB —
36
+ * the background cleanup job may not have swept it yet.
37
+ *
38
+ * @param {object} keys
39
+ * @param {string} keys.nonce - The nonce value to consume.
40
+ * @returns {Promise<number>} 1 if the nonce was found and deleted, 0 otherwise.
41
+ */
42
+ async delete({ nonce } = {}) {
43
+ if (!nonce || typeof nonce !== 'string') {
44
+ throw new Error('nonce is required and must be a non-empty string');
45
+ }
46
+ const { data, error } = await this.postgrestService
47
+ .from(this.tableName)
48
+ .delete()
49
+ .eq('nonce', nonce)
50
+ .gt('expires_at', new Date().toISOString())
51
+ .select();
52
+ if (error) {
53
+ throw error;
54
+ }
55
+ return (data ?? []).length;
56
+ }
57
+ }
58
+
59
+ export default OAuthNonceCollection;
@@ -0,0 +1,35 @@
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
+ * OAuthNonce — a single-use state token used for OAuth 2.0 CSRF/replay prevention.
17
+ *
18
+ * Lifecycle:
19
+ * 1. Created by auth-service at the start of an OAuth authorization flow.
20
+ * 2. Consumed atomically (via OAuthNonceCollection.delete) when the provider
21
+ * redirects back to the callback endpoint.
22
+ * 3. If the nonce cannot be consumed the callback is rejected (replay attack).
23
+ *
24
+ * Rows are short-lived (TTL ≈ 10 minutes). A background cleanup job can sweep
25
+ * expired rows, but replay protection does not depend on cleanup — the nonce is
26
+ * deleted on first use regardless of expiresAt.
27
+ *
28
+ * @class OAuthNonce
29
+ * @extends BaseModel
30
+ */
31
+ class OAuthNonce extends BaseModel {
32
+ static ENTITY_NAME = 'OAuthNonce';
33
+ }
34
+
35
+ export default OAuthNonce;
@@ -0,0 +1,50 @@
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 } from '@adobe/spacecat-shared-utils';
16
+
17
+ import SchemaBuilder from '../base/schema.builder.js';
18
+ import OAuthNonce from './oauth-nonce.model.js';
19
+ import OAuthNonceCollection from './oauth-nonce.collection.js';
20
+
21
+ // nonce is the state parameter sent to the OAuth provider and consumed exactly once
22
+ // at callback time to prevent CSRF/replay attacks. The index on nonce powers the
23
+ // OAuthNonceCollection.delete({ nonce }) lookup used by auth-service at callback time.
24
+ const schema = new SchemaBuilder(OAuthNonce, OAuthNonceCollection)
25
+ // oauth_nonces is append-only — the DB grants no UPDATE privilege (postgrest_anon and
26
+ // postgrest_writer both lack UPDATE). Disabling updates at the model layer surfaces a
27
+ // clean ValidationError instead of an opaque PostgREST 403 at the DB level.
28
+ .allowUpdates(false)
29
+ // oauth_nonces is append-only — no updated_at or updated_by columns in the DB.
30
+ // Suppress the SchemaBuilder auto-added attributes so they are not included in INSERTs.
31
+ .addAttribute('updatedAt', {
32
+ type: 'string', required: false, readOnly: true, postgrestIgnore: true,
33
+ })
34
+ .addAttribute('updatedBy', { type: 'string', required: false, postgrestIgnore: true })
35
+ .addAttribute('nonce', {
36
+ type: 'string',
37
+ required: true,
38
+ readOnly: true,
39
+ })
40
+ .addAttribute('expiresAt', {
41
+ type: 'string',
42
+ required: true,
43
+ validate: (value) => isIsoDate(value),
44
+ })
45
+ .addIndex(
46
+ { composite: ['nonce'] },
47
+ { composite: [] },
48
+ );
49
+
50
+ export default schema.build();
@@ -11,7 +11,8 @@
11
11
  */
12
12
 
13
13
  import type {
14
- BaseCollection, BaseModel, Site, Project, Entitlement, OrganizationIdentityProvider, TrialUser,
14
+ BaseCollection, BaseModel, Site, Project, Entitlement, OrganizationIdentityProvider,
15
+ TaskManagementConnection, TrialUser,
15
16
  } from '../index';
16
17
 
17
18
  export interface Organization extends BaseModel {
@@ -24,6 +25,7 @@ export interface Organization extends BaseModel {
24
25
  getProjects(): Promise<Project[]>;
25
26
  getEntitlements(): Promise<Entitlement[]>;
26
27
  getOrganizationIdentityProviders(): Promise<OrganizationIdentityProvider[]>;
28
+ getTaskManagementConnections(): Promise<TaskManagementConnection[]>;
27
29
  getTrialUsers(): Promise<TrialUser[]>;
28
30
  setConfig(config: object): Organization;
29
31
  setFulfillableItems(fulfillableItems: object): Organization;
@@ -25,6 +25,7 @@ const schema = new SchemaBuilder(Organization, OrganizationCollection)
25
25
  .addReference('has_many', 'Projects')
26
26
  .addReference('has_many', 'Entitlements')
27
27
  .addReference('has_many', 'TrialUsers')
28
+ .addReference('has_many', 'TaskManagementConnections')
28
29
  .addAttribute('config', {
29
30
  type: 'any',
30
31
  required: true,
@@ -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;