@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.
Files changed (37) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/package.json +1 -1
  3. package/src/models/async-job/index.d.ts +11 -0
  4. package/src/models/base/entity.registry.js +15 -0
  5. package/src/models/idempotency-key/idempotency-key.collection.js +89 -0
  6. package/src/models/idempotency-key/idempotency-key.model.js +41 -0
  7. package/src/models/idempotency-key/idempotency-key.schema.js +51 -0
  8. package/src/models/idempotency-key/index.d.ts +41 -0
  9. package/src/models/idempotency-key/index.js +19 -0
  10. package/src/models/index.d.ts +5 -0
  11. package/src/models/index.js +5 -0
  12. package/src/models/oauth-nonce/index.d.ts +26 -0
  13. package/src/models/oauth-nonce/index.js +19 -0
  14. package/src/models/oauth-nonce/oauth-nonce.collection.js +59 -0
  15. package/src/models/oauth-nonce/oauth-nonce.model.js +35 -0
  16. package/src/models/oauth-nonce/oauth-nonce.schema.js +50 -0
  17. package/src/models/organization/index.d.ts +3 -1
  18. package/src/models/organization/organization.schema.js +1 -0
  19. package/src/models/preflight/index.d.ts +4 -7
  20. package/src/models/preflight/preflight.schema.js +13 -20
  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
@@ -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` and `error` use type 'any' (matching neighbor `result`) because
34
- // ElectroDB's `map` type requires a `properties` schema for every sub-key,
35
- // and the validate function below already enforces the precise shape — a
36
- // duplicate `properties` declaration adds nothing. Declaring `type: 'map'`
37
- // here without `properties` was the original definition and throws
38
- // `InvalidAttributeDefinition` at Service construction, blocking any
39
- // downstream consumer that builds a v1 `new Service(EntityRegistry.getEntities())`
40
- // (e.g. spacecat-api-service `fixes.test.js`).
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;