@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
package/CHANGELOG.md CHANGED
@@ -1,3 +1,15 @@
1
+ ## [@adobe/spacecat-shared-data-access-v3.81.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-data-access-v3.80.0...@adobe/spacecat-shared-data-access-v3.81.0) (2026-07-02)
2
+
3
+ ### Features
4
+
5
+ * **SITES-44690:** add TaskManagementConnection, Ticket, TicketSuggestion, IdempotencyKey, and OAuthNonce data models ([#1702](https://github.com/adobe/spacecat-shared/issues/1702)) ([2ca5096](https://github.com/adobe/spacecat-shared/commit/2ca5096b448a07b8e365021e06b31fa3bee96b09))
6
+
7
+ ## [@adobe/spacecat-shared-data-access-v3.80.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-data-access-v3.79.1...@adobe/spacecat-shared-data-access-v3.80.0) (2026-07-02)
8
+
9
+ ### Features
10
+
11
+ * **data-access:** add feedback review constants + verdict/signal helpers (SITES-43974) ([#1696](https://github.com/adobe/spacecat-shared/issues/1696)) ([2cabb05](https://github.com/adobe/spacecat-shared/commit/2cabb0596b27dcdefaf66c7d46531403ae005f2c))
12
+
1
13
  ## [@adobe/spacecat-shared-data-access-v3.79.1](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-data-access-v3.79.0...@adobe/spacecat-shared-data-access-v3.79.1) (2026-06-22)
2
14
 
3
15
  ### Bug Fixes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adobe/spacecat-shared-data-access",
3
- "version": "3.79.1",
3
+ "version": "3.81.0",
4
4
  "description": "Shared modules of the Spacecat Services - Data Access",
5
5
  "type": "module",
6
6
  "engines": {
@@ -55,6 +55,11 @@ import SentimentGuidelineCollection from '../sentiment-guideline/sentiment-guide
55
55
  import SentimentTopicCollection from '../sentiment-topic/sentiment-topic.collection.js';
56
56
  import AccessGrantLogCollection from '../access-grant-log/access-grant-log.collection.js';
57
57
  import SiteImsOrgAccessCollection from '../site-ims-org-access/site-ims-org-access.collection.js';
58
+ import IdempotencyKeyCollection from '../idempotency-key/idempotency-key.collection.js';
59
+ import OAuthNonceCollection from '../oauth-nonce/oauth-nonce.collection.js';
60
+ import TaskManagementConnectionCollection from '../task-management-connection/task-management-connection.collection.js';
61
+ import TicketCollection from '../ticket/ticket.collection.js';
62
+ import TicketSuggestionCollection from '../ticket-suggestion/ticket-suggestion.collection.js';
58
63
 
59
64
  import ApiKeySchema from '../api-key/api-key.schema.js';
60
65
  import AsyncJobSchema from '../async-job/async-job.schema.js';
@@ -97,6 +102,11 @@ import SentimentGuidelineSchema from '../sentiment-guideline/sentiment-guideline
97
102
  import SentimentTopicSchema from '../sentiment-topic/sentiment-topic.schema.js';
98
103
  import AccessGrantLogSchema from '../access-grant-log/access-grant-log.schema.js';
99
104
  import SiteImsOrgAccessSchema from '../site-ims-org-access/site-ims-org-access.schema.js';
105
+ import IdempotencyKeySchema from '../idempotency-key/idempotency-key.schema.js';
106
+ import OAuthNonceSchema from '../oauth-nonce/oauth-nonce.schema.js';
107
+ import TaskManagementConnectionSchema from '../task-management-connection/task-management-connection.schema.js';
108
+ import TicketSchema from '../ticket/ticket.schema.js';
109
+ import TicketSuggestionSchema from '../ticket-suggestion/ticket-suggestion.schema.js';
100
110
 
101
111
  /**
102
112
  * EntityRegistry - A registry class responsible for managing entities, their schema and collection.
@@ -234,6 +244,11 @@ EntityRegistry.registerEntity(SentimentGuidelineSchema, SentimentGuidelineCollec
234
244
  EntityRegistry.registerEntity(SentimentTopicSchema, SentimentTopicCollection);
235
245
  EntityRegistry.registerEntity(AccessGrantLogSchema, AccessGrantLogCollection);
236
246
  EntityRegistry.registerEntity(SiteImsOrgAccessSchema, SiteImsOrgAccessCollection);
247
+ EntityRegistry.registerEntity(IdempotencyKeySchema, IdempotencyKeyCollection);
248
+ EntityRegistry.registerEntity(OAuthNonceSchema, OAuthNonceCollection);
249
+ EntityRegistry.registerEntity(TaskManagementConnectionSchema, TaskManagementConnectionCollection);
250
+ EntityRegistry.registerEntity(TicketSchema, TicketCollection);
251
+ EntityRegistry.registerEntity(TicketSuggestionSchema, TicketSuggestionCollection);
237
252
  EntityRegistry.defaultEntities = { ...EntityRegistry.entities };
238
253
 
239
254
  export default EntityRegistry;
@@ -0,0 +1,249 @@
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
+ /**
14
+ * @fileoverview Shared constants + helpers for the human-review feedback loop
15
+ * (SITES-43974). The `feedback_event` table has a client-supplied `event_id`
16
+ * primary key and is append-only, so it is NOT modelled as a data-access entity
17
+ * (the entity framework forces an auto-generated `id` PK). Producers and
18
+ * consumers — `spacecat-api-service` (capture + ?include=reviews composition)
19
+ * and `spacecat-jobs-dispatcher` (the daily JSONL exporter) — talk to the table
20
+ * via the raw `postgrestClient`. These constants + the verdict<->signal
21
+ * translation live here so the enum values and the JSONL contract never drift
22
+ * across those repos.
23
+ */
24
+
25
+ /**
26
+ * Capture surface. Derived server-side from the API route (never trusted from
27
+ * the request body — FR-10). Only `backoffice` is wired up for v1; `aso_ui` and
28
+ * `aemy_pr` are reserved.
29
+ */
30
+ export const REVIEW_SOURCES = Object.freeze({
31
+ BACKOFFICE: 'backoffice',
32
+ ASO_UI: 'aso_ui',
33
+ AEMY_PR: 'aemy_pr',
34
+ });
35
+
36
+ /**
37
+ * App-layer verdict — what the reviewer expresses in the UI / request body.
38
+ */
39
+ export const REVIEW_VERDICTS = Object.freeze({
40
+ UP: 'up',
41
+ DOWN: 'down',
42
+ });
43
+
44
+ /**
45
+ * Persisted training label on `feedback_event.signal`. Translated once from the
46
+ * verdict at capture time (see {@link verdictToSignal}).
47
+ */
48
+ export const REVIEW_SIGNALS = Object.freeze({
49
+ POSITIVE: 'positive',
50
+ NEGATIVE: 'negative',
51
+ });
52
+
53
+ /**
54
+ * Optional ESE category on a reject. `product_bug` rows are filtered OUT of the
55
+ * Learning Agent export (routed to Jira); `bad_recommendation` / `other` / NULL
56
+ * are exported.
57
+ */
58
+ export const REJECTION_CATEGORIES = Object.freeze({
59
+ PRODUCT_BUG: 'product_bug',
60
+ BAD_RECOMMENDATION: 'bad_recommendation',
61
+ OTHER: 'other',
62
+ });
63
+
64
+ /**
65
+ * Commerce tier of the reviewed site at review time.
66
+ */
67
+ export const FEEDBACK_TIERS = Object.freeze({
68
+ PAID: 'paid',
69
+ FREE: 'free',
70
+ });
71
+
72
+ /**
73
+ * Rejection categories whose rows must NOT be exported to the Learning Agent
74
+ * training corpus (they describe product/detection bugs the LA cannot learn
75
+ * from). The exporter filters these out; NULL-category rows are still exported.
76
+ */
77
+ export const EXPORT_EXCLUDED_REJECTION_CATEGORIES = Object.freeze([
78
+ REJECTION_CATEGORIES.PRODUCT_BUG,
79
+ ]);
80
+
81
+ /**
82
+ * Customer-derived fields stripped from the JSONL export for organizations with
83
+ * `training_opt_in = false`. Verdict / signal / category / identity metadata
84
+ * still ship. snake_case to match the JSONL row shape (and the feedback_event
85
+ * DB columns) — {@link toJsonlRow} uses this list to do the stripping, so the
86
+ * opt-out boundary is single-sourced here.
87
+ */
88
+ export const OPT_OUT_STRIPPED_FIELDS = Object.freeze([
89
+ 'previous_fix',
90
+ 'edited_fix',
91
+ 'detail_markdown',
92
+ ]);
93
+
94
+ /**
95
+ * Current JSONL row schema version (the `schema_version` field stamped on every
96
+ * exported line). DMAI ignores unknown fields within a known version and SHOULD
97
+ * reject unknown versions; bump this in one place on a breaking change.
98
+ */
99
+ export const SCHEMA_VERSION = 1;
100
+
101
+ /**
102
+ * Translate the app-layer verdict to the persisted training signal. Single
103
+ * source of truth shared by the capture handler and any consumer.
104
+ *
105
+ * @param {string} verdict - one of {@link REVIEW_VERDICTS}.
106
+ * @returns {string} the corresponding {@link REVIEW_SIGNALS} value.
107
+ * @throws {Error} if the verdict is not a recognised value.
108
+ */
109
+ export function verdictToSignal(verdict) {
110
+ if (verdict === REVIEW_VERDICTS.UP) {
111
+ return REVIEW_SIGNALS.POSITIVE;
112
+ }
113
+ if (verdict === REVIEW_VERDICTS.DOWN) {
114
+ return REVIEW_SIGNALS.NEGATIVE;
115
+ }
116
+ throw new Error(`Invalid review verdict: ${verdict}`);
117
+ }
118
+
119
+ /**
120
+ * Inverse of {@link verdictToSignal} — translate a persisted signal back to the
121
+ * app-layer verdict (used when composing reviews into an API response).
122
+ *
123
+ * @param {string} signal - one of {@link REVIEW_SIGNALS}.
124
+ * @returns {string} the corresponding {@link REVIEW_VERDICTS} value.
125
+ * @throws {Error} if the signal is not a recognised value.
126
+ */
127
+ export function signalToVerdict(signal) {
128
+ if (signal === REVIEW_SIGNALS.POSITIVE) {
129
+ return REVIEW_VERDICTS.UP;
130
+ }
131
+ if (signal === REVIEW_SIGNALS.NEGATIVE) {
132
+ return REVIEW_VERDICTS.DOWN;
133
+ }
134
+ throw new Error(`Invalid review signal: ${signal}`);
135
+ }
136
+
137
+ /**
138
+ * Map a raw `feedback_event` row (snake_case, as returned by PostgREST) to the
139
+ * **API review view** (camelCase, verdict-flavoured) for HTTP responses — e.g.
140
+ * the `?include=reviews` composition in spacecat-api-service. This is NOT the
141
+ * JSONL export shape; use {@link toJsonlRow} for the Learning Agent export. The
142
+ * heavy patch fields (`previous_fix` / `edited_fix`) are omitted unless
143
+ * `includePatches` is set.
144
+ *
145
+ * @param {object} row - a raw feedback_event row from PostgREST.
146
+ * @param {object} [options]
147
+ * @param {boolean} [options.includePatches=false] - include previous/edited fix.
148
+ * @returns {object|null} the review view object, or null when no row is given.
149
+ */
150
+ export function toReviewView(row, { includePatches = false } = {}) {
151
+ if (!row) {
152
+ return null;
153
+ }
154
+
155
+ const view = {
156
+ eventId: row.event_id,
157
+ eventTime: row.event_time,
158
+ source: row.source,
159
+ verdict: signalToVerdict(row.signal),
160
+ signal: row.signal,
161
+ reviewerId: row.reviewer_id ?? null,
162
+ detailMarkdown: row.detail_markdown ?? null,
163
+ rejectionCategory: row.rejection_category ?? null,
164
+ stateTransition: row.state_transition ?? null,
165
+ tier: row.tier,
166
+ };
167
+
168
+ if (includePatches) {
169
+ view.previousFix = row.previous_fix ?? null;
170
+ view.editedFix = row.edited_fix ?? null;
171
+ }
172
+
173
+ return view;
174
+ }
175
+
176
+ /**
177
+ * Whether a raw `feedback_event` row should be exported to the Learning Agent
178
+ * corpus. `product_bug` rejections are excluded (routed to Jira); NULL-category
179
+ * rows ARE exported. See {@link EXPORT_EXCLUDED_REJECTION_CATEGORIES}.
180
+ *
181
+ * @param {object} row - a raw feedback_event row from PostgREST.
182
+ * @returns {boolean} true if the row should be exported.
183
+ */
184
+ export function shouldExport(row) {
185
+ return !EXPORT_EXCLUDED_REJECTION_CATEGORIES.includes(row.rejection_category);
186
+ }
187
+
188
+ /**
189
+ * Map a raw `feedback_event` row (snake_case, from PostgREST) to one JSONL line
190
+ * object — the canonical Learning Agent export contract. The row mirrors the
191
+ * feedback_event columns (snake_case), stamped with {@link SCHEMA_VERSION} and
192
+ * the derived `verdict`. For organizations that have NOT opted into training,
193
+ * the customer-derived fields in {@link OPT_OUT_STRIPPED_FIELDS} are nulled;
194
+ * verdict / signal / category / identity metadata still ship (§10.5.7).
195
+ *
196
+ * This is the single source of truth for the JSONL row shape — the exporter
197
+ * (spacecat-jobs-dispatcher) calls it rather than re-deriving the contract.
198
+ *
199
+ * @param {object} row - a raw feedback_event row from PostgREST.
200
+ * @param {object} [options]
201
+ * @param {boolean} [options.optedIn=false] - the org's training_opt_in.
202
+ * @returns {object} a single JSONL line object (snake_case).
203
+ */
204
+ export function toJsonlRow(row, { optedIn = false } = {}) {
205
+ const jsonlRow = {
206
+ schema_version: SCHEMA_VERSION,
207
+ event_id: row.event_id,
208
+ event_time: row.event_time,
209
+ organization_id: row.organization_id,
210
+ site_id: row.site_id,
211
+ suggestion_id: row.suggestion_id,
212
+ opportunity_type: row.opportunity_type,
213
+ source: row.source,
214
+ signal: row.signal,
215
+ verdict: signalToVerdict(row.signal),
216
+ reviewer_id: row.reviewer_id ?? null,
217
+ rejection_category: row.rejection_category ?? null,
218
+ state_transition: row.state_transition ?? null,
219
+ tier: row.tier,
220
+ detail_markdown: row.detail_markdown ?? null,
221
+ previous_fix: row.previous_fix ?? null,
222
+ edited_fix: row.edited_fix ?? null,
223
+ };
224
+
225
+ if (!optedIn) {
226
+ OPT_OUT_STRIPPED_FIELDS.forEach((field) => {
227
+ jsonlRow[field] = null;
228
+ });
229
+ }
230
+
231
+ return jsonlRow;
232
+ }
233
+
234
+ /**
235
+ * Build the JSONL (newline-delimited JSON) export body from raw feedback_event
236
+ * rows: filters out non-exportable rows ({@link shouldExport}) and maps the rest
237
+ * through {@link toJsonlRow}.
238
+ *
239
+ * @param {Array<object>} rows - raw feedback_event rows from PostgREST.
240
+ * @param {object} [options]
241
+ * @param {boolean} [options.optedIn=false] - the org's training_opt_in.
242
+ * @returns {string} the JSONL body (no trailing newline).
243
+ */
244
+ export function buildJsonl(rows, { optedIn = false } = {}) {
245
+ return rows
246
+ .filter(shouldExport)
247
+ .map((row) => JSON.stringify(toJsonlRow(row, { optedIn })))
248
+ .join('\n');
249
+ }
@@ -0,0 +1,85 @@
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
+ export const REVIEW_SOURCES: Readonly<{
14
+ BACKOFFICE: 'backoffice';
15
+ ASO_UI: 'aso_ui';
16
+ AEMY_PR: 'aemy_pr';
17
+ }>;
18
+
19
+ export const REVIEW_VERDICTS: Readonly<{ UP: 'up'; DOWN: 'down' }>;
20
+
21
+ export const REVIEW_SIGNALS: Readonly<{ POSITIVE: 'positive'; NEGATIVE: 'negative' }>;
22
+
23
+ export const REJECTION_CATEGORIES: Readonly<{
24
+ PRODUCT_BUG: 'product_bug';
25
+ BAD_RECOMMENDATION: 'bad_recommendation';
26
+ OTHER: 'other';
27
+ }>;
28
+
29
+ export const FEEDBACK_TIERS: Readonly<{ PAID: 'paid'; FREE: 'free' }>;
30
+
31
+ export const EXPORT_EXCLUDED_REJECTION_CATEGORIES: ReadonlyArray<string>;
32
+
33
+ /** snake_case JSONL/DB field names stripped from the export for opted-out orgs. */
34
+ export const OPT_OUT_STRIPPED_FIELDS: ReadonlyArray<string>;
35
+
36
+ /** Current JSONL row schema version stamped on every exported line. */
37
+ export const SCHEMA_VERSION: number;
38
+
39
+ /** Translate the app-layer verdict (`up`/`down`) to the persisted signal. */
40
+ export function verdictToSignal(verdict: string): 'positive' | 'negative';
41
+
42
+ /** Translate a persisted signal (`positive`/`negative`) back to the verdict. */
43
+ export function signalToVerdict(signal: string): 'up' | 'down';
44
+
45
+ export interface ReviewView {
46
+ eventId: string;
47
+ eventTime: string;
48
+ source: string;
49
+ verdict: 'up' | 'down';
50
+ signal: 'positive' | 'negative';
51
+ reviewerId: string | null;
52
+ detailMarkdown: string | null;
53
+ rejectionCategory: string | null;
54
+ stateTransition: string | null;
55
+ tier: string;
56
+ previousFix?: unknown;
57
+ editedFix?: unknown;
58
+ }
59
+
60
+ /**
61
+ * Map a raw PostgREST `feedback_event` row to the API review view (HTTP
62
+ * responses). Returns null when no row is given. NOT for JSONL export.
63
+ */
64
+ export function toReviewView(
65
+ row: Record<string, unknown>,
66
+ options?: { includePatches?: boolean },
67
+ ): ReviewView | null;
68
+
69
+ /** Whether a raw feedback_event row should be exported to the LA corpus. */
70
+ export function shouldExport(row: Record<string, unknown>): boolean;
71
+
72
+ /**
73
+ * Map a raw PostgREST `feedback_event` row to one JSONL export line (snake_case,
74
+ * schema_version-stamped). Customer-derived fields are nulled when optedIn=false.
75
+ */
76
+ export function toJsonlRow(
77
+ row: Record<string, unknown>,
78
+ options?: { optedIn?: boolean },
79
+ ): Record<string, unknown>;
80
+
81
+ /** Build the JSONL export body (filtered + mapped) from raw feedback_event rows. */
82
+ export function buildJsonl(
83
+ rows: Array<Record<string, unknown>>,
84
+ options?: { optedIn?: boolean },
85
+ ): string;
@@ -0,0 +1,34 @@
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
+ // feedback_event has a client-supplied PK and is append-only, so it is NOT a
14
+ // data-access entity (no model/collection/schema/registry). This barrel only
15
+ // re-exports the shared constants + helpers consumed by spacecat-api-service
16
+ // (capture + ?include=reviews) and spacecat-jobs-dispatcher (the JSONL exporter):
17
+ // the enum vocabulary, the verdict<->signal translation, the API review view,
18
+ // and the single-sourced JSONL row contract.
19
+ export {
20
+ REVIEW_SOURCES,
21
+ REVIEW_VERDICTS,
22
+ REVIEW_SIGNALS,
23
+ REJECTION_CATEGORIES,
24
+ FEEDBACK_TIERS,
25
+ EXPORT_EXCLUDED_REJECTION_CATEGORIES,
26
+ OPT_OUT_STRIPPED_FIELDS,
27
+ SCHEMA_VERSION,
28
+ verdictToSignal,
29
+ signalToVerdict,
30
+ toReviewView,
31
+ shouldExport,
32
+ toJsonlRow,
33
+ buildJsonl,
34
+ } from './feedback-event.constants.js';
@@ -0,0 +1,89 @@
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 { hasText, isValidUUID } from '@adobe/spacecat-shared-utils';
14
+
15
+ import { DataAccessError, ValidationError } from '../../errors/index.js';
16
+ import BaseCollection from '../base/base.collection.js';
17
+
18
+ /**
19
+ * IdempotencyKeyCollection — manages IdempotencyKey entities.
20
+ *
21
+ * Auto-generated index query methods (via schema references):
22
+ * allByOrganizationId(organizationId) — all keys for an organization
23
+ *
24
+ * @class IdempotencyKeyCollection
25
+ * @extends BaseCollection
26
+ */
27
+ class IdempotencyKeyCollection extends BaseCollection {
28
+ static COLLECTION_NAME = 'IdempotencyKeyCollection';
29
+
30
+ /**
31
+ * Finds a non-expired idempotency key by its value and organization.
32
+ *
33
+ * Returns null when:
34
+ * - No key exists with that value for the organization
35
+ * - The key exists but has expired (expires_at < NOW())
36
+ *
37
+ * @param {string} key - The idempotency key value.
38
+ * @param {string} organizationId - The organization UUID.
39
+ * @returns {Promise<import('./idempotency-key.model.js').default|null>}
40
+ */
41
+ async findActiveKey(key, organizationId) {
42
+ if (!hasText(key)) {
43
+ throw new ValidationError('key is required', this);
44
+ }
45
+ if (!isValidUUID(organizationId)) {
46
+ throw new ValidationError('organizationId must be a valid UUID', this);
47
+ }
48
+
49
+ const { data, error } = await this.postgrestService
50
+ .from(this.tableName)
51
+ .select()
52
+ .eq('key', key)
53
+ .eq('organization_id', organizationId)
54
+ .gt('expires_at', new Date().toISOString())
55
+ .limit(1);
56
+
57
+ if (error) {
58
+ throw new DataAccessError('Failed to find active idempotency key', { entityName: 'IdempotencyKey' }, error);
59
+ }
60
+
61
+ if (!data || data.length === 0) {
62
+ return null;
63
+ }
64
+
65
+ return this.createInstanceFromRow(data[0]);
66
+ }
67
+
68
+ /**
69
+ * Deletes all expired idempotency keys.
70
+ * Called by the cleanup scheduler (every 5 minutes).
71
+ *
72
+ * @returns {Promise<number>} Number of expired keys deleted.
73
+ */
74
+ async deleteExpired() {
75
+ const { data, error } = await this.postgrestService
76
+ .from(this.tableName)
77
+ .delete()
78
+ .lt('expires_at', new Date().toISOString())
79
+ .select('id');
80
+
81
+ if (error) {
82
+ throw new DataAccessError('Failed to delete expired idempotency keys', { entityName: 'IdempotencyKey' }, error);
83
+ }
84
+
85
+ return (data ?? []).length;
86
+ }
87
+ }
88
+
89
+ export default IdempotencyKeyCollection;
@@ -0,0 +1,41 @@
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
+ * IdempotencyKey — deduplicates concurrent or retried requests using the
17
+ * Stripe idempotency pattern.
18
+ *
19
+ * Status lifecycle:
20
+ * processing → completed (cached success response)
21
+ * processing → failed (cached error response, client must generate new key to retry)
22
+ *
23
+ * Keys are scoped to (key, organizationId, endpoint) — cross-org collision is impossible
24
+ * and the same key can be used independently across different operations.
25
+ *
26
+ * Keys expire after 24 hours (or shorter for ephemeral locks like token refresh dedup).
27
+ *
28
+ * @class IdempotencyKey
29
+ * @extends BaseModel
30
+ */
31
+ class IdempotencyKey extends BaseModel {
32
+ static ENTITY_NAME = 'IdempotencyKey';
33
+
34
+ static STATUSES = {
35
+ PROCESSING: 'processing',
36
+ COMPLETED: 'completed',
37
+ FAILED: 'failed',
38
+ };
39
+ }
40
+
41
+ export default IdempotencyKey;
@@ -0,0 +1,51 @@
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 IdempotencyKey from './idempotency-key.model.js';
19
+ import IdempotencyKeyCollection from './idempotency-key.collection.js';
20
+
21
+ // idempotency_keys table has updated_at but no updated_by column.
22
+ const schema = new SchemaBuilder(IdempotencyKey, IdempotencyKeyCollection)
23
+ .addAttribute('updatedBy', { type: 'string', required: false, postgrestIgnore: true })
24
+ .addReference('belongs_to', 'Organization')
25
+ .addAttribute('key', {
26
+ type: 'string',
27
+ required: true,
28
+ readOnly: true,
29
+ })
30
+ .addAttribute('endpoint', {
31
+ type: 'string',
32
+ required: true,
33
+ readOnly: true,
34
+ })
35
+ .addAttribute('status', {
36
+ type: Object.values(IdempotencyKey.STATUSES),
37
+ required: true,
38
+ default: IdempotencyKey.STATUSES.PROCESSING,
39
+ })
40
+ .addAttribute('response', {
41
+ type: 'any',
42
+ required: false,
43
+ })
44
+ .addAttribute('expiresAt', {
45
+ type: 'string',
46
+ required: true,
47
+ readOnly: true,
48
+ validate: (value) => isIsoDate(value),
49
+ });
50
+
51
+ export default schema.build();
@@ -0,0 +1,41 @@
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, Organization } from '../index';
14
+
15
+ export interface IdempotencyKey extends BaseModel {
16
+ getEndpoint(): string;
17
+ getExpiresAt(): string;
18
+ getKey(): string;
19
+ getOrganization(): Promise<Organization>;
20
+ getOrganizationId(): string;
21
+ getResponse(): Record<string, unknown> | null;
22
+ getStatus(): string;
23
+
24
+ setResponse(response: Record<string, unknown> | null): IdempotencyKey;
25
+ setStatus(status: string): IdempotencyKey;
26
+ }
27
+
28
+ export interface IdempotencyKeyCollection extends BaseCollection<IdempotencyKey> {
29
+ allByOrganizationId(organizationId: string): Promise<IdempotencyKey[]>;
30
+
31
+ /**
32
+ * Finds a non-expired idempotency key by its value and organization.
33
+ * Returns null when no active key exists.
34
+ */
35
+ findActiveKey(key: string, organizationId: string): Promise<IdempotencyKey | null>;
36
+
37
+ /**
38
+ * Deletes all expired idempotency keys. Returns the number deleted.
39
+ */
40
+ deleteExpired(): Promise<number>;
41
+ }
@@ -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 IdempotencyKey from './idempotency-key.model.js';
14
+ import IdempotencyKeyCollection from './idempotency-key.collection.js';
15
+
16
+ export {
17
+ IdempotencyKey,
18
+ IdempotencyKeyCollection,
19
+ };