@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.
- package/CHANGELOG.md +12 -0
- package/package.json +1 -1
- package/src/models/base/entity.registry.js +15 -0
- package/src/models/feedback-event/feedback-event.constants.js +249 -0
- package/src/models/feedback-event/index.d.ts +85 -0
- package/src/models/feedback-event/index.js +34 -0
- package/src/models/idempotency-key/idempotency-key.collection.js +89 -0
- package/src/models/idempotency-key/idempotency-key.model.js +41 -0
- package/src/models/idempotency-key/idempotency-key.schema.js +51 -0
- package/src/models/idempotency-key/index.d.ts +41 -0
- package/src/models/idempotency-key/index.js +19 -0
- package/src/models/index.d.ts +5 -0
- package/src/models/index.js +6 -0
- package/src/models/oauth-nonce/index.d.ts +26 -0
- package/src/models/oauth-nonce/index.js +19 -0
- package/src/models/oauth-nonce/oauth-nonce.collection.js +59 -0
- package/src/models/oauth-nonce/oauth-nonce.model.js +35 -0
- package/src/models/oauth-nonce/oauth-nonce.schema.js +50 -0
- package/src/models/organization/index.d.ts +3 -1
- package/src/models/organization/organization.schema.js +1 -0
- package/src/models/task-management-connection/index.d.ts +82 -0
- package/src/models/task-management-connection/index.js +21 -0
- package/src/models/task-management-connection/metadata-validator.js +97 -0
- package/src/models/task-management-connection/task-management-connection.collection.js +61 -0
- package/src/models/task-management-connection/task-management-connection.model.js +119 -0
- package/src/models/task-management-connection/task-management-connection.schema.js +111 -0
- package/src/models/ticket/index.d.ts +40 -0
- package/src/models/ticket/index.js +19 -0
- package/src/models/ticket/ticket.collection.js +30 -0
- package/src/models/ticket/ticket.model.js +44 -0
- package/src/models/ticket/ticket.schema.js +72 -0
- package/src/models/ticket-suggestion/index.d.ts +26 -0
- package/src/models/ticket-suggestion/index.js +19 -0
- package/src/models/ticket-suggestion/ticket-suggestion.collection.js +29 -0
- package/src/models/ticket-suggestion/ticket-suggestion.model.js +36 -0
- package/src/models/ticket-suggestion/ticket-suggestion.schema.js +53 -0
- package/src/service/index.d.ts +10 -0
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
|
@@ -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
|
+
};
|